vigiles 2.5.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 +73 -434
- 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-proxy.d.ts +2 -0
- package/dist/adapters/claude-code/egress-proxy.js +60 -0
- package/dist/adapters/claude-code/egress.d.ts +114 -0
- package/dist/adapters/claude-code/egress.js +276 -0
- package/dist/adapters/claude-code/eval-baseline.d.ts +68 -0
- package/dist/adapters/claude-code/eval-baseline.js +173 -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/adapters/claude-code/run-hook.d.ts +204 -0
- package/dist/adapters/claude-code/run-hook.js +418 -0
- 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} +45 -4
- package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +76 -8
- 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 +16 -0
- package/dist/claude-code.js +32 -0
- 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 +60 -11
- package/dist/harness-assert.js +111 -9
- 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 +9 -0
- package/dist/linting.js +25 -0
- 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 +12 -0
- package/dist/testing.js +28 -0
- package/dist/unit.d.ts +17 -0
- package/dist/unit.js +36 -0
- package/hooks/refs-nudge.sh +24 -0
- package/package.json +35 -16
- 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-hook.d.ts +0 -77
- package/dist/run-hook.js +0 -80
- 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/{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,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseHookOutput = parseHookOutput;
|
|
4
|
+
exports.decideHook = decideHook;
|
|
5
|
+
exports.runHookWith = runHookWith;
|
|
6
|
+
exports.egressRoutes = egressRoutes;
|
|
7
|
+
exports.runHook = runHook;
|
|
8
|
+
/**
|
|
9
|
+
* vigiles — the *unit* tier for Claude Code hooks.
|
|
10
|
+
*
|
|
11
|
+
* A hook is just a process: Claude Code pipes a JSON event to its stdin and
|
|
12
|
+
* reads back an exit code (0 ok, 2 = block) and, optionally, a JSON decision on
|
|
13
|
+
* stdout. `runHook` exercises exactly that contract directly — no `claude`
|
|
14
|
+
* binary, no model, no sandbox — so a hook's logic can be unit-tested in
|
|
15
|
+
* milliseconds:
|
|
16
|
+
*
|
|
17
|
+
* const r = runHook('"$GUARD" ', {
|
|
18
|
+
* hook_event_name: "PreToolUse",
|
|
19
|
+
* tool_name: "Bash",
|
|
20
|
+
* tool_input: { command: "git commit --no-verify" },
|
|
21
|
+
* }, { env: { GUARD: guardPath } });
|
|
22
|
+
* assert.ok(r.blocked); // exit 2 / decision:block / permission:deny
|
|
23
|
+
*
|
|
24
|
+
* Why this exists alongside `runHarnessTest`:
|
|
25
|
+
* - It is the cheap base of the pyramid — no CLI dependency, runs anywhere.
|
|
26
|
+
* - It reaches every event. The deterministic `runHarnessTest` mock can drive
|
|
27
|
+
* SessionStart/Stop/UserPromptSubmit/Bash PreToolUse|PostToolUse, but NOT
|
|
28
|
+
* Edit/Write tool events (headless-gated), PreCompact, Notification,
|
|
29
|
+
* SessionEnd, or SubagentStop. At this tier you hand the hook the event
|
|
30
|
+
* JSON yourself, so all of them are testable.
|
|
31
|
+
*
|
|
32
|
+
* It does NOT prove the hook is *wired* into the harness (that the settings
|
|
33
|
+
* point at it, that `${CLAUDE_PLUGIN_ROOT}` resolves) — that is what the
|
|
34
|
+
* `plugin:` loader + `runHarnessTest` cover. Use both: unit-test the hook's
|
|
35
|
+
* logic here, then assert it fires in the assembled machine there.
|
|
36
|
+
*/
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const node_os_1 = require("node:os");
|
|
40
|
+
const node_path_1 = require("node:path");
|
|
41
|
+
const hook_protocol_js_1 = require("./hook-protocol.js");
|
|
42
|
+
const egress_js_1 = require("./egress.js");
|
|
43
|
+
const sandbox_js_1 = require("./sandbox.js");
|
|
44
|
+
/** Parse stdout as a hook JSON decision (pure, testable without a process). */
|
|
45
|
+
function parseHookOutput(stdout) {
|
|
46
|
+
const s = stdout.trim();
|
|
47
|
+
if (!s.startsWith("{"))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(s);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Decide whether a hook result blocked, and the normalized decision. Pure, so
|
|
58
|
+
* the policy is unit-testable independent of spawning anything.
|
|
59
|
+
*/
|
|
60
|
+
function decideHook(exitCode, json, protocol = hook_protocol_js_1.claudeCodeHookProtocol) {
|
|
61
|
+
const permission = json?.hookSpecificOutput?.permissionDecision;
|
|
62
|
+
const decision = permission ?? json?.decision;
|
|
63
|
+
const blocked = exitCode === protocol.blockExitCode ||
|
|
64
|
+
(decision !== undefined && protocol.denyDecisionValues.includes(decision));
|
|
65
|
+
return { blocked, decision };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The hook-run orchestration with injectable spawn seams: pick direct vs.
|
|
69
|
+
* confined via the safe-by-default policy (`decideSandbox`), then parse the exit
|
|
70
|
+
* code + stdout into a normalized decision. Exported so all three branches
|
|
71
|
+
* (direct / sandbox / refuse) are unit-tested with fake spawners — no real
|
|
72
|
+
* bwrap. `runHook` is this with the real seams.
|
|
73
|
+
*/
|
|
74
|
+
function runHookWith(command, input, opts, deps) {
|
|
75
|
+
// Allowlisted egress is its own confined path (bwrap netns + slirp4netns + nft);
|
|
76
|
+
// it can't run unconfined, so it refuses outright when the tooling is absent
|
|
77
|
+
// rather than falling back to a direct run that would ignore the allowlist.
|
|
78
|
+
const res = opts.egress
|
|
79
|
+
? runEgress(command, input, opts, deps)
|
|
80
|
+
: runConfinedOrDirect(command, input, opts, deps);
|
|
81
|
+
const exitCode = res.status ?? (res.signal ? 1 : 0);
|
|
82
|
+
const stdout = res.stdout ?? "";
|
|
83
|
+
const stderr = res.stderr ?? "";
|
|
84
|
+
const json = parseHookOutput(stdout);
|
|
85
|
+
const { blocked, decision: dec } = decideHook(exitCode, json);
|
|
86
|
+
return {
|
|
87
|
+
exitCode,
|
|
88
|
+
stdout,
|
|
89
|
+
stderr,
|
|
90
|
+
json,
|
|
91
|
+
blocked,
|
|
92
|
+
egress: res.egress ?? [],
|
|
93
|
+
egressDropped: res.egressDropped,
|
|
94
|
+
filesWritten: res.filesWritten ?? [],
|
|
95
|
+
decision: dec,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** The allowlisted-egress branch: confine-with-netns, or refuse if unavailable. */
|
|
99
|
+
function runEgress(command, input, opts, deps) {
|
|
100
|
+
if (!deps.egressAvailable) {
|
|
101
|
+
throw new Error("refusing to run egress: { allow } without the allowlist sandbox: it " +
|
|
102
|
+
"needs Linux + bubblewrap (bwrap) + slirp4netns + nft — install them to " +
|
|
103
|
+
"run with a packet-layer egress allowlist, or use recordEgress to record " +
|
|
104
|
+
"and block instead");
|
|
105
|
+
}
|
|
106
|
+
return deps.egress(command, input, opts);
|
|
107
|
+
}
|
|
108
|
+
/** The default branch: pick direct vs. confined via the safe-by-default policy. */
|
|
109
|
+
function runConfinedOrDirect(command, input, opts, deps) {
|
|
110
|
+
// Confinement follows provenance: a trusted hook (the default) runs directly;
|
|
111
|
+
// marking a hook untrusted defaults it to "auto" (confine-or-refuse), so
|
|
112
|
+
// foreign code is never run unconfined by accident. An explicit `sandbox`
|
|
113
|
+
// overrides the default either way. The trust fed to decideSandbox stays
|
|
114
|
+
// `false` at this tier — a raw command has no provenance, so an explicit
|
|
115
|
+
// "auto"/"strict" here is always a request to *confine*, not "trusted→direct".
|
|
116
|
+
// recordEgress needs the netns recorder, so it forces confinement too.
|
|
117
|
+
const mode = opts.sandbox ??
|
|
118
|
+
(opts.trusted === false || opts.recordEgress ? "auto" : false);
|
|
119
|
+
const decision = (0, sandbox_js_1.decideSandbox)({
|
|
120
|
+
trusted: false,
|
|
121
|
+
mode,
|
|
122
|
+
available: deps.available,
|
|
123
|
+
});
|
|
124
|
+
if (decision.action === "throw")
|
|
125
|
+
throw new Error(decision.reason);
|
|
126
|
+
return decision.action === "sandbox"
|
|
127
|
+
? deps.sandboxed(command, input, opts)
|
|
128
|
+
: deps.direct(command, input, opts);
|
|
129
|
+
}
|
|
130
|
+
/** Run the hook command directly through a shell (the default, unconfined). */
|
|
131
|
+
function directSpawn(command, input, opts) {
|
|
132
|
+
const res = (0, node_child_process_1.spawnSync)(command, {
|
|
133
|
+
shell: true,
|
|
134
|
+
cwd: opts.cwd,
|
|
135
|
+
env: { ...process.env, ...opts.env },
|
|
136
|
+
input: JSON.stringify(input),
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
timeout: opts.timeoutMs ?? 10000,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
status: res.status,
|
|
142
|
+
signal: res.signal,
|
|
143
|
+
stdout: res.stdout ?? "",
|
|
144
|
+
stderr: res.stderr ?? "",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/* v8 ignore start -- spawns real bwrap; exercised by the bwrap-gated integration
|
|
148
|
+
test (skipped without bwrap). The decision logic is runHookWith (unit-tested
|
|
149
|
+
with fakes); the confinement argv is bwrapArgs/setenvArgs and the egress log
|
|
150
|
+
parse is parseEgressLog (all unit-tested). */
|
|
151
|
+
// When recordEgress is on: co-launch the recorder on loopback, point HTTP(S)_PROXY
|
|
152
|
+
// at it, run the hook, then stop it. Paths come in via env (no shell escaping).
|
|
153
|
+
const EGRESS_WRAPPER = [
|
|
154
|
+
'node "$VIG_EGRESS_ENTRY" "$VIG_EGRESS_LOG" "$VIG_EGRESS_PORT" &',
|
|
155
|
+
"EPID=$!",
|
|
156
|
+
"i=0",
|
|
157
|
+
'while [ ! -s "$VIG_EGRESS_PORT" ] && [ "$i" -lt 100 ]; do sleep 0.05; i=$((i+1)); done',
|
|
158
|
+
'export HTTP_PROXY="http://127.0.0.1:$(cat "$VIG_EGRESS_PORT")"',
|
|
159
|
+
'export HTTPS_PROXY="$HTTP_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTP_PROXY"',
|
|
160
|
+
// Node's fetch (undici) ignores the proxy env unless this is set — without it a
|
|
161
|
+
// hook that uses fetch() (e.g. an update check) would bypass the recorder.
|
|
162
|
+
"export NODE_USE_ENV_PROXY=1",
|
|
163
|
+
'sh -c "$VIG_HOOK_CMD"',
|
|
164
|
+
"code=$?",
|
|
165
|
+
'kill "$EPID" 2>/dev/null',
|
|
166
|
+
'exit "$code"',
|
|
167
|
+
].join("\n");
|
|
168
|
+
function egressProxyEntry() {
|
|
169
|
+
return ([
|
|
170
|
+
(0, node_path_1.join)(__dirname, "egress-proxy.js"),
|
|
171
|
+
(0, node_path_1.join)(__dirname, "..", "dist", "egress-proxy.js"),
|
|
172
|
+
].find((p) => (0, node_fs_1.existsSync)(p)) ?? (0, node_path_1.join)(__dirname, "egress-proxy.js"));
|
|
173
|
+
}
|
|
174
|
+
/** Map every file under `dir` to a content signature (size:mtime), recursively. */
|
|
175
|
+
function snapshotTree(dir) {
|
|
176
|
+
const out = {};
|
|
177
|
+
const walk = (d, rel) => {
|
|
178
|
+
for (const name of (0, node_fs_1.readdirSync)(d)) {
|
|
179
|
+
const full = (0, node_path_1.join)(d, name);
|
|
180
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
181
|
+
const st = (0, node_fs_1.statSync)(full);
|
|
182
|
+
if (st.isDirectory())
|
|
183
|
+
walk(full, r);
|
|
184
|
+
else
|
|
185
|
+
out[r] = `${String(st.size)}:${String(st.mtimeMs)}`;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
walk(dir, "");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* dir removed mid-walk — best effort */
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function sandboxedSpawn(command, input, opts) {
|
|
197
|
+
const ioDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-hook-sbx-"));
|
|
198
|
+
const home = (0, node_path_1.join)(ioDir, "home");
|
|
199
|
+
(0, node_fs_1.mkdirSync)(home);
|
|
200
|
+
// The hook's confined writable work dir: the caller's cwd if given, else a
|
|
201
|
+
// dedicated `work/` under the IO dir (kept separate from the egress log/home so
|
|
202
|
+
// those don't pollute the filesWritten diff).
|
|
203
|
+
const work = opts.cwd ?? (0, node_path_1.join)(ioDir, "work");
|
|
204
|
+
(0, node_fs_1.mkdirSync)(work, { recursive: true });
|
|
205
|
+
try {
|
|
206
|
+
const baseArgs = [
|
|
207
|
+
...(0, sandbox_js_1.bwrapArgs)({
|
|
208
|
+
cwd: work,
|
|
209
|
+
ioDir,
|
|
210
|
+
home,
|
|
211
|
+
path: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
212
|
+
}),
|
|
213
|
+
...(0, sandbox_js_1.setenvArgs)(opts.env ?? {}),
|
|
214
|
+
];
|
|
215
|
+
const spawnOpts = {
|
|
216
|
+
cwd: work,
|
|
217
|
+
env: process.env,
|
|
218
|
+
input: JSON.stringify(input),
|
|
219
|
+
encoding: "utf-8",
|
|
220
|
+
timeout: opts.timeoutMs ?? 10000,
|
|
221
|
+
};
|
|
222
|
+
const before = snapshotTree(work);
|
|
223
|
+
let res;
|
|
224
|
+
let egress = [];
|
|
225
|
+
if (opts.recordEgress) {
|
|
226
|
+
const egressLog = (0, node_path_1.join)(ioDir, "egress.ndjson");
|
|
227
|
+
const portFile = (0, node_path_1.join)(ioDir, "egress.port");
|
|
228
|
+
(0, node_fs_1.writeFileSync)(egressLog, "");
|
|
229
|
+
res = (0, node_child_process_1.spawnSync)("bwrap", [
|
|
230
|
+
...baseArgs,
|
|
231
|
+
"--setenv",
|
|
232
|
+
"VIG_EGRESS_ENTRY",
|
|
233
|
+
egressProxyEntry(),
|
|
234
|
+
"--setenv",
|
|
235
|
+
"VIG_EGRESS_LOG",
|
|
236
|
+
egressLog,
|
|
237
|
+
"--setenv",
|
|
238
|
+
"VIG_EGRESS_PORT",
|
|
239
|
+
portFile,
|
|
240
|
+
"--setenv",
|
|
241
|
+
"VIG_HOOK_CMD",
|
|
242
|
+
command,
|
|
243
|
+
"sh",
|
|
244
|
+
"-c",
|
|
245
|
+
EGRESS_WRAPPER,
|
|
246
|
+
], spawnOpts);
|
|
247
|
+
egress = (0, sandbox_js_1.parseEgressLog)((0, node_fs_1.existsSync)(egressLog) ? (0, node_fs_1.readFileSync)(egressLog, "utf-8") : "");
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
res = (0, node_child_process_1.spawnSync)("bwrap", [...baseArgs, "sh", "-c", command], spawnOpts);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
status: res.status,
|
|
254
|
+
signal: res.signal,
|
|
255
|
+
stdout: res.stdout ?? "",
|
|
256
|
+
stderr: res.stderr ?? "",
|
|
257
|
+
egress,
|
|
258
|
+
filesWritten: (0, sandbox_js_1.diffTrees)(before, snapshotTree(work)),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
(0, node_fs_1.rmSync)(ioDir, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function egressEntry() {
|
|
266
|
+
return ([
|
|
267
|
+
(0, node_path_1.join)(__dirname, "egress-entry.js"),
|
|
268
|
+
(0, node_path_1.join)(__dirname, "..", "dist", "egress-entry.js"),
|
|
269
|
+
].find((p) => (0, node_fs_1.existsSync)(p)) ?? (0, node_path_1.join)(__dirname, "egress-entry.js"));
|
|
270
|
+
}
|
|
271
|
+
// Runs INSIDE the bwrap netns: block until the parent has attached slirp4netns
|
|
272
|
+
// (the netready file), load the nft allowlist, run the hook with the event on its
|
|
273
|
+
// stdin, then dump the nft counters to a bound file BEFORE exiting — the netns
|
|
274
|
+
// (and its counters) die with this process, so the read-back must happen here.
|
|
275
|
+
const EGRESS_ALLOW_WRAPPER = [
|
|
276
|
+
"i=0",
|
|
277
|
+
'while [ ! -s "$VIG_NETREADY" ] && [ "$i" -lt 400 ]; do sleep 0.05; i=$((i+1)); done',
|
|
278
|
+
'nft -f "$VIG_NFT" > "$VIG_IODIR/nfterr" 2>&1',
|
|
279
|
+
'sh -c "$VIG_HOOK" < "$VIG_EVENT"',
|
|
280
|
+
"code=$?",
|
|
281
|
+
'nft list chain inet vig output > "$VIG_COUNTERS" 2>/dev/null',
|
|
282
|
+
'exit "$code"',
|
|
283
|
+
].join("\n");
|
|
284
|
+
/** Read the orchestrator's result file, or a failed-run default if it's absent. */
|
|
285
|
+
function readEgressResult(resultFile) {
|
|
286
|
+
return (0, node_fs_1.existsSync)(resultFile)
|
|
287
|
+
? JSON.parse((0, node_fs_1.readFileSync)(resultFile, "utf-8"))
|
|
288
|
+
: { status: 1, signal: null, stdout: "", stderr: "", counters: "" };
|
|
289
|
+
}
|
|
290
|
+
function egressSpawn(command, input, opts) {
|
|
291
|
+
const ioDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-hook-egr-"));
|
|
292
|
+
const home = (0, node_path_1.join)(ioDir, "home");
|
|
293
|
+
(0, node_fs_1.mkdirSync)(home);
|
|
294
|
+
const work = opts.cwd ?? (0, node_path_1.join)(ioDir, "work");
|
|
295
|
+
(0, node_fs_1.mkdirSync)(work, { recursive: true });
|
|
296
|
+
try {
|
|
297
|
+
// Resolve the allowlist to IPs and the system resolvers, then generate the
|
|
298
|
+
// nft ruleset (pure helpers in src/egress.ts) — the packet-layer wall.
|
|
299
|
+
const files = {
|
|
300
|
+
ioDir,
|
|
301
|
+
netready: (0, node_path_1.join)(ioDir, "netready"),
|
|
302
|
+
nft: (0, node_path_1.join)(ioDir, "rules.nft"),
|
|
303
|
+
event: (0, node_path_1.join)(ioDir, "event.json"),
|
|
304
|
+
counters: (0, node_path_1.join)(ioDir, "counters.txt"),
|
|
305
|
+
};
|
|
306
|
+
const allow = (0, egress_js_1.resolveAllow)(opts.egress?.allow ?? []);
|
|
307
|
+
const resolvers = (0, egress_js_1.parseResolvers)((0, node_fs_1.existsSync)("/etc/resolv.conf")
|
|
308
|
+
? (0, node_fs_1.readFileSync)("/etc/resolv.conf", "utf-8")
|
|
309
|
+
: "");
|
|
310
|
+
(0, node_fs_1.writeFileSync)(files.nft, (0, egress_js_1.buildEgressNft)({ allow, resolvers }));
|
|
311
|
+
(0, node_fs_1.writeFileSync)(files.event, JSON.stringify(input));
|
|
312
|
+
// The in-netns resolv.conf: only the routable resolvers (the host's may be a
|
|
313
|
+
// 127.0.0.53 stub the netns can't reach). Bound over /etc/resolv.conf below.
|
|
314
|
+
const resolvConf = (0, node_path_1.join)(ioDir, "resolv.conf");
|
|
315
|
+
(0, node_fs_1.writeFileSync)(resolvConf, resolvers.map((r) => `nameserver ${r}`).join("\n") + "\n");
|
|
316
|
+
const bwrapArgv = (0, egress_js_1.buildEgressBwrapArgv)({
|
|
317
|
+
base: (0, sandbox_js_1.bwrapArgs)({
|
|
318
|
+
cwd: work,
|
|
319
|
+
ioDir,
|
|
320
|
+
home,
|
|
321
|
+
path: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
322
|
+
}),
|
|
323
|
+
setenv: (0, sandbox_js_1.setenvArgs)(opts.env ?? {}),
|
|
324
|
+
files,
|
|
325
|
+
command,
|
|
326
|
+
wrapper: EGRESS_ALLOW_WRAPPER,
|
|
327
|
+
resolvConf,
|
|
328
|
+
});
|
|
329
|
+
const configFile = (0, node_path_1.join)(ioDir, "config.json");
|
|
330
|
+
const resultFile = (0, node_path_1.join)(ioDir, "result.json");
|
|
331
|
+
(0, node_fs_1.writeFileSync)(configFile, JSON.stringify({
|
|
332
|
+
bwrapArgv,
|
|
333
|
+
slirpArgs: ["--configure", "--disable-host-loopback"],
|
|
334
|
+
// --config-net brings up the interface + host-derived routes in the
|
|
335
|
+
// target netns. The connector is slirp4netns by default (proven local);
|
|
336
|
+
// set VIGILES_EGRESS_CONNECTOR=pasta to use pasta (passt), which routes
|
|
337
|
+
// on hosted runners where slirp4netns's tap-attach fails.
|
|
338
|
+
pastaArgs: ["--config-net"],
|
|
339
|
+
connector: process.env.VIGILES_EGRESS_CONNECTOR === "pasta"
|
|
340
|
+
? "pasta"
|
|
341
|
+
: "slirp4netns",
|
|
342
|
+
infoFile: (0, node_path_1.join)(ioDir, "info.json"),
|
|
343
|
+
readyFile: (0, node_path_1.join)(ioDir, "ready"),
|
|
344
|
+
netreadyFile: files.netready,
|
|
345
|
+
countersFile: files.counters,
|
|
346
|
+
resultFile,
|
|
347
|
+
timeoutMs: opts.timeoutMs ?? 30000,
|
|
348
|
+
}));
|
|
349
|
+
(0, node_child_process_1.spawnSync)("node", [egressEntry(), configFile], {
|
|
350
|
+
encoding: "utf-8",
|
|
351
|
+
timeout: (opts.timeoutMs ?? 30000) + 20000,
|
|
352
|
+
});
|
|
353
|
+
const result = readEgressResult(resultFile);
|
|
354
|
+
const { egress, egressDropped } = (0, egress_js_1.countersToResult)((0, egress_js_1.parseNftCounters)(result.counters), Date.now());
|
|
355
|
+
return {
|
|
356
|
+
status: result.status,
|
|
357
|
+
signal: result.signal,
|
|
358
|
+
stdout: result.stdout,
|
|
359
|
+
stderr: result.stderr,
|
|
360
|
+
egress,
|
|
361
|
+
egressDropped,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
(0, node_fs_1.rmSync)(ioDir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const REAL_DEPS = {
|
|
369
|
+
available: (0, sandbox_js_1.sandboxAvailable)(),
|
|
370
|
+
egressAvailable: (0, egress_js_1.egressAvailable)((0, sandbox_js_1.sandboxAvailable)()),
|
|
371
|
+
direct: directSpawn,
|
|
372
|
+
sandboxed: sandboxedSpawn,
|
|
373
|
+
egress: egressSpawn,
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Whether allowlisted egress can ACTUALLY route here — not just whether the tools
|
|
377
|
+
* exist. On GitHub-hosted runners bwrap+slirp4netns+nft are all present, yet
|
|
378
|
+
* slirp4netns fails to attach `tap0`: the netns has only `lo`, so nothing leaves
|
|
379
|
+
* and the egress assertions can't be exercised. Run a trivial hook in the egress
|
|
380
|
+
* sandbox and check a non-loopback interface came up; memoized (the capability is
|
|
381
|
+
* fixed per run). Gates the egress e2e tests so they run for real where egress
|
|
382
|
+
* works and SKIP — honestly — where it provably can't, instead of failing red.
|
|
383
|
+
* (slirp4netns → pasta is the fix that makes it route in CI too; see
|
|
384
|
+
* research/egress-sandbox-tooling.md.)
|
|
385
|
+
*/
|
|
386
|
+
let egressRoutesMemo;
|
|
387
|
+
function egressRoutes() {
|
|
388
|
+
if (egressRoutesMemo !== undefined)
|
|
389
|
+
return egressRoutesMemo;
|
|
390
|
+
if (!(0, egress_js_1.egressAvailable)((0, sandbox_js_1.sandboxAvailable)()))
|
|
391
|
+
return (egressRoutesMemo = false);
|
|
392
|
+
try {
|
|
393
|
+
// The only reliable signal is the egress path itself: try to reach an
|
|
394
|
+
// allowlisted host and check a packet actually got out. Filesystem probes
|
|
395
|
+
// (/proc/net/dev) and `ip` are unreliable here — bwrap may bind the host
|
|
396
|
+
// /proc (so /proc/net shows host interfaces, a false positive) and `ip` isn't
|
|
397
|
+
// on the sandbox PATH. A real reach is namespace-accurate by construction.
|
|
398
|
+
const r = egressSpawn("curl -s -m 8 -o /dev/null https://example.com/ || true", { hook_event_name: "PreToolUse" }, { egress: { allow: ["example.com"] }, timeoutMs: 20000 });
|
|
399
|
+
egressRoutesMemo = (r.egress ?? []).some((e) => e.host === "example.com" && (e.packets ?? 0) > 0);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
egressRoutesMemo = false;
|
|
403
|
+
}
|
|
404
|
+
return egressRoutesMemo;
|
|
405
|
+
}
|
|
406
|
+
/* v8 ignore stop */
|
|
407
|
+
/**
|
|
408
|
+
* Run a hook command, piping `input` as JSON to its stdin, and report the exit
|
|
409
|
+
* code + parsed decision. Synchronous (so it can be used inside an eval's
|
|
410
|
+
* `measure` too). `command` is run through a shell, so the same command string a
|
|
411
|
+
* plugin ships (with args / env refs) works verbatim. Mark a hook you didn't
|
|
412
|
+
* write with `trusted: false` and it is confined by default (or pass `sandbox:
|
|
413
|
+
* "auto"` directly) — see {@link RunHookOptions.trusted}.
|
|
414
|
+
*/
|
|
415
|
+
function runHook(command, input, opts = {}) {
|
|
416
|
+
return runHookWith(command, input, opts, REAL_DEPS);
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=run-hook.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type ScriptStatus = "pass" | "skip" | "fail";
|
|
2
|
+
export interface ScriptRunResult {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
readonly code: number;
|
|
5
|
+
readonly status: ScriptStatus;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Exit code a harness/eval script uses to report itself SKIPPED (e.g. the
|
|
9
|
+
* deterministic tier when `claude` isn't installed) — the autotools convention.
|
|
10
|
+
* The runner surfaces it as a loud `⊘ SKIPPED` instead of a silent `✓`, and a
|
|
11
|
+
* skip never fails the run. Scripts call `skip()` (vigiles/testing) to emit it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SKIP_EXIT_CODE = 77;
|
|
14
|
+
/** Filename extensions accepted for harness/eval scripts (JS and TS). */
|
|
15
|
+
export declare const SCRIPT_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
16
|
+
/** Glob suffix matching every accepted script extension, e.g. `harness`. */
|
|
17
|
+
export declare function scriptGlob(kind: "harness" | "eval"): string;
|
|
18
|
+
/** Node runtime capabilities that decide how a TypeScript script is run. */
|
|
19
|
+
export interface NodeCaps {
|
|
20
|
+
/** `tsx` is installed locally (the preferred, version-agnostic TS loader). */
|
|
21
|
+
readonly tsx: boolean;
|
|
22
|
+
/** Node supports `--experimental-strip-types` (>= 22.6). */
|
|
23
|
+
readonly stripTypes: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The `node` argv (after the binary) to run a single script. Plain JS runs
|
|
27
|
+
* directly; a TypeScript script picks `tsx` when available, else Node's native
|
|
28
|
+
* type stripping. Throws a clear, actionable error when neither is available.
|
|
29
|
+
* Pure — exported for testing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function interpreterArgs(file: string, caps: NodeCaps): string[];
|
|
32
|
+
/** Detect TS-running capabilities for a project root. */
|
|
33
|
+
export declare function detectNodeCaps(cwd: string): NodeCaps;
|
|
34
|
+
/**
|
|
35
|
+
* Expand the given path/glob patterns into concrete script files. A pattern
|
|
36
|
+
* that is an existing file passes through unchanged; anything else is treated
|
|
37
|
+
* as a glob. Falls back to `defaultGlob` when no patterns are given. Results
|
|
38
|
+
* are deduped and sorted; `node_modules` and `dist` are always ignored.
|
|
39
|
+
*/
|
|
40
|
+
export declare function discoverScripts(patterns: readonly string[], defaultGlob: string, cwd: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Run each script as `node <file>`, inheriting stdio so the script's own report
|
|
43
|
+
* streams to the console. `env` is merged over `process.env` for every child
|
|
44
|
+
* (e.g. `VIGILES_TRIALS`). Returns the per-file exit codes.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runScripts(files: readonly string[], cwd: string, env?: NodeJS.ProcessEnv): ScriptRunResult[];
|
|
47
|
+
/** Whether any script FAILED (a skip is not a failure). */
|
|
48
|
+
export declare function anyFailed(results: readonly ScriptRunResult[]): boolean;
|
|
49
|
+
/** One line per file + an explicit pass/skip/fail tally. Skips are SHOWN, never
|
|
50
|
+
* folded into "passed" — a `⊘ SKIPPED` is loud, not a silent green. */
|
|
51
|
+
export declare function formatScriptSummary(results: readonly ScriptRunResult[]): string;
|
|
52
|
+
//# sourceMappingURL=run-scripts.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCRIPT_EXTS = exports.SKIP_EXIT_CODE = void 0;
|
|
4
|
+
exports.scriptGlob = scriptGlob;
|
|
5
|
+
exports.interpreterArgs = interpreterArgs;
|
|
6
|
+
exports.detectNodeCaps = detectNodeCaps;
|
|
7
|
+
exports.discoverScripts = discoverScripts;
|
|
8
|
+
exports.runScripts = runScripts;
|
|
9
|
+
exports.anyFailed = anyFailed;
|
|
10
|
+
exports.formatScriptSummary = formatScriptSummary;
|
|
11
|
+
/**
|
|
12
|
+
* vigiles — run harness-test / eval script files via the CLI.
|
|
13
|
+
*
|
|
14
|
+
* `vigiles test` and `vigiles eval` discover `*.harness.*` / `*.eval.*`
|
|
15
|
+
* scripts and run each as a child `node` process, so the two-tier
|
|
16
|
+
* harness-testing API (`src/harness-test.ts`, `src/eval.ts`) works as a CI
|
|
17
|
+
* command, not just `node x.mjs`. Scripts may be authored in **JavaScript**
|
|
18
|
+
* (`.mjs` / `.cjs` / `.js`) **or TypeScript** (`.ts` / `.mts` / `.cts`) — a TS
|
|
19
|
+
* script is run through `tsx` when installed, else Node's built-in type
|
|
20
|
+
* stripping (Node >= 22.6). The scripts import from the built `dist/`, so they
|
|
21
|
+
* also run standalone — the CLI just discovers, runs, and aggregates exit codes.
|
|
22
|
+
*/
|
|
23
|
+
const node_child_process_1 = require("node:child_process");
|
|
24
|
+
const node_path_1 = require("node:path");
|
|
25
|
+
const node_fs_1 = require("node:fs");
|
|
26
|
+
const glob_1 = require("glob");
|
|
27
|
+
/**
|
|
28
|
+
* Exit code a harness/eval script uses to report itself SKIPPED (e.g. the
|
|
29
|
+
* deterministic tier when `claude` isn't installed) — the autotools convention.
|
|
30
|
+
* The runner surfaces it as a loud `⊘ SKIPPED` instead of a silent `✓`, and a
|
|
31
|
+
* skip never fails the run. Scripts call `skip()` (vigiles/testing) to emit it.
|
|
32
|
+
*/
|
|
33
|
+
exports.SKIP_EXIT_CODE = 77;
|
|
34
|
+
function statusForCode(code) {
|
|
35
|
+
if (code === 0)
|
|
36
|
+
return "pass";
|
|
37
|
+
if (code === exports.SKIP_EXIT_CODE)
|
|
38
|
+
return "skip";
|
|
39
|
+
return "fail";
|
|
40
|
+
}
|
|
41
|
+
/** Filename extensions accepted for harness/eval scripts (JS and TS). */
|
|
42
|
+
exports.SCRIPT_EXTS = ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
43
|
+
/** Glob suffix matching every accepted script extension, e.g. `harness`. */
|
|
44
|
+
function scriptGlob(kind) {
|
|
45
|
+
return `**/*.${kind}.{${exports.SCRIPT_EXTS.join(",")}}`;
|
|
46
|
+
}
|
|
47
|
+
const TS_EXT = /\.(?:m|c)?ts$/;
|
|
48
|
+
/**
|
|
49
|
+
* The `node` argv (after the binary) to run a single script. Plain JS runs
|
|
50
|
+
* directly; a TypeScript script picks `tsx` when available, else Node's native
|
|
51
|
+
* type stripping. Throws a clear, actionable error when neither is available.
|
|
52
|
+
* Pure — exported for testing.
|
|
53
|
+
*/
|
|
54
|
+
function interpreterArgs(file, caps) {
|
|
55
|
+
if (!TS_EXT.test(file))
|
|
56
|
+
return [file];
|
|
57
|
+
if (caps.tsx)
|
|
58
|
+
return ["--import", "tsx", file];
|
|
59
|
+
if (caps.stripTypes)
|
|
60
|
+
return ["--experimental-strip-types", file];
|
|
61
|
+
throw new Error(`Cannot run TypeScript test script "${file}": install tsx ` +
|
|
62
|
+
`(npm i -D tsx) or use Node >= 22.6, or author it as a .mjs file.`);
|
|
63
|
+
}
|
|
64
|
+
/** Detect TS-running capabilities for a project root. */
|
|
65
|
+
function detectNodeCaps(cwd) {
|
|
66
|
+
const tsx = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules/tsx/package.json")) ||
|
|
67
|
+
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules/.bin/tsx"));
|
|
68
|
+
const stripTypes = process.allowedNodeEnvironmentFlags.has("--experimental-strip-types");
|
|
69
|
+
return { tsx, stripTypes };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Expand the given path/glob patterns into concrete script files. A pattern
|
|
73
|
+
* that is an existing file passes through unchanged; anything else is treated
|
|
74
|
+
* as a glob. Falls back to `defaultGlob` when no patterns are given. Results
|
|
75
|
+
* are deduped and sorted; `node_modules` and `dist` are always ignored.
|
|
76
|
+
*/
|
|
77
|
+
function discoverScripts(patterns, defaultGlob, cwd) {
|
|
78
|
+
const globs = patterns.length > 0 ? patterns : [defaultGlob];
|
|
79
|
+
const found = new Set();
|
|
80
|
+
for (const p of globs) {
|
|
81
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, p))) {
|
|
82
|
+
found.add(p);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const m of (0, glob_1.globSync)(p, {
|
|
86
|
+
cwd,
|
|
87
|
+
ignore: ["node_modules/**", "dist/**"],
|
|
88
|
+
})) {
|
|
89
|
+
found.add(m);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...found].sort();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run each script as `node <file>`, inheriting stdio so the script's own report
|
|
96
|
+
* streams to the console. `env` is merged over `process.env` for every child
|
|
97
|
+
* (e.g. `VIGILES_TRIALS`). Returns the per-file exit codes.
|
|
98
|
+
*/
|
|
99
|
+
function runScripts(files, cwd, env = {}) {
|
|
100
|
+
const caps = detectNodeCaps(cwd);
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
let argv;
|
|
104
|
+
try {
|
|
105
|
+
argv = interpreterArgs(file, caps);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(`✗ ${file}: ${e.message}`);
|
|
109
|
+
results.push({ file, code: 1, status: "fail" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const res = (0, node_child_process_1.spawnSync)("node", argv, {
|
|
113
|
+
cwd,
|
|
114
|
+
stdio: "inherit",
|
|
115
|
+
env: { ...process.env, ...env },
|
|
116
|
+
});
|
|
117
|
+
const code = res.status ?? 1;
|
|
118
|
+
results.push({ file, code, status: statusForCode(code) });
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
/** Whether any script FAILED (a skip is not a failure). */
|
|
123
|
+
function anyFailed(results) {
|
|
124
|
+
return results.some((r) => r.status === "fail");
|
|
125
|
+
}
|
|
126
|
+
const MARK = {
|
|
127
|
+
pass: "✓",
|
|
128
|
+
skip: "⊘",
|
|
129
|
+
fail: "✗",
|
|
130
|
+
};
|
|
131
|
+
/** One line per file + an explicit pass/skip/fail tally. Skips are SHOWN, never
|
|
132
|
+
* folded into "passed" — a `⊘ SKIPPED` is loud, not a silent green. */
|
|
133
|
+
function formatScriptSummary(results) {
|
|
134
|
+
const lines = results.map((r) => {
|
|
135
|
+
if (r.status === "skip")
|
|
136
|
+
return ` ⊘ ${r.file} — SKIPPED`;
|
|
137
|
+
if (r.status === "fail")
|
|
138
|
+
return ` ✗ ${r.file} (exit ${String(r.code)})`;
|
|
139
|
+
return ` ${MARK.pass} ${r.file}`;
|
|
140
|
+
});
|
|
141
|
+
const n = (s) => results.filter((r) => r.status === s).length;
|
|
142
|
+
const parts = [`${String(n("pass"))} passed`];
|
|
143
|
+
if (n("skip") > 0)
|
|
144
|
+
parts.push(`${String(n("skip"))} skipped`);
|
|
145
|
+
if (n("fail") > 0)
|
|
146
|
+
parts.push(`${String(n("fail"))} failed`);
|
|
147
|
+
lines.push(`\n${parts.join(", ")}.`);
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=run-scripts.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claudeCodeRuntime — the Claude Code `HarnessRuntime` (spawn `claude`, reach the
|
|
3
|
+
* mock via `ANTHROPIC_BASE_URL` with a dummy `ANTHROPIC_API_KEY`). The runners
|
|
4
|
+
* (`harness-test.ts`, `eval.ts`) read the binary + env from here instead of
|
|
5
|
+
* hard-coding them; a Codex adapter defines `codexRuntime` and its runner uses it.
|
|
6
|
+
*/
|
|
7
|
+
import type { HarnessRuntime } from "../../core/runtime.js";
|
|
8
|
+
export declare const claudeCodeRuntime: HarnessRuntime;
|
|
9
|
+
/**
|
|
10
|
+
* Build the spawn env that points the agent CLI at the mock model: the caller's
|
|
11
|
+
* environment plus the runtime's base-URL var (→ the mock's URL) and a dummy
|
|
12
|
+
* API key. Pure (no spawn), so it's unit-tested directly — the testable seam of
|
|
13
|
+
* the otherwise un-coverable real-subprocess path.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mockModelEnv(runtime: HarnessRuntime, baseUrl: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
16
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeCodeRuntime = void 0;
|
|
4
|
+
exports.mockModelEnv = mockModelEnv;
|
|
5
|
+
exports.claudeCodeRuntime = {
|
|
6
|
+
name: "claude-code",
|
|
7
|
+
agentBinary: "claude",
|
|
8
|
+
modelBaseUrlEnv: "ANTHROPIC_BASE_URL",
|
|
9
|
+
modelApiKeyEnv: "ANTHROPIC_API_KEY",
|
|
10
|
+
mockApiKey: "sk-vigiles-mock",
|
|
11
|
+
/**
|
|
12
|
+
* Claude Code reaches the mock purely through env (no argv flags): the
|
|
13
|
+
* base-URL var + a dummy key. The returned `env` is the overlay the runner
|
|
14
|
+
* layers over `process.env`, so the spawn env is identical to `mockModelEnv`.
|
|
15
|
+
*/
|
|
16
|
+
wireMock(baseUrl) {
|
|
17
|
+
return {
|
|
18
|
+
args: [],
|
|
19
|
+
env: {
|
|
20
|
+
[exports.claudeCodeRuntime.modelBaseUrlEnv]: baseUrl,
|
|
21
|
+
[exports.claudeCodeRuntime.modelApiKeyEnv]: exports.claudeCodeRuntime.mockApiKey,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Build the spawn env that points the agent CLI at the mock model: the caller's
|
|
28
|
+
* environment plus the runtime's base-URL var (→ the mock's URL) and a dummy
|
|
29
|
+
* API key. Pure (no spawn), so it's unit-tested directly — the testable seam of
|
|
30
|
+
* the otherwise un-coverable real-subprocess path.
|
|
31
|
+
*/
|
|
32
|
+
function mockModelEnv(runtime, baseUrl, base = process.env) {
|
|
33
|
+
return {
|
|
34
|
+
...base,
|
|
35
|
+
[runtime.modelBaseUrlEnv]: baseUrl,
|
|
36
|
+
[runtime.modelApiKeyEnv]: runtime.mockApiKey,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=runtime.js.map
|