vigiles 2.6.0 → 4.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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +11 -2
- package/README.md +76 -129
- package/action.yml +144 -8
- 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.d.ts +1 -1
- package/dist/cli.js +670 -245
- 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 +91 -0
- package/dist/setup-plan.js +160 -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/edit-spec/SKILL.md +131 -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/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,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkAdapterConformance = checkAdapterConformance;
|
|
4
|
+
exports.assertAdapterConformance = assertAdapterConformance;
|
|
5
|
+
exports.assertHarnessTestable = assertHarnessTestable;
|
|
6
|
+
exports.assertAdapterLoadsHooks = assertAdapterLoadsHooks;
|
|
7
|
+
/**
|
|
8
|
+
* Adapter conformance kit — the reusable check every `HarnessAdapter` runs, so
|
|
9
|
+
* authoring one is guided and safe rather than "hope it's wired right". It
|
|
10
|
+
* verifies each port is populated AND a behavioural invariant: the adapter's
|
|
11
|
+
* dialect actually drives tool-contract verification (its own built-in tool is
|
|
12
|
+
* accepted). A third-party adapter author runs `assertAdapterConformance(myAdapter)`
|
|
13
|
+
* in their test suite. See `docs/authoring-an-adapter.md`.
|
|
14
|
+
*/
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
const compile_js_1 = require("./core/compile.js");
|
|
19
|
+
const spec_js_1 = require("./core/spec.js");
|
|
20
|
+
const plugin_loader_js_1 = require("./plugin-loader.js");
|
|
21
|
+
/** Check an adapter against the port contracts; returns the (possibly empty) failure list. */
|
|
22
|
+
function checkAdapterConformance(adapter) {
|
|
23
|
+
const failures = [];
|
|
24
|
+
const need = (cond, msg) => {
|
|
25
|
+
if (!cond)
|
|
26
|
+
failures.push(msg);
|
|
27
|
+
};
|
|
28
|
+
const caps = adapter.capabilities;
|
|
29
|
+
// Widen to boolean so a malformed (non-TS) adapter that set this false is still
|
|
30
|
+
// caught at runtime — the literal `true` type would make a direct check redundant.
|
|
31
|
+
const refVerification = caps.referenceVerification;
|
|
32
|
+
need(adapter.name.length > 0, "name is empty");
|
|
33
|
+
need(refVerification, "capabilities.referenceVerification must be true (every adapter does pillar 1)");
|
|
34
|
+
// --- Pillar 1 (always required): dialect + layout ---
|
|
35
|
+
need(adapter.dialect.builtinAgentTools.length > 0, "dialect has no builtinAgentTools");
|
|
36
|
+
need(adapter.dialect.instructionTargets.length > 0, "dialect has no instructionTargets");
|
|
37
|
+
need(adapter.layout.instructionFile.length > 0, "layout.instructionFile is empty");
|
|
38
|
+
need(adapter.layout.manifestPath.length > 0, "layout.manifestPath is empty");
|
|
39
|
+
need(adapter.layout.surfaceDirs.length > 0, "layout has no surfaceDirs");
|
|
40
|
+
need(typeof adapter.detect === "function", "detect is not a function");
|
|
41
|
+
// --- Pillar 2 transport ports: required ONLY for the capabilities the
|
|
42
|
+
// adapter declares. A pillar-1-only adapter (harnessTesting:false) may omit
|
|
43
|
+
// runtime/modelMock; a code-module-hook adapter (shellHooks:false) may omit
|
|
44
|
+
// hookProtocol — and conformance must NOT demand a fake one. The flip side:
|
|
45
|
+
// if it CLAIMS the capability, the port must be there and populated.
|
|
46
|
+
const portNames = [
|
|
47
|
+
["dialect", adapter.dialect.name],
|
|
48
|
+
["layout", adapter.layout.name],
|
|
49
|
+
];
|
|
50
|
+
if (caps.harnessTesting) {
|
|
51
|
+
need(adapter.runtime !== undefined, "capabilities.harnessTesting is true but runtime is missing");
|
|
52
|
+
need(adapter.modelMock !== undefined, "capabilities.harnessTesting is true but modelMock is missing");
|
|
53
|
+
if (adapter.runtime) {
|
|
54
|
+
need(adapter.runtime.agentBinary.length > 0, "runtime.agentBinary is empty");
|
|
55
|
+
need(adapter.runtime.modelBaseUrlEnv.length > 0, "runtime.modelBaseUrlEnv is empty");
|
|
56
|
+
portNames.push(["runtime", adapter.runtime.name]);
|
|
57
|
+
}
|
|
58
|
+
if (adapter.modelMock) {
|
|
59
|
+
need(adapter.modelMock.modelEndpoint.length > 0, "modelMock.modelEndpoint is empty");
|
|
60
|
+
portNames.push(["modelMock", adapter.modelMock.name]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
need(adapter.runtime === undefined && adapter.modelMock === undefined, "capabilities.harnessTesting is false — omit runtime/modelMock (a pillar-1-only adapter must not ship a half-wired transport)");
|
|
65
|
+
}
|
|
66
|
+
if (caps.shellHooks) {
|
|
67
|
+
need(adapter.hookProtocol !== undefined, "capabilities.shellHooks is true but hookProtocol is missing");
|
|
68
|
+
if (adapter.hookProtocol) {
|
|
69
|
+
need(Number.isInteger(adapter.hookProtocol.blockExitCode), "hookProtocol.blockExitCode is not an integer");
|
|
70
|
+
portNames.push(["hookProtocol", adapter.hookProtocol.name]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
need(adapter.hookProtocol === undefined, "capabilities.shellHooks is false — omit hookProtocol (hooks are code modules, not shell processes)");
|
|
75
|
+
}
|
|
76
|
+
// Cross-port invariants — the kind of mismatch a copy-paste authoring slip
|
|
77
|
+
// produces, that no single-port check would catch. Only the present ports.
|
|
78
|
+
for (const [port, name] of portNames) {
|
|
79
|
+
need(name === adapter.name, `${port}.name "${name}" != adapter.name "${adapter.name}"`);
|
|
80
|
+
}
|
|
81
|
+
need(adapter.layout.pluginRootToken === adapter.dialect.pluginRootToken, "layout.pluginRootToken and dialect.pluginRootToken disagree");
|
|
82
|
+
need(adapter.dialect.instructionTargets.includes(adapter.layout.instructionFile), `layout.instructionFile "${adapter.layout.instructionFile}" is not one of dialect.instructionTargets`);
|
|
83
|
+
need(adapter.layout.settingsFormat === "json" ||
|
|
84
|
+
adapter.layout.settingsFormat === "toml", `layout.settingsFormat "${adapter.layout.settingsFormat}" is not "json" | "toml"`);
|
|
85
|
+
// Behavioural: the dialect drives the compiler — its own built-in tool must
|
|
86
|
+
// pass the subagent tool-contract check under this dialect.
|
|
87
|
+
const tool = adapter.dialect.builtinAgentTools[0];
|
|
88
|
+
if (tool) {
|
|
89
|
+
const spec = (0, spec_js_1.agent)({
|
|
90
|
+
name: "conformance",
|
|
91
|
+
description: "conformance probe",
|
|
92
|
+
tools: [tool],
|
|
93
|
+
body: "probe",
|
|
94
|
+
});
|
|
95
|
+
const r = (0, compile_js_1.compileAgent)(spec, {
|
|
96
|
+
specFile: "conformance.md.spec.ts",
|
|
97
|
+
dialect: adapter.dialect,
|
|
98
|
+
});
|
|
99
|
+
need(!r.errors.some((e) => e.type === "unknown-tool"), `dialect rejects its own built-in tool "${tool}"`);
|
|
100
|
+
}
|
|
101
|
+
return { ok: failures.length === 0, failures };
|
|
102
|
+
}
|
|
103
|
+
/** Throw if the adapter fails conformance — drop this in an adapter's test suite. */
|
|
104
|
+
function assertAdapterConformance(adapter) {
|
|
105
|
+
const r = checkAdapterConformance(adapter);
|
|
106
|
+
if (!r.ok) {
|
|
107
|
+
throw new Error(`Adapter "${adapter.name}" failed conformance:\n - ${r.failures.join("\n - ")}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Guard for the pillar-2 entry points (runHarnessTest/runEval): a pillar-1-only
|
|
112
|
+
* adapter (Cursor, Devin, Amp, Amazon Q) has no mockable transport, so driving
|
|
113
|
+
* the deterministic/eval tiers against it would hang or spawn nothing. Calling
|
|
114
|
+
* this up front turns that into a clear, immediate error. Returns the narrowed
|
|
115
|
+
* runtime+modelMock so the caller can use them without re-checking for undefined.
|
|
116
|
+
*/
|
|
117
|
+
function assertHarnessTestable(adapter) {
|
|
118
|
+
if (!adapter.capabilities.harnessTesting ||
|
|
119
|
+
!adapter.runtime ||
|
|
120
|
+
!adapter.modelMock) {
|
|
121
|
+
throw new Error(`Adapter "${adapter.name}" does not support harness testing (pillar 2): it is reference-verification-only (no mockable runtime). Use it for compile/scan/audit, not runHarnessTest/runEval.`);
|
|
122
|
+
}
|
|
123
|
+
return { runtime: adapter.runtime, modelMock: adapter.modelMock };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Behavioural conformance the pure checks can't reach: write a minimal settings
|
|
127
|
+
* file in the adapter's declared `settingsFormat` (with a hook), load it through
|
|
128
|
+
* the adapter's `layout`, and assert the hooks actually came back. This is what
|
|
129
|
+
* catches a layout that points at the right file but in the wrong format (the
|
|
130
|
+
* JSON-vs-TOML trap) — the pure checker would pass it, the agent would silently
|
|
131
|
+
* run with zero hooks. Does filesystem IO, so it's a separate opt-in assert.
|
|
132
|
+
*/
|
|
133
|
+
function assertAdapterLoadsHooks(adapter) {
|
|
134
|
+
const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-conformance-"));
|
|
135
|
+
try {
|
|
136
|
+
const settingsAbs = (0, node_path_1.join)(dir, adapter.layout.settingsPath);
|
|
137
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(settingsAbs), { recursive: true });
|
|
138
|
+
const content = adapter.layout.settingsFormat === "toml"
|
|
139
|
+
? '[[hooks.PreToolUse]]\ncommand = "echo conformance"\n'
|
|
140
|
+
: JSON.stringify({
|
|
141
|
+
hooks: { PreToolUse: [{ command: "echo conformance" }] },
|
|
142
|
+
});
|
|
143
|
+
(0, node_fs_1.writeFileSync)(settingsAbs, content);
|
|
144
|
+
const loaded = (0, plugin_loader_js_1.loadPlugin)(dir, adapter.layout);
|
|
145
|
+
if (!loaded.settings.hooks) {
|
|
146
|
+
throw new Error(`Adapter "${adapter.name}": loadPlugin read no hooks from a ${adapter.layout.settingsFormat} settings file at ${adapter.layout.settingsPath} — the settings-format wiring is broken.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=adapter-conformance.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter registry — the composition-root list the CLI uses to auto-detect which
|
|
3
|
+
* harness a repo targets. The library API selects an adapter by import
|
|
4
|
+
* (`vigiles/claude-code`); the CLI can't, so it walks this registry. Claude Code
|
|
5
|
+
* is the default, so detection is backwards-compatible: an undetected repo (or a
|
|
6
|
+
* repo with no adapter markers) resolves to Claude Code exactly as before.
|
|
7
|
+
*
|
|
8
|
+
* Adding a harness = add its `HarnessAdapter` to `ADAPTERS` (and a
|
|
9
|
+
* `vigiles/<harness>` export). Order matters only if two adapters could both
|
|
10
|
+
* match a repo; `detect()` returns a specificity score so the strongest signal
|
|
11
|
+
* wins regardless of order.
|
|
12
|
+
*/
|
|
13
|
+
import type { HarnessAdapter } from "./core/adapter.js";
|
|
14
|
+
/** The default adapter when detection finds no harness markers. */
|
|
15
|
+
export declare const defaultAdapter: HarnessAdapter;
|
|
16
|
+
/** All registered adapters. detect() specificity (not order) breaks ties. */
|
|
17
|
+
export declare const ADAPTERS: readonly HarnessAdapter[];
|
|
18
|
+
/** The result of auto-detecting a harness from a repo's layout. */
|
|
19
|
+
export interface DetectResult {
|
|
20
|
+
readonly adapter: HarnessAdapter;
|
|
21
|
+
/** True when detection found no harness markers and fell back to the default. */
|
|
22
|
+
readonly fallback: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Other adapters that matched at the same top specificity — a non-empty list
|
|
25
|
+
* means the repo looks like more than one harness (e.g. a CLAUDE.md + an
|
|
26
|
+
* AGENTS.md), so the pick is ambiguous; resolve with `--harness`.
|
|
27
|
+
*/
|
|
28
|
+
readonly ambiguousWith: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** Auto-detect the harness for a repo at `root` by highest detect() specificity. */
|
|
31
|
+
export declare function detectAdapterResult(root: string): DetectResult;
|
|
32
|
+
/** The detected adapter (highest specificity), else the default (Claude Code). */
|
|
33
|
+
export declare function detectAdapter(root: string): HarnessAdapter;
|
|
34
|
+
/** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
|
|
35
|
+
export declare function getAdapter(name: string): HarnessAdapter | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
|
|
38
|
+
* if unknown); otherwise auto-detect from `root`. The single entry point the CLI
|
|
39
|
+
* uses so detection + override live in one place.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveAdapter(root: string, harness?: string): HarnessAdapter;
|
|
42
|
+
//# sourceMappingURL=adapter-registry.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ADAPTERS = exports.defaultAdapter = void 0;
|
|
4
|
+
exports.detectAdapterResult = detectAdapterResult;
|
|
5
|
+
exports.detectAdapter = detectAdapter;
|
|
6
|
+
exports.getAdapter = getAdapter;
|
|
7
|
+
exports.resolveAdapter = resolveAdapter;
|
|
8
|
+
const adapter_js_1 = require("./adapters/claude-code/adapter.js");
|
|
9
|
+
const adapter_js_2 = require("./adapters/codex/adapter.js");
|
|
10
|
+
/** The default adapter when detection finds no harness markers. */
|
|
11
|
+
exports.defaultAdapter = adapter_js_1.claudeCodeAdapter;
|
|
12
|
+
/** All registered adapters. detect() specificity (not order) breaks ties. */
|
|
13
|
+
exports.ADAPTERS = [
|
|
14
|
+
adapter_js_1.claudeCodeAdapter,
|
|
15
|
+
adapter_js_2.codexAdapter,
|
|
16
|
+
];
|
|
17
|
+
/** Auto-detect the harness for a repo at `root` by highest detect() specificity. */
|
|
18
|
+
function detectAdapterResult(root) {
|
|
19
|
+
const scored = exports.ADAPTERS.map((a) => ({ a, score: a.detect(root) })).filter((s) => s.score > 0);
|
|
20
|
+
if (scored.length === 0) {
|
|
21
|
+
return { adapter: exports.defaultAdapter, fallback: true, ambiguousWith: [] };
|
|
22
|
+
}
|
|
23
|
+
const top = Math.max(...scored.map((s) => s.score));
|
|
24
|
+
const winners = scored.filter((s) => s.score === top).map((s) => s.a);
|
|
25
|
+
return {
|
|
26
|
+
adapter: winners[0],
|
|
27
|
+
fallback: false,
|
|
28
|
+
ambiguousWith: winners.slice(1).map((a) => a.name),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** The detected adapter (highest specificity), else the default (Claude Code). */
|
|
32
|
+
function detectAdapter(root) {
|
|
33
|
+
return detectAdapterResult(root).adapter;
|
|
34
|
+
}
|
|
35
|
+
/** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
|
|
36
|
+
function getAdapter(name) {
|
|
37
|
+
return exports.ADAPTERS.find((a) => a.name === name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
|
|
41
|
+
* if unknown); otherwise auto-detect from `root`. The single entry point the CLI
|
|
42
|
+
* uses so detection + override live in one place.
|
|
43
|
+
*/
|
|
44
|
+
function resolveAdapter(root, harness) {
|
|
45
|
+
if (harness !== undefined) {
|
|
46
|
+
const a = getAdapter(harness);
|
|
47
|
+
if (!a) {
|
|
48
|
+
const known = exports.ADAPTERS.map((x) => x.name).join(", ");
|
|
49
|
+
throw new Error(`Unknown harness "${harness}". Known: ${known}.`);
|
|
50
|
+
}
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
return detectAdapter(root);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=adapter-registry.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vigiles/adapter` — the harness-adapter authoring kit.
|
|
3
|
+
*
|
|
4
|
+
* Everything you need to teach vigiles a new harness, in one import: the five
|
|
5
|
+
* port interfaces to implement, the `HarnessAdapter` bundle that groups them,
|
|
6
|
+
* the conformance kit to validate yours, and the registry the CLI detects
|
|
7
|
+
* through. vigiles ships a Claude Code adapter; building your own is welcome and
|
|
8
|
+
* supported — see `docs/authoring-an-adapter.md`.
|
|
9
|
+
*
|
|
10
|
+
* import {
|
|
11
|
+
* type HarnessAdapter, type HarnessDialect, type PluginLayout,
|
|
12
|
+
* type HarnessRuntime, type HookProtocol, type ModelMock,
|
|
13
|
+
* assertAdapterConformance,
|
|
14
|
+
* } from "vigiles/adapter";
|
|
15
|
+
*
|
|
16
|
+
* export const myHarnessAdapter: HarnessAdapter = { name: "my-harness", … };
|
|
17
|
+
*/
|
|
18
|
+
export type { HarnessAdapter, AdapterCapabilities } from "./core/adapter.js";
|
|
19
|
+
export type { HarnessDialect } from "./core/dialect.js";
|
|
20
|
+
export type { PluginLayout } from "./core/layout.js";
|
|
21
|
+
export type { HarnessRuntime } from "./core/runtime.js";
|
|
22
|
+
export type { HookProtocol } from "./core/hook-protocol.js";
|
|
23
|
+
export type { ModelMock } from "./core/model-mock.js";
|
|
24
|
+
export { checkAdapterConformance, assertAdapterConformance, assertAdapterLoadsHooks, assertHarnessTestable, type ConformanceResult, } from "./adapter-conformance.js";
|
|
25
|
+
export { ADAPTERS, defaultAdapter, detectAdapter, detectAdapterResult, resolveAdapter, getAdapter, type DetectResult, } from "./adapter-registry.js";
|
|
26
|
+
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAdapter = exports.resolveAdapter = exports.detectAdapterResult = exports.detectAdapter = exports.defaultAdapter = exports.ADAPTERS = exports.assertHarnessTestable = exports.assertAdapterLoadsHooks = exports.assertAdapterConformance = exports.checkAdapterConformance = void 0;
|
|
4
|
+
var adapter_conformance_js_1 = require("./adapter-conformance.js");
|
|
5
|
+
Object.defineProperty(exports, "checkAdapterConformance", { enumerable: true, get: function () { return adapter_conformance_js_1.checkAdapterConformance; } });
|
|
6
|
+
Object.defineProperty(exports, "assertAdapterConformance", { enumerable: true, get: function () { return adapter_conformance_js_1.assertAdapterConformance; } });
|
|
7
|
+
Object.defineProperty(exports, "assertAdapterLoadsHooks", { enumerable: true, get: function () { return adapter_conformance_js_1.assertAdapterLoadsHooks; } });
|
|
8
|
+
Object.defineProperty(exports, "assertHarnessTestable", { enumerable: true, get: function () { return adapter_conformance_js_1.assertHarnessTestable; } });
|
|
9
|
+
var adapter_registry_js_1 = require("./adapter-registry.js");
|
|
10
|
+
Object.defineProperty(exports, "ADAPTERS", { enumerable: true, get: function () { return adapter_registry_js_1.ADAPTERS; } });
|
|
11
|
+
Object.defineProperty(exports, "defaultAdapter", { enumerable: true, get: function () { return adapter_registry_js_1.defaultAdapter; } });
|
|
12
|
+
Object.defineProperty(exports, "detectAdapter", { enumerable: true, get: function () { return adapter_registry_js_1.detectAdapter; } });
|
|
13
|
+
Object.defineProperty(exports, "detectAdapterResult", { enumerable: true, get: function () { return adapter_registry_js_1.detectAdapterResult; } });
|
|
14
|
+
Object.defineProperty(exports, "resolveAdapter", { enumerable: true, get: function () { return adapter_registry_js_1.resolveAdapter; } });
|
|
15
|
+
Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return adapter_registry_js_1.getAdapter; } });
|
|
16
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeCodeAdapter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* claudeCodeAdapter — the Claude Code `HarnessAdapter`: the five port
|
|
6
|
+
* implementations bundled, plus a `detect` that recognizes a Claude Code repo
|
|
7
|
+
* (a `.claude-plugin/` manifest, a `.claude/settings.json`, or a `CLAUDE.md`).
|
|
8
|
+
* This is the reference adapter a second harness (Codex, Gemini, …) mirrors.
|
|
9
|
+
*/
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const dialect_js_1 = require("./dialect.js");
|
|
13
|
+
const layout_js_1 = require("./layout.js");
|
|
14
|
+
const runtime_js_1 = require("./runtime.js");
|
|
15
|
+
const hook_protocol_js_1 = require("./hook-protocol.js");
|
|
16
|
+
const model_mock_js_1 = require("./model-mock.js");
|
|
17
|
+
const harness_test_js_1 = require("./harness-test.js");
|
|
18
|
+
exports.claudeCodeAdapter = {
|
|
19
|
+
name: "claude-code",
|
|
20
|
+
// The reference harness: every tier. Mockable transport (Anthropic SSE) and
|
|
21
|
+
// shell hooks (exit 2 / decision JSON) — both pillars, all tiers.
|
|
22
|
+
capabilities: {
|
|
23
|
+
referenceVerification: true,
|
|
24
|
+
harnessTesting: true,
|
|
25
|
+
shellHooks: true,
|
|
26
|
+
},
|
|
27
|
+
dialect: dialect_js_1.claudeCodeDialect,
|
|
28
|
+
layout: layout_js_1.claudeCodeLayout,
|
|
29
|
+
runtime: runtime_js_1.claudeCodeRuntime,
|
|
30
|
+
hookProtocol: hook_protocol_js_1.claudeCodeHookProtocol,
|
|
31
|
+
modelMock: model_mock_js_1.claudeCodeModelMock,
|
|
32
|
+
harnessTestDriver: harness_test_js_1.claudeCodeDriver,
|
|
33
|
+
detect(root) {
|
|
34
|
+
// Most specific signal wins: a plugin manifest (3) > repo settings (2) >
|
|
35
|
+
// a bare CLAUDE.md (1, weak — many tools also read it / AGENTS.md).
|
|
36
|
+
const has = (rel) => (0, node_fs_1.existsSync)((0, node_path_1.join)(root, rel));
|
|
37
|
+
if (has(layout_js_1.claudeCodeLayout.manifestPath))
|
|
38
|
+
return 3;
|
|
39
|
+
if (has(layout_js_1.claudeCodeLayout.settingsPath))
|
|
40
|
+
return 2;
|
|
41
|
+
if (has(layout_js_1.claudeCodeLayout.instructionFile))
|
|
42
|
+
return 1;
|
|
43
|
+
return 0;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* third `malformed` track for a worker that didn't honor its contract (no block,
|
|
18
18
|
* bad JSON, or a shape that doesn't match the declared schema).
|
|
19
19
|
*/
|
|
20
|
-
import type { OutputContract } from "
|
|
20
|
+
import type { OutputContract } from "../../core/spec.js";
|
|
21
21
|
/** The outcome of parsing a worker's result block. */
|
|
22
22
|
export type ParsedAgentResult<S = Record<string, unknown>, E = Record<string, unknown>> = {
|
|
23
23
|
readonly kind: "ok";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claudeCodeDialect — the Claude Code adapter's `HarnessDialect` (format axis).
|
|
3
|
+
* The concrete dialect is DEFINED here, in the adapter, symmetric with the other
|
|
4
|
+
* four ports (`claudeCodeLayout`/`Runtime`/`HookProtocol`/`ModelMock`): the core
|
|
5
|
+
* holds only the `HarnessDialect` interface and never a harness's vocabulary. The
|
|
6
|
+
* compiler/validator receive this by injection (`compileAgent(spec, { dialect })`,
|
|
7
|
+
* the CLI/composition root supplies it). A second harness defines its own dialect
|
|
8
|
+
* in its adapter (e.g. `src/adapters/codex/dialect.ts` exporting `codexDialect`).
|
|
9
|
+
*/
|
|
10
|
+
import type { HarnessDialect } from "../../core/dialect.js";
|
|
11
|
+
export declare const claudeCodeDialect: HarnessDialect;
|
|
12
|
+
export type { HarnessDialect } from "../../core/dialect.js";
|
|
13
|
+
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claudeCodeDialect = void 0;
|
|
4
|
+
exports.claudeCodeDialect = {
|
|
5
|
+
name: "claude-code",
|
|
6
|
+
// The tool contract a subagent may declare — the rails it runs on. Anything
|
|
7
|
+
// else must be an MCP tool, else it's a typo / nonexistent tool.
|
|
8
|
+
builtinAgentTools: [
|
|
9
|
+
"Read",
|
|
10
|
+
"Write",
|
|
11
|
+
"Edit",
|
|
12
|
+
"Bash",
|
|
13
|
+
"Grep",
|
|
14
|
+
"Glob",
|
|
15
|
+
"WebSearch",
|
|
16
|
+
"WebFetch",
|
|
17
|
+
"NotebookEdit",
|
|
18
|
+
"TodoWrite",
|
|
19
|
+
"Task",
|
|
20
|
+
"Skill",
|
|
21
|
+
],
|
|
22
|
+
// Tools the platform never exposes to a subagent, whatever the list says — so
|
|
23
|
+
// a subagent listing one is a guaranteed-dead reference only a compiler catches.
|
|
24
|
+
neverAvailableTools: [
|
|
25
|
+
"Agent",
|
|
26
|
+
"AskUserQuestion",
|
|
27
|
+
"EnterPlanMode",
|
|
28
|
+
"ExitPlanMode",
|
|
29
|
+
"ScheduleWakeup",
|
|
30
|
+
"WaitForMcpServers",
|
|
31
|
+
],
|
|
32
|
+
mcpToolPattern: /^mcp__[a-z0-9_-]+__[a-z0-9_-]+$/i,
|
|
33
|
+
hookEvents: [
|
|
34
|
+
"PreToolUse",
|
|
35
|
+
"PostToolUse",
|
|
36
|
+
"PreSession",
|
|
37
|
+
"PostSession",
|
|
38
|
+
"Notification",
|
|
39
|
+
],
|
|
40
|
+
// Claude Code natively reads CLAUDE.md only — it does NOT auto-load AGENTS.md
|
|
41
|
+
// (anthropics/claude-code#34235 is open; AGENTS.md works solely via an
|
|
42
|
+
// `@AGENTS.md` import inside CLAUDE.md or a symlink). AGENTS.md is the
|
|
43
|
+
// cross-tool standard (Codex's native target), not a CC dialect fact; vigiles's
|
|
44
|
+
// tool-agnostic recognition of it lives in validate.ts's INSTRUCTION_FILES.
|
|
45
|
+
instructionTargets: ["CLAUDE.md"],
|
|
46
|
+
pluginRootToken: "${CLAUDE_PLUGIN_ROOT}",
|
|
47
|
+
// Claude Code reads the full SKILL.md frontmatter set (description,
|
|
48
|
+
// disable-model-invocation, argument-hint, …).
|
|
49
|
+
skillFrontmatter: "claude-code",
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=dialect.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* vigiles — the allowlisted-egress orchestrator (in-process subprocess entry).
|
|
5
|
+
*
|
|
6
|
+
* `runHook` is synchronous (`spawnSync`), but allowlisted egress needs TWO
|
|
7
|
+
* processes alive at once: `bwrap` (which creates the netns and runs the hook)
|
|
8
|
+
* and `slirp4netns` (which, from the PARENT netns, attaches a tap to bwrap's netns
|
|
9
|
+
* to give it controlled egress). So the parent `spawnSync`s THIS entry, which runs
|
|
10
|
+
* the concurrent dance and writes a result file the parent reads back — keeping
|
|
11
|
+
* `runHook` synchronous while the handoff happens here.
|
|
12
|
+
*
|
|
13
|
+
* The dance (proven in `research/spikes/sandbox-network-allowlist.sh`):
|
|
14
|
+
* 1. spawn bwrap with `--info-fd` → learn the sandboxed child's PID;
|
|
15
|
+
* 2. `slirp4netns --configure --ready-fd N <pid> tap0` → tap up inside the netns;
|
|
16
|
+
* 3. touch the netready file → the in-sandbox wrapper (blocked on it) proceeds:
|
|
17
|
+
* it loads the nft allowlist, runs the hook, then dumps the nft counters to a
|
|
18
|
+
* bound file BEFORE exiting (the netns dies with the child, so the read-back
|
|
19
|
+
* must happen inside);
|
|
20
|
+
* 4. capture bwrap's stdout/stderr/exit + the counters → write the result file.
|
|
21
|
+
*
|
|
22
|
+
* Run as: `node dist/egress-entry.js <config.json>`. v8-ignored: it spawns real
|
|
23
|
+
* bwrap + slirp4netns and is exercised only by the bwrap/slirp-gated integration
|
|
24
|
+
* test; the testable logic (ruleset, counter parse) lives in `src/egress.ts`.
|
|
25
|
+
*/
|
|
26
|
+
/* v8 ignore start */
|
|
27
|
+
const node_child_process_1 = require("node:child_process");
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
/** Is a binary on PATH? (probe via `--version`; ENOENT sets `.error`.) */
|
|
31
|
+
function hasBinary(name) {
|
|
32
|
+
return !(0, node_child_process_1.spawnSync)(name, ["--version"], { stdio: "ignore" }).error;
|
|
33
|
+
}
|
|
34
|
+
/** Poll a file until it has content (the info / ready fd targets), or time out. */
|
|
35
|
+
async function waitForFile(path, timeoutMs) {
|
|
36
|
+
const deadline = Date.now() + timeoutMs;
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
try {
|
|
39
|
+
if ((0, node_fs_1.readFileSync)(path, "utf-8").trim().length > 0)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* not written yet */
|
|
44
|
+
}
|
|
45
|
+
await sleep(50);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const cfg = JSON.parse((0, node_fs_1.readFileSync)(process.argv[2], "utf-8"));
|
|
51
|
+
// bwrap writes the sandboxed child's PID (init-namespace view) to fd 3.
|
|
52
|
+
const infoFd = (0, node_fs_1.openSync)(cfg.infoFile, "w");
|
|
53
|
+
const bwrap = (0, node_child_process_1.spawn)("bwrap", cfg.bwrapArgv, {
|
|
54
|
+
stdio: ["ignore", "pipe", "pipe", infoFd],
|
|
55
|
+
});
|
|
56
|
+
(0, node_fs_1.closeSync)(infoFd);
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
bwrap.stdout?.on("data", (d) => (stdout += d.toString()));
|
|
60
|
+
bwrap.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
61
|
+
let connector;
|
|
62
|
+
const timer = setTimeout(() => bwrap.kill("SIGKILL"), cfg.timeoutMs);
|
|
63
|
+
const exit = new Promise((resolve) => {
|
|
64
|
+
bwrap.on("close", (status, signal) => {
|
|
65
|
+
resolve({ status, signal });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// Learn the child PID, attach a rootless egress connector to its netns, release
|
|
69
|
+
// the wrapper. Prefer pasta (passt): it routes on hosted runners where
|
|
70
|
+
// slirp4netns's tap-attach silently fails (the netns ends up with only `lo`).
|
|
71
|
+
// Fall back to slirp4netns where pasta isn't installed. nft (the allowlist wall)
|
|
72
|
+
// matches on destination, not interface name, so it's connector-agnostic. See
|
|
73
|
+
// research/egress-sandbox-tooling.md.
|
|
74
|
+
if (await waitForFile(cfg.infoFile, 5_000)) {
|
|
75
|
+
const info = (0, node_fs_1.readFileSync)(cfg.infoFile, "utf-8");
|
|
76
|
+
const m = /"child-pid":\s*(\d+)/.exec(info);
|
|
77
|
+
if (m) {
|
|
78
|
+
if (cfg.connector === "pasta" && hasBinary("pasta")) {
|
|
79
|
+
// pasta configures the netns then forks to background; no ready-fd, so
|
|
80
|
+
// give it a moment to bring the interface + routes up. (Experimental:
|
|
81
|
+
// pasta's attach-to-existing-netns path is fragile in nested-namespace
|
|
82
|
+
// sandboxes — see research/egress-sandbox-tooling.md. The connector
|
|
83
|
+
// defaults to slirp4netns; both need /dev/net/tun + privilege, which is
|
|
84
|
+
// why the e2e CI job runs privileged.)
|
|
85
|
+
connector = (0, node_child_process_1.spawn)("pasta", [...cfg.pastaArgs, m[1]], {
|
|
86
|
+
stdio: "ignore",
|
|
87
|
+
});
|
|
88
|
+
await sleep(800);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const readyFd = (0, node_fs_1.openSync)(cfg.readyFile, "w");
|
|
92
|
+
connector = (0, node_child_process_1.spawn)("slirp4netns", [...cfg.slirpArgs, "--ready-fd", "4", m[1], "tap0"], { stdio: ["ignore", "ignore", "ignore", "ignore", readyFd] });
|
|
93
|
+
(0, node_fs_1.closeSync)(readyFd);
|
|
94
|
+
// slirp writes "1" to the ready fd once tap0 is configured; fall back to a
|
|
95
|
+
// short sleep if it never signals (older builds).
|
|
96
|
+
if (!(await waitForFile(cfg.readyFile, 4_000)))
|
|
97
|
+
await sleep(800);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
(0, node_fs_1.writeFileSync)(cfg.netreadyFile, "1");
|
|
102
|
+
const { status, signal } = await exit;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
connector?.kill("SIGKILL");
|
|
105
|
+
const counters = (0, node_fs_1.existsSync)(cfg.countersFile)
|
|
106
|
+
? (0, node_fs_1.readFileSync)(cfg.countersFile, "utf-8")
|
|
107
|
+
: "";
|
|
108
|
+
(0, node_fs_1.writeFileSync)(cfg.resultFile, JSON.stringify({ status, signal, stdout, stderr, counters }));
|
|
109
|
+
}
|
|
110
|
+
main().catch((e) => {
|
|
111
|
+
(0, node_fs_1.writeFileSync)(process.argv[2] + ".error", e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
/* v8 ignore stop */
|
|
115
|
+
//# sourceMappingURL=egress-entry.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type EgressAttempt } from "./sandbox.js";
|
|
2
|
+
/** A host plus the IPs it resolved to, split by family (nft needs them apart). */
|
|
3
|
+
export interface ResolvedHost {
|
|
4
|
+
readonly host: string;
|
|
5
|
+
readonly v4: readonly string[];
|
|
6
|
+
readonly v6: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Per-host and aggregate nftables counters read back after a confined run. */
|
|
9
|
+
export interface EgressCounters {
|
|
10
|
+
/** Allowlisted hosts that saw traffic, with the nft counter for each. */
|
|
11
|
+
readonly allowed: readonly {
|
|
12
|
+
readonly host: string;
|
|
13
|
+
readonly packets: number;
|
|
14
|
+
readonly bytes: number;
|
|
15
|
+
}[];
|
|
16
|
+
/** The catch-all drop rule: traffic to anything OFF the allowlist. */
|
|
17
|
+
readonly dropped: {
|
|
18
|
+
readonly packets: number;
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether this host can run the allowlisted-egress sandbox: it needs the same
|
|
24
|
+
* bubblewrap confinement the other tiers use PLUS `slirp4netns` (the rootless
|
|
25
|
+
* gateway) and `nft` (the packet-layer allowlist). Cached — the answer can't
|
|
26
|
+
* change within a run. `available` is injected so the bwrap probe isn't repeated.
|
|
27
|
+
*/
|
|
28
|
+
export declare function probeEgressAvailable(available: boolean): boolean;
|
|
29
|
+
/** Cached {@link probeEgressAvailable}. `available` = the bwrap-sandbox probe. */
|
|
30
|
+
export declare function egressAvailable(available: boolean): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Parse `getent ahosts <host>` output into the unique IPs, split by family. The
|
|
33
|
+
* first whitespace token of each line is an address; `:` marks IPv6. Pure, so the
|
|
34
|
+
* resolution-parsing is unit-tested without touching DNS.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseGetent(stdout: string): {
|
|
37
|
+
v4: string[];
|
|
38
|
+
v6: string[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Resolve every allowlisted host to IPs. The lookup is injectable so the
|
|
42
|
+
* empty-result / family-split behaviour is unit-tested with a fake resolver.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveAllow(hosts: readonly string[], lookup?: (host: string) => {
|
|
45
|
+
v4: string[];
|
|
46
|
+
v6: string[];
|
|
47
|
+
}): ResolvedHost[];
|
|
48
|
+
/**
|
|
49
|
+
* Nameservers usable from INSIDE the egress netns. Parses a resolv.conf body but
|
|
50
|
+
* drops loopback stubs (see {@link isLoopbackResolver}) — they can't be reached
|
|
51
|
+
* across the namespace — and falls back to a public resolver (8.8.8.8, which
|
|
52
|
+
* slirp4netns NATs out) when nothing routable remains.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseResolvers(resolvConf: string): string[];
|
|
55
|
+
/**
|
|
56
|
+
* Build the nftables ruleset that enforces the allowlist INSIDE the netns: a
|
|
57
|
+
* `policy drop` output chain that accepts loopback, established replies, DNS to
|
|
58
|
+
* the resolvers, and each allowlisted host's resolved IPs (a per-host `counter` +
|
|
59
|
+
* `comment "allow:<host>"` so the read-back maps traffic to a name), then a
|
|
60
|
+
* catch-all `log`+`counter`+`drop`. v4 and v6 are separate rules (an `inet` set
|
|
61
|
+
* can't mix families) sharing the host's comment. Pure → the ruleset is asserted
|
|
62
|
+
* in a unit test.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildEgressNft(opts: {
|
|
65
|
+
allow: readonly ResolvedHost[];
|
|
66
|
+
resolvers: readonly string[];
|
|
67
|
+
}): string;
|
|
68
|
+
/** The bound paths the in-netns wrapper and the orchestrator hand back through. */
|
|
69
|
+
export interface EgressFiles {
|
|
70
|
+
readonly ioDir: string;
|
|
71
|
+
readonly netready: string;
|
|
72
|
+
readonly nft: string;
|
|
73
|
+
readonly event: string;
|
|
74
|
+
readonly counters: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Assemble the full bwrap argv for an allowlisted-egress run: the shared
|
|
78
|
+
* confinement args, the hook's added-back env, `CAP_NET_ADMIN` (so the in-netns
|
|
79
|
+
* wrapper can load nft), `--info-fd 3` (so the orchestrator learns the child PID
|
|
80
|
+
* to hand slirp4netns), the `VIG_*` paths the wrapper reads, and the trailing
|
|
81
|
+
* `sh -c <wrapper>`. Pure (the bwrap/setenv args are computed by the caller), so
|
|
82
|
+
* the assembled shape — caps, info-fd, the VIG_* env, the wrapper payload — is
|
|
83
|
+
* asserted in a unit test.
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildEgressBwrapArgv(opts: {
|
|
86
|
+
base: readonly string[];
|
|
87
|
+
setenv: readonly string[];
|
|
88
|
+
files: EgressFiles;
|
|
89
|
+
command: string;
|
|
90
|
+
wrapper: string;
|
|
91
|
+
/** Host path to the generated resolv.conf bound over /etc/resolv.conf in-netns,
|
|
92
|
+
* so the hook resolves via the routable resolvers, not the host's loopback stub. */
|
|
93
|
+
resolvConf: string;
|
|
94
|
+
}): string[];
|
|
95
|
+
/**
|
|
96
|
+
* Parse `nft list chain inet vig output` back into per-host allowed counters and
|
|
97
|
+
* the aggregate drop counter. Sums multiple rules that share a host comment (the
|
|
98
|
+
* v4 + v6 split). Pure, so the read-back is unit-tested without a sandbox.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseNftCounters(nftText: string): EgressCounters;
|
|
101
|
+
/**
|
|
102
|
+
* Map parsed counters to the run result: one {@link EgressAttempt} per
|
|
103
|
+
* allowlisted host that saw traffic (`allowed: true`, with its packet/byte
|
|
104
|
+
* counts), plus the aggregate dropped counter. `ts` is stamped once per run (the
|
|
105
|
+
* counters are end-of-run totals, not per-connection events). Pure.
|
|
106
|
+
*/
|
|
107
|
+
export declare function countersToResult(counters: EgressCounters, now: number): {
|
|
108
|
+
egress: EgressAttempt[];
|
|
109
|
+
egressDropped: {
|
|
110
|
+
packets: number;
|
|
111
|
+
bytes: number;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=egress.d.ts.map
|