vigiles 2.6.0 → 3.0.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/.claude-plugin/plugin.json +9 -0
- package/README.md +74 -129
- package/action.yml +143 -7
- package/dist/action-gate.d.ts +1 -1
- package/dist/action-gate.js +1 -1
- package/dist/adapter-conformance.d.ts +30 -0
- package/dist/adapter-conformance.js +153 -0
- package/dist/adapter-registry.d.ts +42 -0
- package/dist/adapter-registry.js +55 -0
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.js +16 -0
- package/dist/adapters/claude-code/adapter.d.ts +3 -0
- package/dist/adapters/claude-code/adapter.js +46 -0
- package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
- package/dist/adapters/claude-code/dialect.d.ts +13 -0
- package/dist/adapters/claude-code/dialect.js +51 -0
- package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
- package/dist/adapters/claude-code/egress-entry.js +115 -0
- package/dist/adapters/claude-code/egress.d.ts +114 -0
- package/dist/adapters/claude-code/egress.js +276 -0
- package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
- package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
- package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
- package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
- package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
- package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
- package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
- package/dist/adapters/claude-code/hook-protocol.js +10 -0
- package/dist/adapters/claude-code/layout.d.ts +8 -0
- package/dist/adapters/claude-code/layout.js +18 -0
- package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
- package/dist/adapters/claude-code/model-mock.d.ts +11 -0
- package/dist/adapters/claude-code/model-mock.js +10 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
- package/dist/adapters/claude-code/plugin-loader.js +19 -0
- package/dist/{run-hook.d.ts → adapters/claude-code/run-hook.d.ts} +50 -3
- package/dist/{run-hook.js → adapters/claude-code/run-hook.js} +178 -18
- package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
- package/dist/adapters/claude-code/run-scripts.js +150 -0
- package/dist/adapters/claude-code/runtime.d.ts +16 -0
- package/dist/adapters/claude-code/runtime.js +39 -0
- package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +10 -0
- package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +5 -3
- package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
- package/dist/adapters/codex/adapter.d.ts +3 -0
- package/dist/adapters/codex/adapter.js +49 -0
- package/dist/adapters/codex/dialect.d.ts +10 -0
- package/dist/adapters/codex/dialect.js +30 -0
- package/dist/adapters/codex/driver.d.ts +20 -0
- package/dist/adapters/codex/driver.js +89 -0
- package/dist/adapters/codex/hook-protocol.d.ts +10 -0
- package/dist/adapters/codex/hook-protocol.js +18 -0
- package/dist/adapters/codex/layout.d.ts +16 -0
- package/dist/adapters/codex/layout.js +18 -0
- package/dist/adapters/codex/mock-model.d.ts +52 -0
- package/dist/adapters/codex/mock-model.js +210 -0
- package/dist/adapters/codex/model-mock.d.ts +11 -0
- package/dist/adapters/codex/model-mock.js +10 -0
- package/dist/adapters/codex/runtime.d.ts +38 -0
- package/dist/adapters/codex/runtime.js +62 -0
- package/dist/adapters/opencode/adapter.d.ts +3 -0
- package/dist/adapters/opencode/adapter.js +48 -0
- package/dist/adapters/opencode/dialect.d.ts +10 -0
- package/dist/adapters/opencode/dialect.js +36 -0
- package/dist/adapters/opencode/layout.d.ts +9 -0
- package/dist/adapters/opencode/layout.js +25 -0
- package/dist/adapters/opencode/model-mock.d.ts +10 -0
- package/dist/adapters/opencode/model-mock.js +10 -0
- package/dist/adapters/opencode/runtime.d.ts +9 -0
- package/dist/adapters/opencode/runtime.js +21 -0
- package/dist/claude-code.d.ts +8 -2
- package/dist/claude-code.js +8 -2
- package/dist/cli-flags.d.ts +22 -0
- package/dist/cli-flags.js +38 -0
- package/dist/cli.js +406 -206
- package/dist/codex.d.ts +20 -0
- package/dist/codex.js +36 -0
- package/dist/community-skills.d.ts +1 -1
- package/dist/community-skills.js +2 -2
- package/dist/core/adapter.d.ts +88 -0
- package/dist/core/adapter.js +3 -0
- package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
- package/dist/{compile.js → core/compile.js} +68 -53
- package/dist/core/compose.d.ts +79 -0
- package/dist/core/compose.js +145 -0
- package/dist/core/dialect.d.ts +51 -0
- package/dist/core/dialect.js +3 -0
- package/dist/core/harness-driver.d.ts +134 -0
- package/dist/core/harness-driver.js +3 -0
- package/dist/core/hook-protocol.d.ts +28 -0
- package/dist/core/hook-protocol.js +3 -0
- package/dist/core/layout.d.ts +43 -0
- package/dist/core/layout.js +3 -0
- package/dist/core/model-mock.d.ts +27 -0
- package/dist/core/model-mock.js +3 -0
- package/dist/core/refs.d.ts +62 -0
- package/dist/{refs.js → core/refs.js} +49 -23
- package/dist/core/runtime.d.ts +38 -0
- package/dist/core/runtime.js +3 -0
- package/dist/{types.d.ts → core/types.d.ts} +29 -0
- package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
- package/dist/{validate.js → core/validate.js} +41 -4
- package/dist/e2e.d.ts +19 -0
- package/dist/e2e.js +39 -0
- package/dist/harness-assert.d.ts +29 -14
- package/dist/harness-assert.js +37 -10
- package/dist/integration.d.ts +16 -0
- package/dist/integration.js +32 -0
- package/dist/leaderboard.d.ts +33 -0
- package/dist/leaderboard.js +107 -0
- package/dist/linting.d.ts +2 -2
- package/dist/linting.js +2 -2
- package/dist/plugin-loader.d.ts +6 -3
- package/dist/plugin-loader.js +101 -49
- package/dist/scan.d.ts +49 -0
- package/dist/scan.js +177 -0
- package/dist/setup-plan.d.ts +51 -0
- package/dist/setup-plan.js +85 -0
- package/dist/skill-test.d.ts +1 -1
- package/dist/skill-test.js +1 -1
- package/dist/test-coverage.d.ts +71 -0
- package/dist/test-coverage.js +228 -0
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -3
- package/dist/unit.d.ts +17 -0
- package/dist/unit.js +36 -0
- package/hooks/refs-nudge.sh +24 -0
- package/package.json +30 -15
- package/skills/audit-feedback-loop/SKILL.md +76 -0
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/enforce-rules-format/SKILL.md +71 -0
- package/skills/generate-logo/SKILL.md +103 -0
- package/skills/generate-rule/SKILL.md +64 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +126 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/skills/test-harness/SKILL.md +157 -0
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -180
- package/dist/refs.d.ts +0 -44
- package/dist/run-scripts.d.ts +0 -20
- package/dist/run-scripts.js +0 -70
- /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
- /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
- /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
- /package/dist/{egress-proxy.d.ts → adapters/claude-code/egress-proxy.d.ts} +0 -0
- /package/dist/{egress-proxy.js → adapters/claude-code/egress-proxy.js} +0 -0
- /package/dist/{eval-baseline.d.ts → adapters/claude-code/eval-baseline.d.ts} +0 -0
- /package/dist/{eval-baseline.js → adapters/claude-code/eval-baseline.js} +0 -0
- /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
- /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
- /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
- /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
- /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
- /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
- /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
- /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
- /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
- /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
- /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
- /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
- /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
- /package/dist/{coverage.js → core/coverage.js} +0 -0
- /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
- /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
- /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
- /package/dist/{evolve.js → core/evolve.js} +0 -0
- /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
- /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
- /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
- /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
- /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
- /package/dist/{generate-types.js → core/generate-types.js} +0 -0
- /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
- /package/dist/{hash.js → core/hash.js} +0 -0
- /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
- /package/dist/{inline.js → core/inline.js} +0 -0
- /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
- /package/dist/{integrity.js → core/integrity.js} +0 -0
- /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
- /package/dist/{linters.js → core/linters.js} +0 -0
- /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
- /package/dist/{mcp.js → core/mcp.js} +0 -0
- /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
- /package/dist/{orphans.js → core/orphans.js} +0 -0
- /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
- /package/dist/{proofs.js → core/proofs.js} +0 -0
- /package/dist/{session.d.ts → core/session.d.ts} +0 -0
- /package/dist/{session.js → core/session.js} +0 -0
- /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
- /package/dist/{sidecar.js → core/sidecar.js} +0 -0
- /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
- /package/dist/{spec.js → core/spec.js} +0 -0
- /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
- /package/dist/{symbols.js → core/symbols.js} +0 -0
- /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
- /package/dist/{test-utils.js → core/test-utils.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.probeEgressAvailable = probeEgressAvailable;
|
|
4
|
+
exports.egressAvailable = egressAvailable;
|
|
5
|
+
exports.parseGetent = parseGetent;
|
|
6
|
+
exports.resolveAllow = resolveAllow;
|
|
7
|
+
exports.parseResolvers = parseResolvers;
|
|
8
|
+
exports.buildEgressNft = buildEgressNft;
|
|
9
|
+
exports.buildEgressBwrapArgv = buildEgressBwrapArgv;
|
|
10
|
+
exports.parseNftCounters = parseNftCounters;
|
|
11
|
+
exports.countersToResult = countersToResult;
|
|
12
|
+
/**
|
|
13
|
+
* vigiles — allowlisted, recorded egress for a confined hook (`egress: { allow }`).
|
|
14
|
+
*
|
|
15
|
+
* The deny-all wall (`--unshare-all`) and the `recordEgress` proxy both BLOCK; a
|
|
16
|
+
* hook whose setup needs a real `npm install` can't be tested under either. This
|
|
17
|
+
* is the in-between: let traffic actually reach the network, but **only to an
|
|
18
|
+
* allowlist**, and **record** it — with the boundary at the packet layer so it
|
|
19
|
+
* can't be bypassed by a raw socket (an `HTTP_PROXY` allowlist can).
|
|
20
|
+
*
|
|
21
|
+
* How it works (proven in `research/spikes/sandbox-network-allowlist.sh`):
|
|
22
|
+
* `slirp4netns --configure` attaches a `tap` to the bwrap netns (rootless egress
|
|
23
|
+
* via a userspace TCP/IP stack), then an `nft` ruleset INSIDE the netns — a
|
|
24
|
+
* `policy drop` output chain that `accept`s only the resolved allowlist IPs (plus
|
|
25
|
+
* loopback + the DNS resolver) and `log`+`drop`s the rest — is the hard wall. The
|
|
26
|
+
* per-rule `counter`s read back which allowlisted hosts were actually reached and
|
|
27
|
+
* how much was dropped.
|
|
28
|
+
*
|
|
29
|
+
* This module holds the PURE seams — resolution parsing, ruleset generation,
|
|
30
|
+
* counter parsing, result mapping — so the enforcement logic is unit-tested
|
|
31
|
+
* without a sandbox. The orchestration (`src/egress-entry.ts`) needs real bwrap +
|
|
32
|
+
* slirp4netns + nft and is covered by the gated integration test.
|
|
33
|
+
*
|
|
34
|
+
* Honest limits (see `docs/sandboxing.md`): the allowlist is resolved to IPs at
|
|
35
|
+
* launch (a host whose DNS rotates outside the run's window could miss an IP —
|
|
36
|
+
* the dynamic resolver-pinned set is the documented next layer). The record names
|
|
37
|
+
* the allowlisted hosts that were reached and counts what was dropped, but does
|
|
38
|
+
* not yet name the DROPPED hosts (that needs the in-netns DNS-query log).
|
|
39
|
+
*/
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
let cachedEgressAvailable;
|
|
42
|
+
/** Does a binary resolve on PATH? (probe via `command -v`, no output kept.) */
|
|
43
|
+
function hasBinary(name) {
|
|
44
|
+
try {
|
|
45
|
+
return ((0, node_child_process_1.spawnSync)("sh", ["-c", `command -v ${name}`], {
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
timeout: 5_000,
|
|
48
|
+
}).status === 0);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* v8 ignore next -- spawnSync only throws on a fork failure */
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether this host can run the allowlisted-egress sandbox: it needs the same
|
|
57
|
+
* bubblewrap confinement the other tiers use PLUS `slirp4netns` (the rootless
|
|
58
|
+
* gateway) and `nft` (the packet-layer allowlist). Cached — the answer can't
|
|
59
|
+
* change within a run. `available` is injected so the bwrap probe isn't repeated.
|
|
60
|
+
*/
|
|
61
|
+
function probeEgressAvailable(available) {
|
|
62
|
+
/* v8 ignore next -- non-Linux has no bwrap/slirp; CI/coverage runs on Linux */
|
|
63
|
+
if (process.platform !== "linux")
|
|
64
|
+
return false;
|
|
65
|
+
return available && hasBinary("slirp4netns") && hasBinary("nft");
|
|
66
|
+
}
|
|
67
|
+
/** Cached {@link probeEgressAvailable}. `available` = the bwrap-sandbox probe. */
|
|
68
|
+
function egressAvailable(available) {
|
|
69
|
+
if (cachedEgressAvailable === undefined) {
|
|
70
|
+
cachedEgressAvailable = probeEgressAvailable(available);
|
|
71
|
+
}
|
|
72
|
+
return cachedEgressAvailable;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse `getent ahosts <host>` output into the unique IPs, split by family. The
|
|
76
|
+
* first whitespace token of each line is an address; `:` marks IPv6. Pure, so the
|
|
77
|
+
* resolution-parsing is unit-tested without touching DNS.
|
|
78
|
+
*/
|
|
79
|
+
function parseGetent(stdout) {
|
|
80
|
+
const v4 = new Set();
|
|
81
|
+
const v6 = new Set();
|
|
82
|
+
for (const line of stdout.split("\n")) {
|
|
83
|
+
const ip = line.trim().split(/\s+/)[0];
|
|
84
|
+
if (!ip)
|
|
85
|
+
continue;
|
|
86
|
+
if (ip.includes(":"))
|
|
87
|
+
v6.add(ip);
|
|
88
|
+
else if (/^\d+\.\d+\.\d+\.\d+$/.test(ip))
|
|
89
|
+
v4.add(ip);
|
|
90
|
+
}
|
|
91
|
+
return { v4: [...v4], v6: [...v6] };
|
|
92
|
+
}
|
|
93
|
+
/** Resolve a host to IPs via the system resolver (`getent`, synchronous). */
|
|
94
|
+
function getentLookup(host) {
|
|
95
|
+
/* v8 ignore start -- shells out to the real resolver; the parse is parseGetent */
|
|
96
|
+
const res = (0, node_child_process_1.spawnSync)("getent", ["ahosts", host], {
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
timeout: 10_000,
|
|
99
|
+
});
|
|
100
|
+
return parseGetent(res.stdout ?? "");
|
|
101
|
+
/* v8 ignore stop */
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve every allowlisted host to IPs. The lookup is injectable so the
|
|
105
|
+
* empty-result / family-split behaviour is unit-tested with a fake resolver.
|
|
106
|
+
*/
|
|
107
|
+
function resolveAllow(hosts, lookup = getentLookup) {
|
|
108
|
+
return hosts.map((host) => {
|
|
109
|
+
const { v4, v6 } = lookup(host);
|
|
110
|
+
return { host, v4, v6 };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** A loopback nameserver (127.0.0.0/8 or ::1) — e.g. systemd-resolved's
|
|
114
|
+
* 127.0.0.53 stub. Unreachable from inside the slirp4netns netns (which runs
|
|
115
|
+
* with `--disable-host-loopback`), so it must never be used as the in-netns
|
|
116
|
+
* resolver. GitHub-hosted runners ship exactly this stub, which is why the
|
|
117
|
+
* egress tests saw zero packets there. */
|
|
118
|
+
function isLoopbackResolver(ip) {
|
|
119
|
+
return ip === "::1" || /^127\./.test(ip);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Nameservers usable from INSIDE the egress netns. Parses a resolv.conf body but
|
|
123
|
+
* drops loopback stubs (see {@link isLoopbackResolver}) — they can't be reached
|
|
124
|
+
* across the namespace — and falls back to a public resolver (8.8.8.8, which
|
|
125
|
+
* slirp4netns NATs out) when nothing routable remains.
|
|
126
|
+
*/
|
|
127
|
+
function parseResolvers(resolvConf) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const line of resolvConf.split("\n")) {
|
|
130
|
+
const m = /^\s*nameserver\s+(\S+)/.exec(line);
|
|
131
|
+
if (m && !isLoopbackResolver(m[1]))
|
|
132
|
+
out.push(m[1]);
|
|
133
|
+
}
|
|
134
|
+
return out.length > 0 ? out : ["8.8.8.8"];
|
|
135
|
+
}
|
|
136
|
+
/** Escape an nft set comment label so a hostname can't break the ruleset. */
|
|
137
|
+
function nftLabel(host) {
|
|
138
|
+
return host.replace(/[^A-Za-z0-9._:-]/g, "_");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the nftables ruleset that enforces the allowlist INSIDE the netns: a
|
|
142
|
+
* `policy drop` output chain that accepts loopback, established replies, DNS to
|
|
143
|
+
* the resolvers, and each allowlisted host's resolved IPs (a per-host `counter` +
|
|
144
|
+
* `comment "allow:<host>"` so the read-back maps traffic to a name), then a
|
|
145
|
+
* catch-all `log`+`counter`+`drop`. v4 and v6 are separate rules (an `inet` set
|
|
146
|
+
* can't mix families) sharing the host's comment. Pure → the ruleset is asserted
|
|
147
|
+
* in a unit test.
|
|
148
|
+
*/
|
|
149
|
+
function buildEgressNft(opts) {
|
|
150
|
+
const lines = [
|
|
151
|
+
"table inet vig {",
|
|
152
|
+
" chain output {",
|
|
153
|
+
" type filter hook output priority 0; policy drop;",
|
|
154
|
+
' oifname "lo" accept',
|
|
155
|
+
" ct state established,related accept",
|
|
156
|
+
];
|
|
157
|
+
for (const r of opts.resolvers) {
|
|
158
|
+
const fam = r.includes(":") ? "ip6" : "ip";
|
|
159
|
+
lines.push(` ${fam} daddr ${r} udp dport 53 counter accept comment "dns"`);
|
|
160
|
+
lines.push(` ${fam} daddr ${r} tcp dport 53 counter accept comment "dns"`);
|
|
161
|
+
}
|
|
162
|
+
for (const h of opts.allow) {
|
|
163
|
+
const label = nftLabel(h.host);
|
|
164
|
+
if (h.v4.length > 0) {
|
|
165
|
+
lines.push(` ip daddr { ${h.v4.join(", ")} } counter accept comment "allow:${label}"`);
|
|
166
|
+
}
|
|
167
|
+
if (h.v6.length > 0) {
|
|
168
|
+
lines.push(` ip6 daddr { ${h.v6.join(", ")} } counter accept comment "allow:${label}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
lines.push(' log prefix "vig-drop " counter drop');
|
|
172
|
+
lines.push(" }", "}");
|
|
173
|
+
return lines.join("\n") + "\n";
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Assemble the full bwrap argv for an allowlisted-egress run: the shared
|
|
177
|
+
* confinement args, the hook's added-back env, `CAP_NET_ADMIN` (so the in-netns
|
|
178
|
+
* wrapper can load nft), `--info-fd 3` (so the orchestrator learns the child PID
|
|
179
|
+
* to hand slirp4netns), the `VIG_*` paths the wrapper reads, and the trailing
|
|
180
|
+
* `sh -c <wrapper>`. Pure (the bwrap/setenv args are computed by the caller), so
|
|
181
|
+
* the assembled shape — caps, info-fd, the VIG_* env, the wrapper payload — is
|
|
182
|
+
* asserted in a unit test.
|
|
183
|
+
*/
|
|
184
|
+
function buildEgressBwrapArgv(opts) {
|
|
185
|
+
return [
|
|
186
|
+
...opts.base,
|
|
187
|
+
// Override the host's /etc/resolv.conf (often a 127.0.0.53 stub) with one
|
|
188
|
+
// listing only netns-routable resolvers — placed after `base` so it shadows
|
|
189
|
+
// the `--ro-bind / /` copy.
|
|
190
|
+
"--ro-bind",
|
|
191
|
+
opts.resolvConf,
|
|
192
|
+
"/etc/resolv.conf",
|
|
193
|
+
"--cap-add",
|
|
194
|
+
"CAP_NET_ADMIN",
|
|
195
|
+
...opts.setenv,
|
|
196
|
+
"--setenv",
|
|
197
|
+
"VIG_NETREADY",
|
|
198
|
+
opts.files.netready,
|
|
199
|
+
"--setenv",
|
|
200
|
+
"VIG_NFT",
|
|
201
|
+
opts.files.nft,
|
|
202
|
+
"--setenv",
|
|
203
|
+
"VIG_EVENT",
|
|
204
|
+
opts.files.event,
|
|
205
|
+
"--setenv",
|
|
206
|
+
"VIG_COUNTERS",
|
|
207
|
+
opts.files.counters,
|
|
208
|
+
"--setenv",
|
|
209
|
+
"VIG_IODIR",
|
|
210
|
+
opts.files.ioDir,
|
|
211
|
+
"--setenv",
|
|
212
|
+
"VIG_HOOK",
|
|
213
|
+
opts.command,
|
|
214
|
+
"--info-fd",
|
|
215
|
+
"3",
|
|
216
|
+
"sh",
|
|
217
|
+
"-c",
|
|
218
|
+
opts.wrapper,
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
const COUNTER = /counter packets (\d+) bytes (\d+)/;
|
|
222
|
+
const ALLOW_COMMENT = /comment "allow:([^"]+)"/;
|
|
223
|
+
/**
|
|
224
|
+
* Parse `nft list chain inet vig output` back into per-host allowed counters and
|
|
225
|
+
* the aggregate drop counter. Sums multiple rules that share a host comment (the
|
|
226
|
+
* v4 + v6 split). Pure, so the read-back is unit-tested without a sandbox.
|
|
227
|
+
*/
|
|
228
|
+
function parseNftCounters(nftText) {
|
|
229
|
+
const allowed = new Map();
|
|
230
|
+
let dropped = { packets: 0, bytes: 0 };
|
|
231
|
+
for (const line of nftText.split("\n")) {
|
|
232
|
+
const c = COUNTER.exec(line);
|
|
233
|
+
if (!c)
|
|
234
|
+
continue;
|
|
235
|
+
const packets = Number(c[1]);
|
|
236
|
+
const bytes = Number(c[2]);
|
|
237
|
+
const a = ALLOW_COMMENT.exec(line);
|
|
238
|
+
if (a) {
|
|
239
|
+
const prev = allowed.get(a[1]) ?? { packets: 0, bytes: 0 };
|
|
240
|
+
allowed.set(a[1], {
|
|
241
|
+
packets: prev.packets + packets,
|
|
242
|
+
bytes: prev.bytes + bytes,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (line.includes("vig-drop") || /\bdrop\b/.test(line)) {
|
|
246
|
+
dropped = {
|
|
247
|
+
packets: dropped.packets + packets,
|
|
248
|
+
bytes: dropped.bytes + bytes,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
allowed: [...allowed.entries()].map(([host, v]) => ({ host, ...v })),
|
|
254
|
+
dropped,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Map parsed counters to the run result: one {@link EgressAttempt} per
|
|
259
|
+
* allowlisted host that saw traffic (`allowed: true`, with its packet/byte
|
|
260
|
+
* counts), plus the aggregate dropped counter. `ts` is stamped once per run (the
|
|
261
|
+
* counters are end-of-run totals, not per-connection events). Pure.
|
|
262
|
+
*/
|
|
263
|
+
function countersToResult(counters, now) {
|
|
264
|
+
const egress = counters.allowed
|
|
265
|
+
.filter((a) => a.packets > 0)
|
|
266
|
+
.map((a) => ({
|
|
267
|
+
host: a.host,
|
|
268
|
+
port: 0,
|
|
269
|
+
ts: now,
|
|
270
|
+
allowed: true,
|
|
271
|
+
packets: a.packets,
|
|
272
|
+
bytes: a.bytes,
|
|
273
|
+
}));
|
|
274
|
+
return { egress, egressDropped: counters.dropped };
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=egress.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SHA256Hash } from "
|
|
1
|
+
import { type SHA256Hash } from "../../core/hash.js";
|
|
2
2
|
import type { RunOut } from "./eval.js";
|
|
3
3
|
/** Cache behaviour: never touch the cache / read-only / read-and-write. */
|
|
4
4
|
export type CacheMode = "off" | "read" | "readwrite";
|
|
@@ -24,7 +24,7 @@ exports.restoreDir = restoreDir;
|
|
|
24
24
|
*/
|
|
25
25
|
const node_fs_1 = require("node:fs");
|
|
26
26
|
const node_path_1 = require("node:path");
|
|
27
|
-
const hash_js_1 = require("
|
|
27
|
+
const hash_js_1 = require("../../core/hash.js");
|
|
28
28
|
const MAX_SNAPSHOT_FILE_BYTES = 1024 * 1024;
|
|
29
29
|
const SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
30
30
|
/**
|
|
@@ -213,6 +213,14 @@ export interface TriggerRateSpec {
|
|
|
213
213
|
readonly pluginDir: string;
|
|
214
214
|
/** The varied prompts to test the trigger against. */
|
|
215
215
|
readonly prompts: readonly string[];
|
|
216
|
+
/**
|
|
217
|
+
* Optional *irrelevant* prompts the skill should **not** fire on — the
|
|
218
|
+
* precision side of triggering. Firing on these is a false positive (a skill
|
|
219
|
+
* whose description is too broad and hijacks unrelated work). When given, the
|
|
220
|
+
* report adds {@link TriggerRateReport.falsePositiveRate} and
|
|
221
|
+
* {@link TriggerRateReport.precision}; `prompts` alone measures recall only.
|
|
222
|
+
*/
|
|
223
|
+
readonly irrelevantPrompts?: readonly string[];
|
|
216
224
|
/** Did the behaviour fire on this run? e.g. `(t) => skillResolved(t, "x:y")`. */
|
|
217
225
|
readonly fired: (trace: Trace) => boolean;
|
|
218
226
|
/** Trials per prompt. Default 1. */
|
|
@@ -235,17 +243,33 @@ export interface PromptTriggerStat {
|
|
|
235
243
|
readonly rate: number;
|
|
236
244
|
}
|
|
237
245
|
export interface TriggerRateReport {
|
|
238
|
-
/** Overall fraction of runs in which the behaviour fired (0..1). */
|
|
246
|
+
/** Overall fraction of relevant runs in which the behaviour fired (recall, 0..1). */
|
|
239
247
|
readonly rate: number;
|
|
240
|
-
/** Total runs (prompts × trials). */
|
|
248
|
+
/** Total relevant runs (prompts × trials). */
|
|
241
249
|
readonly n: number;
|
|
242
250
|
readonly perPrompt: readonly PromptTriggerStat[];
|
|
251
|
+
/**
|
|
252
|
+
* Fraction of *irrelevant* runs that wrongly fired (lower is better). Present
|
|
253
|
+
* only when {@link TriggerRateSpec.irrelevantPrompts} was given.
|
|
254
|
+
*/
|
|
255
|
+
readonly falsePositiveRate?: number;
|
|
256
|
+
/**
|
|
257
|
+
* `relevantFired / (relevantFired + irrelevantFired)` — of all firings, the
|
|
258
|
+
* share on the right prompts. Present only when irrelevant prompts were given
|
|
259
|
+
* AND something fired (undefined when nothing fired at all). Pairs with `rate`
|
|
260
|
+
* (recall) to catch a skill that fires on everything _or_ nothing.
|
|
261
|
+
*/
|
|
262
|
+
readonly precision?: number;
|
|
263
|
+
/** Per-prompt stats for the irrelevant set. Present with irrelevant prompts. */
|
|
264
|
+
readonly perIrrelevant?: readonly PromptTriggerStat[];
|
|
243
265
|
}
|
|
244
266
|
/**
|
|
245
267
|
* Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
|
|
246
268
|
* predicate evaluated per run and aggregated into an overall + per-prompt rate.
|
|
247
|
-
*
|
|
248
|
-
*
|
|
269
|
+
* With `irrelevantPrompts`, also runs the precision side (firing there is a false
|
|
270
|
+
* positive) and adds `falsePositiveRate` + `precision`. Exported with an
|
|
271
|
+
* injectable `runner` so the loop is unit-testable without a model;
|
|
272
|
+
* `measureTriggerRate` is this with the real agent runner.
|
|
249
273
|
*/
|
|
250
274
|
export declare function measureTriggerRateWith(spec: TriggerRateSpec, runner: AgentRunner): Promise<TriggerRateReport>;
|
|
251
275
|
/**
|
|
@@ -40,6 +40,7 @@ const node_fs_1 = require("node:fs");
|
|
|
40
40
|
const node_os_1 = require("node:os");
|
|
41
41
|
const node_path_1 = require("node:path");
|
|
42
42
|
const plugin_loader_js_1 = require("./plugin-loader.js");
|
|
43
|
+
const runtime_js_1 = require("./runtime.js");
|
|
43
44
|
const harness_test_js_1 = require("./harness-test.js");
|
|
44
45
|
const eval_cache_js_1 = require("./eval-cache.js");
|
|
45
46
|
function writeFiles(cwd, files) {
|
|
@@ -72,7 +73,7 @@ function spawnAgent(a) {
|
|
|
72
73
|
"--allowedTools",
|
|
73
74
|
...a.tools,
|
|
74
75
|
];
|
|
75
|
-
const child = (0, node_child_process_1.spawn)(
|
|
76
|
+
const child = (0, node_child_process_1.spawn)(runtime_js_1.claudeCodeRuntime.agentBinary, args, {
|
|
76
77
|
cwd: a.cwd,
|
|
77
78
|
env: process.env,
|
|
78
79
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -401,53 +402,78 @@ function formatEvalReport(report) {
|
|
|
401
402
|
}
|
|
402
403
|
return lines.join("\n");
|
|
403
404
|
}
|
|
404
|
-
/**
|
|
405
|
-
|
|
406
|
-
* predicate evaluated per run and aggregated into an overall + per-prompt rate.
|
|
407
|
-
* Exported with an injectable `runner` so the loop is unit-testable without a
|
|
408
|
-
* model; `measureTriggerRate` is this with the real agent runner.
|
|
409
|
-
*/
|
|
410
|
-
async function measureTriggerRateWith(spec, runner) {
|
|
411
|
-
const trials = spec.trials ?? 1;
|
|
412
|
-
const model = spec.model ?? "haiku";
|
|
413
|
-
const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"];
|
|
414
|
-
const timeoutMs = spec.timeoutMs ?? 240000;
|
|
415
|
-
const spacing = (spec.spacingSec ?? 4) * 1000;
|
|
405
|
+
/** Run one prompt set × trials through `runner`, aggregating fired counts. */
|
|
406
|
+
async function runTriggerSet(prompts, cfg, runner) {
|
|
416
407
|
const perPrompt = [];
|
|
417
408
|
let firedTotal = 0;
|
|
418
409
|
let n = 0;
|
|
419
|
-
for (const prompt of
|
|
410
|
+
for (const prompt of prompts) {
|
|
420
411
|
let fired = 0;
|
|
421
|
-
for (let t = 0; t < trials; t++) {
|
|
412
|
+
for (let t = 0; t < cfg.trials; t++) {
|
|
422
413
|
const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-trigger-"));
|
|
423
414
|
try {
|
|
424
415
|
const out = await runner({
|
|
425
416
|
task: prompt,
|
|
426
417
|
cwd,
|
|
427
|
-
model,
|
|
428
|
-
tools,
|
|
418
|
+
model: cfg.model,
|
|
419
|
+
tools: cfg.tools,
|
|
429
420
|
hasSettings: false,
|
|
430
|
-
pluginDir:
|
|
431
|
-
timeoutMs,
|
|
421
|
+
pluginDir: cfg.pluginDir,
|
|
422
|
+
timeoutMs: cfg.timeoutMs,
|
|
432
423
|
});
|
|
433
|
-
if (
|
|
424
|
+
if (cfg.fired(makeContext(cwd, out)))
|
|
434
425
|
fired++;
|
|
435
426
|
}
|
|
436
427
|
finally {
|
|
437
428
|
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
438
|
-
await sleep(spacing);
|
|
429
|
+
await sleep(cfg.spacing);
|
|
439
430
|
}
|
|
440
431
|
}
|
|
441
432
|
perPrompt.push({
|
|
442
433
|
prompt,
|
|
443
434
|
fired,
|
|
444
|
-
trials,
|
|
445
|
-
rate: trials > 0 ? fired / trials : 0,
|
|
435
|
+
trials: cfg.trials,
|
|
436
|
+
rate: cfg.trials > 0 ? fired / cfg.trials : 0,
|
|
446
437
|
});
|
|
447
438
|
firedTotal += fired;
|
|
448
|
-
n += trials;
|
|
439
|
+
n += cfg.trials;
|
|
449
440
|
}
|
|
450
|
-
return {
|
|
441
|
+
return { perPrompt, fired: firedTotal, n };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
|
|
445
|
+
* predicate evaluated per run and aggregated into an overall + per-prompt rate.
|
|
446
|
+
* With `irrelevantPrompts`, also runs the precision side (firing there is a false
|
|
447
|
+
* positive) and adds `falsePositiveRate` + `precision`. Exported with an
|
|
448
|
+
* injectable `runner` so the loop is unit-testable without a model;
|
|
449
|
+
* `measureTriggerRate` is this with the real agent runner.
|
|
450
|
+
*/
|
|
451
|
+
async function measureTriggerRateWith(spec, runner) {
|
|
452
|
+
const cfg = {
|
|
453
|
+
trials: spec.trials ?? 1,
|
|
454
|
+
model: spec.model ?? "haiku",
|
|
455
|
+
tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"],
|
|
456
|
+
timeoutMs: spec.timeoutMs ?? 240000,
|
|
457
|
+
spacing: (spec.spacingSec ?? 4) * 1000,
|
|
458
|
+
pluginDir: spec.pluginDir,
|
|
459
|
+
fired: spec.fired,
|
|
460
|
+
};
|
|
461
|
+
const relevant = await runTriggerSet(spec.prompts, cfg, runner);
|
|
462
|
+
const base = {
|
|
463
|
+
rate: relevant.n > 0 ? relevant.fired / relevant.n : 0,
|
|
464
|
+
n: relevant.n,
|
|
465
|
+
perPrompt: relevant.perPrompt,
|
|
466
|
+
};
|
|
467
|
+
if ((spec.irrelevantPrompts?.length ?? 0) === 0)
|
|
468
|
+
return base;
|
|
469
|
+
const irrelevant = await runTriggerSet(spec.irrelevantPrompts ?? [], cfg, runner);
|
|
470
|
+
const fires = relevant.fired + irrelevant.fired;
|
|
471
|
+
return {
|
|
472
|
+
...base,
|
|
473
|
+
falsePositiveRate: irrelevant.n > 0 ? irrelevant.fired / irrelevant.n : 0,
|
|
474
|
+
precision: fires > 0 ? relevant.fired / fires : undefined,
|
|
475
|
+
perIrrelevant: irrelevant.perPrompt,
|
|
476
|
+
};
|
|
451
477
|
}
|
|
452
478
|
/* v8 ignore start -- real claude subprocess; thin wrapper over measureTriggerRateWith */
|
|
453
479
|
/**
|
|
@@ -465,6 +491,16 @@ function formatTriggerRateReport(report) {
|
|
|
465
491
|
for (const p of report.perPrompt) {
|
|
466
492
|
lines.push(` ${p.rate.toFixed(2)} ${p.prompt.slice(0, 60)}`);
|
|
467
493
|
}
|
|
494
|
+
if (report.falsePositiveRate !== undefined) {
|
|
495
|
+
const fpr = (report.falsePositiveRate * 100).toFixed(0);
|
|
496
|
+
const prec = report.precision === undefined
|
|
497
|
+
? "n/a"
|
|
498
|
+
: `${(report.precision * 100).toFixed(0)}%`;
|
|
499
|
+
lines.push(`false-positive: ${fpr}% precision: ${prec}`);
|
|
500
|
+
for (const p of report.perIrrelevant ?? []) {
|
|
501
|
+
lines.push(` ${p.rate.toFixed(2)} [irrelevant] ${p.prompt.slice(0, 48)}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
468
504
|
return lines.join("\n");
|
|
469
505
|
}
|
|
470
506
|
//# sourceMappingURL=eval.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { HarnessAdapter } from "../../core/adapter.js";
|
|
2
|
+
import type { HarnessTestDriver, ToolCall, HookFire, ParsedRun, ModelTurn, ModelRequest } from "../../core/harness-driver.js";
|
|
2
3
|
import { type SandboxMode } from "./sandbox.js";
|
|
3
|
-
export { scriptModel
|
|
4
|
+
export { scriptModel } from "./mock-model.js";
|
|
5
|
+
export type { ModelTurn, ModelRequest, ToolCall, HookFire, HarnessTestDriver, } from "../../core/harness-driver.js";
|
|
4
6
|
export { loadPlugin, resolveHarness } from "./plugin-loader.js";
|
|
5
7
|
export { decideSandbox, specTrusted, sandboxAvailable, type SandboxMode, } from "./sandbox.js";
|
|
6
8
|
export interface HarnessTestSpec {
|
|
@@ -54,24 +56,6 @@ export interface HarnessTestSpec {
|
|
|
54
56
|
*/
|
|
55
57
|
readonly sandbox?: SandboxMode;
|
|
56
58
|
}
|
|
57
|
-
/**
|
|
58
|
-
* A hook invocation observed during the run, recorded (not inferred) from the
|
|
59
|
-
* `hook_response` system events the CLI emits in the stream — so a test can
|
|
60
|
-
* assert which hook fired and whether it blocked, instead of inferring it from a
|
|
61
|
-
* marker file the hook had to write.
|
|
62
|
-
*/
|
|
63
|
-
export interface HookFire {
|
|
64
|
-
/** The hook label, e.g. `"PreToolUse:Edit"` (`Event:Matcher`). */
|
|
65
|
-
readonly name: string;
|
|
66
|
-
/** The hook event, e.g. `"PreToolUse"`, `"PostToolUse"`, `"Stop"`. */
|
|
67
|
-
readonly event: string;
|
|
68
|
-
/** The hook process exit code (2 = block), or undefined if not reported. */
|
|
69
|
-
readonly exitCode: number | undefined;
|
|
70
|
-
/** Whether the hook blocked / errored (exit ≠ 0 or outcome "error"). */
|
|
71
|
-
readonly blocked: boolean;
|
|
72
|
-
/** What the hook printed (its block reason / diagnostic), or "". */
|
|
73
|
-
readonly output: string;
|
|
74
|
-
}
|
|
75
59
|
/**
|
|
76
60
|
* The observable record of ONE run — the unified shape produced by BOTH testing
|
|
77
61
|
* tiers: `runHarnessTest`'s result and `runEval`'s `measure` ctx (`eval.ts`)
|
|
@@ -124,15 +108,6 @@ export interface HarnessTestResult extends Trace {
|
|
|
124
108
|
/** Remove the temp working dir. */
|
|
125
109
|
cleanup(): void;
|
|
126
110
|
}
|
|
127
|
-
/** A tool the agent invoked, paired with its result (transcript mode only). */
|
|
128
|
-
export interface ToolCall {
|
|
129
|
-
readonly name: string;
|
|
130
|
-
readonly input: unknown;
|
|
131
|
-
/** The tool_result text ("" if none / not captured). */
|
|
132
|
-
readonly resultText: string;
|
|
133
|
-
/** Whether the tool_result came back flagged as an error. */
|
|
134
|
-
readonly isError: boolean;
|
|
135
|
-
}
|
|
136
111
|
/**
|
|
137
112
|
* Parse `--output-format stream-json` (the `transcript: true` output) into the
|
|
138
113
|
* tools the agent invoked, each joined to its result by id. Returns [] for the
|
|
@@ -155,16 +130,37 @@ export declare function parseHooks(stdout: string): HookFire[];
|
|
|
155
130
|
* here. Pure, so the arg shape is unit-tested.
|
|
156
131
|
*/
|
|
157
132
|
export declare function buildClaudeArgs(spec: HarnessTestSpec, hasSettings: boolean): string[];
|
|
158
|
-
/**
|
|
133
|
+
/** Parse the `claude` stdout/stream into the unified trace fields. */
|
|
134
|
+
export declare function parseClaudeRun(stdout: string): ParsedRun;
|
|
135
|
+
/** Whether the agent CLI is available — harness tests need it. */
|
|
159
136
|
export declare function claudeAvailable(): boolean;
|
|
160
137
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
138
|
+
* The Claude Code `HarnessTestDriver`: the existing argv/mock/parse seams bundled
|
|
139
|
+
* behind the port the adapter-driven runner dispatches through. Behaviourally
|
|
140
|
+
* identical to the previous hard-wired path.
|
|
141
|
+
*/
|
|
142
|
+
export declare const claudeCodeDriver: HarnessTestDriver;
|
|
143
|
+
/** Options for {@link runHarnessTest}. */
|
|
144
|
+
export interface RunHarnessTestOptions {
|
|
145
|
+
/**
|
|
146
|
+
* Which harness to drive. Defaults to Claude Code. Pass `codexAdapter`
|
|
147
|
+
* (`vigiles/codex`) to drive real `codex exec` against its Responses mock.
|
|
148
|
+
* The adapter must support pillar 2 (`capabilities.harnessTesting`) and carry
|
|
149
|
+
* a `harnessTestDriver`.
|
|
150
|
+
*/
|
|
151
|
+
readonly adapter?: HarnessAdapter;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run the real agent CLI against a scripted mock model, with the given fixture
|
|
155
|
+
* and settings (hooks). Deterministic — same script, same result. Adapter-driven
|
|
156
|
+
* (`opts.adapter`, default Claude Code): the Claude Code path is unchanged
|
|
157
|
+
* (incl. the safe-by-default sandbox); pass `codexAdapter` to drive real codex.
|
|
163
158
|
*
|
|
164
|
-
* Safe by default: an external `plugin` / `pluginDir` brings in
|
|
165
|
-
* third-party hooks and is confined under bubblewrap (`spec.sandbox`,
|
|
166
|
-
* `"auto"`); if no sandbox is available the run REFUSES rather than
|
|
167
|
-
* unconfined. See `src/sandbox.ts`.
|
|
159
|
+
* Safe by default (Claude Code): an external `plugin` / `pluginDir` brings in
|
|
160
|
+
* untrusted third-party hooks and is confined under bubblewrap (`spec.sandbox`,
|
|
161
|
+
* default `"auto"`); if no sandbox is available the run REFUSES rather than
|
|
162
|
+
* executing unconfined. See `src/sandbox.ts`. The sandbox path is Claude Code
|
|
163
|
+
* only — requesting confinement for another harness throws.
|
|
168
164
|
*/
|
|
169
|
-
export declare function runHarnessTest(spec: HarnessTestSpec): Promise<HarnessTestResult>;
|
|
165
|
+
export declare function runHarnessTest(spec: HarnessTestSpec, opts?: RunHarnessTestOptions): Promise<HarnessTestResult>;
|
|
170
166
|
//# sourceMappingURL=harness-test.d.ts.map
|