vigiles 5.2.0 → 7.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/README.md +99 -48
- package/dist/action-gate.js +1 -1
- package/dist/adapters/claude-code/agent-runtime.d.ts +64 -4
- package/dist/adapters/claude-code/agent-runtime.js +131 -17
- package/dist/adapters/claude-code/dialect.d.ts +34 -0
- package/dist/adapters/claude-code/dialect.js +46 -33
- package/dist/adapters/claude-code/effect-region.js +1 -1
- package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
- package/dist/adapters/claude-code/skill-runtime.js +1 -9
- package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
- package/dist/adapters/claude-code/typed-spec.js +55 -0
- package/dist/adapters/codex/hook-protocol.js +3 -0
- package/dist/adapters/codex/mock-model.js +1 -1
- package/dist/claude-code.d.ts +1 -0
- package/dist/claude-code.js +8 -1
- package/dist/cli-commands.d.ts +19 -0
- package/dist/cli-commands.js +51 -0
- package/dist/cli.js +735 -76
- package/dist/core/bash-effects.d.ts +12 -0
- package/dist/core/bash-effects.js +31 -0
- package/dist/core/capability-diff.d.ts +46 -0
- package/dist/core/capability-diff.js +97 -0
- package/dist/core/compile.d.ts +1 -1
- package/dist/core/compile.js +14 -0
- package/dist/core/generate-harness.d.ts +187 -0
- package/dist/core/generate-harness.js +337 -0
- package/dist/core/guards.d.ts +126 -0
- package/dist/core/guards.js +309 -0
- package/dist/core/harness-driver.d.ts +1 -1
- package/dist/core/hook-program.d.ts +459 -0
- package/dist/core/hook-program.js +468 -0
- package/dist/core/hook-protocol.d.ts +7 -0
- package/dist/core/hook-providers.d.ts +138 -0
- package/dist/core/hook-providers.js +155 -0
- package/dist/core/hook-spec.d.ts +74 -0
- package/dist/core/hook-spec.js +130 -0
- package/dist/core/inline.js +1 -1
- package/dist/core/mcp-tool.d.ts +12 -0
- package/dist/core/mcp-tool.js +20 -0
- package/dist/core/mcp.d.ts +13 -0
- package/dist/core/mcp.js +67 -0
- package/dist/core/spec.d.ts +290 -8
- package/dist/core/spec.js +118 -3
- package/dist/core/types.d.ts +8 -0
- package/dist/dialect-drift.d.ts +65 -0
- package/dist/dialect-drift.js +216 -0
- package/dist/eval.d.ts +40 -5
- package/dist/eval.js +59 -5
- package/dist/guardrail-check.d.ts +85 -0
- package/dist/guardrail-check.js +152 -0
- package/dist/harness-assert.d.ts +10 -0
- package/dist/harness-assert.js +30 -0
- package/dist/hook-install.d.ts +43 -0
- package/dist/hook-install.js +91 -0
- package/dist/hook.d.ts +52 -0
- package/dist/hook.js +98 -0
- package/dist/leaderboard.d.ts +6 -0
- package/dist/leaderboard.js +43 -1
- package/dist/linting.d.ts +9 -5
- package/dist/linting.js +17 -5
- package/dist/optimize.js +1 -1
- package/dist/scaffold-test.d.ts +28 -0
- package/dist/scaffold-test.js +134 -15
- package/dist/scan-behavioral.d.ts +60 -0
- package/dist/scan-behavioral.js +239 -1
- package/dist/scan.d.ts +14 -0
- package/dist/scan.js +33 -1
- package/dist/score-explainer.js +1 -1
- package/dist/self-command-refs.d.ts +21 -0
- package/dist/self-command-refs.js +125 -0
- package/dist/testing.d.ts +5 -3
- package/dist/testing.js +37 -23
- package/dist/tool-intercept.d.ts +4 -4
- package/dist/tool-intercept.js +5 -5
- package/dist/unit.d.ts +2 -0
- package/dist/unit.js +8 -1
- package/hooks/refs-nudge.sh +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BUILTIN_PROVIDERS = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = void 0;
|
|
4
|
+
exports.unknownProviders = unknownProviders;
|
|
5
|
+
exports.unsafeInlineProviders = unsafeInlineProviders;
|
|
6
|
+
exports.unsafeProvider = unsafeProvider;
|
|
7
|
+
exports.gatherContext = gatherContext;
|
|
8
|
+
/**
|
|
9
|
+
* Hook CONTEXT PROVIDERS — let a compiled gate decide on EXTERNAL STATE (git
|
|
10
|
+
* branch, working-tree dirtiness, cwd, OS) WITHOUT breaking `capability = API
|
|
11
|
+
* surface`. The design (research/hook-context-providers.md, grounded in
|
|
12
|
+
* Cedar/OPA/Gatekeeper): the pure `decide` never fetches; the TRUSTED runtime
|
|
13
|
+
* gathers a DECLARED set of read-only facts and hands them in as `e.ctx`. A hook
|
|
14
|
+
* declares `needs: [...]`, so the dependency is explicit + auditable, and the
|
|
15
|
+
* fact-gathering I/O lives here (the trusted host), never inside the hook.
|
|
16
|
+
*
|
|
17
|
+
* Two tiers of `needs` entry, both DECLARED (the opt-out ladder):
|
|
18
|
+
* - a built-in provider NAME (`"git.branch"`) — the curated, famous-80% set;
|
|
19
|
+
* - an INLINE one-off — `provide(name, cmd)` (read-only, compile-rejected if not)
|
|
20
|
+
* or `dangerously(name, cmd)` (the loud, greppable escape for a command that
|
|
21
|
+
* isn't provably read-only) — for the long tail, no registration ceremony.
|
|
22
|
+
*
|
|
23
|
+
* Every built-in command is provably read-only (asserted in the test via
|
|
24
|
+
* bash-effects); a `provide()` command is checked the same way at compile. The
|
|
25
|
+
* gathering is parameterized over an injected `ProviderIO`, so the registry is
|
|
26
|
+
* testable with a fake exec and core depends on no child_process.
|
|
27
|
+
*/
|
|
28
|
+
const bash_effects_js_1 = require("./bash-effects.js");
|
|
29
|
+
/**
|
|
30
|
+
* Declare an INLINE read-only fact: `provide("k8sCtx", "kubectl config current-context")`.
|
|
31
|
+
* The command MUST be provably read-only (compile rejects it otherwise — use
|
|
32
|
+
* {@link dangerously} to acknowledge a side-effecting/undecidable one). Its stdout
|
|
33
|
+
* is `e.ctx[name]`.
|
|
34
|
+
*/
|
|
35
|
+
const provide = (name, run) => ({ kind: "inline", name, run, dangerous: false });
|
|
36
|
+
exports.provide = provide;
|
|
37
|
+
/**
|
|
38
|
+
* Declare an INLINE fact whose command ISN'T provably read-only — the loud,
|
|
39
|
+
* greppable escape hatch (the `dangerouslySetInnerHTML` / `unsafe` / `http.send`
|
|
40
|
+
* convention; sibling of `purity:'dangerously-unrestricted'`). A security review
|
|
41
|
+
* searches for this one word.
|
|
42
|
+
*/
|
|
43
|
+
const dangerously = (name, run) => ({ kind: "inline", name, run, dangerous: true });
|
|
44
|
+
exports.dangerously = dangerously;
|
|
45
|
+
/** Author a registered provider: `export default defineProvider({ name, run })`. */
|
|
46
|
+
const defineProvider = (p) => ({
|
|
47
|
+
kind: "provider-def",
|
|
48
|
+
name: p.name,
|
|
49
|
+
run: p.run,
|
|
50
|
+
dangerous: p.dangerous ?? false,
|
|
51
|
+
});
|
|
52
|
+
exports.defineProvider = defineProvider;
|
|
53
|
+
/** Reference a registered provider (from `.vigiles/providers/`) by name in `needs`. */
|
|
54
|
+
const provider = (name) => ({ kind: "provider-ref", name });
|
|
55
|
+
exports.provider = provider;
|
|
56
|
+
/** Run a command and trim, returning "" on any failure (total). */
|
|
57
|
+
function tryExec(io, command) {
|
|
58
|
+
try {
|
|
59
|
+
return io.exec(command).trim();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The closed built-in registry. Each `run` is provably read-only (see the test). */
|
|
66
|
+
exports.BUILTIN_PROVIDERS = {
|
|
67
|
+
"git.branch": {
|
|
68
|
+
run: "git branch --show-current",
|
|
69
|
+
gather: (io) => tryExec(io, "git branch --show-current"),
|
|
70
|
+
},
|
|
71
|
+
"git.isDirty": {
|
|
72
|
+
run: "git status --porcelain",
|
|
73
|
+
gather: (io) => tryExec(io, "git status --porcelain").length > 0,
|
|
74
|
+
},
|
|
75
|
+
"git.root": {
|
|
76
|
+
run: "git rev-parse --show-toplevel",
|
|
77
|
+
gather: (io) => tryExec(io, "git rev-parse --show-toplevel"),
|
|
78
|
+
},
|
|
79
|
+
cwd: {
|
|
80
|
+
gather: (io) => io.cwd,
|
|
81
|
+
},
|
|
82
|
+
"os.platform": {
|
|
83
|
+
gather: (io) => io.platform,
|
|
84
|
+
},
|
|
85
|
+
"env.isCI": {
|
|
86
|
+
gather: (io) => io.isCI,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/** True iff a `needs` entry is an inline `provide`/`dangerously`. */
|
|
90
|
+
function isInline(need) {
|
|
91
|
+
return typeof need !== "string" && need.kind === "inline";
|
|
92
|
+
}
|
|
93
|
+
/** True iff a `needs` entry is a `provider()` reference to a registered provider. */
|
|
94
|
+
function isRef(need) {
|
|
95
|
+
return typeof need !== "string" && need.kind === "provider-ref";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `needs` entries that don't resolve: a built-in NAME that isn't a built-in (a
|
|
99
|
+
* typo), or a `provider()` ref whose name isn't in `registeredNames` (a dangling
|
|
100
|
+
* ref). Inline `provide`/`dangerously` are always self-defined, so never flagged.
|
|
101
|
+
*/
|
|
102
|
+
function unknownProviders(needs, registeredNames = []) {
|
|
103
|
+
const registered = new Set(registeredNames);
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const n of needs) {
|
|
106
|
+
if (isInline(n))
|
|
107
|
+
continue;
|
|
108
|
+
if (isRef(n)) {
|
|
109
|
+
if (!registered.has(n.name))
|
|
110
|
+
out.push(n.name);
|
|
111
|
+
}
|
|
112
|
+
else if (!(n in exports.BUILTIN_PROVIDERS))
|
|
113
|
+
out.push(n);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Inline `provide()` entries whose command ISN'T provably read-only — these must
|
|
119
|
+
* switch to `dangerously()` (the acknowledged escape) or compile is rejected. A
|
|
120
|
+
* `dangerously()` entry is never flagged (it already acknowledged the risk).
|
|
121
|
+
*/
|
|
122
|
+
function unsafeInlineProviders(needs) {
|
|
123
|
+
return needs
|
|
124
|
+
.filter(isInline)
|
|
125
|
+
.filter((n) => !n.dangerous && !(0, bash_effects_js_1.isReadOnlyBash)(n.run));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Is a REGISTERED provider's command unsafe (not read-only and not acknowledged
|
|
129
|
+
* `dangerous`)? Reused when compiling a `.vigiles/providers/` file.
|
|
130
|
+
*/
|
|
131
|
+
function unsafeProvider(def) {
|
|
132
|
+
return !def.dangerous && !(0, bash_effects_js_1.isReadOnlyBash)(def.run);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Gather the DECLARED facts into a context object (the trusted-host step). Only
|
|
136
|
+
* the names in `needs` are gathered, each at most once; a built-in, inline, or
|
|
137
|
+
* registered provider that can't resolve yields its default ("" / false), never
|
|
138
|
+
* throws. Pure over the injected `io` (CLI passes a real execSync; tests a fake)
|
|
139
|
+
* and the `registry` (the loaded `.vigiles/providers/`, for `provider()` refs).
|
|
140
|
+
*/
|
|
141
|
+
function gatherContext(needs, io, registry = {}) {
|
|
142
|
+
const ctx = {};
|
|
143
|
+
for (const need of needs) {
|
|
144
|
+
if (isInline(need))
|
|
145
|
+
ctx[need.name] = tryExec(io, need.run);
|
|
146
|
+
else if (isRef(need)) {
|
|
147
|
+
const def = registry[need.name];
|
|
148
|
+
ctx[need.name] = def ? tryExec(io, def.run) : "";
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
ctx[need] = exports.BUILTIN_PROVIDERS[need].gather(io);
|
|
152
|
+
}
|
|
153
|
+
return ctx;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=hook-providers.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** A hook's declared side-effect posture. */
|
|
2
|
+
export type HookEffect = "observe" | "mutate";
|
|
3
|
+
/** A declared hook (the typed source the compiler reads). */
|
|
4
|
+
export interface HookSpec {
|
|
5
|
+
/** The harness event, e.g. "PreToolUse" | "PostToolUse". */
|
|
6
|
+
readonly event: string;
|
|
7
|
+
/** The tool name(s) the hook matches, e.g. ["Bash"] or ["Edit", "Write"]. */
|
|
8
|
+
readonly match: readonly string[];
|
|
9
|
+
/** The `tool_input` fields the hook extracts (must exist on the matched tools). */
|
|
10
|
+
readonly reads: readonly string[];
|
|
11
|
+
/** Observe-only (a gate/checker) or allowed to mutate (an action runner). */
|
|
12
|
+
readonly effect: HookEffect;
|
|
13
|
+
/** Optional command the hook runs — classified against {@link effect}. */
|
|
14
|
+
readonly run?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Build a hook spec (the untyped on-ramp; see `hookFor` for the typed one). */
|
|
17
|
+
export declare const hook: (spec: HookSpec) => HookSpec;
|
|
18
|
+
/** tool name → the `tool_input` fields it carries (injected; harness-specific). */
|
|
19
|
+
export type ToolFieldCatalog = Record<string, readonly string[]>;
|
|
20
|
+
export interface HookIssue {
|
|
21
|
+
readonly severity: "error" | "warning";
|
|
22
|
+
readonly message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ValidateHookOptions {
|
|
25
|
+
/** The per-tool `tool_input` field catalog for the active harness. */
|
|
26
|
+
readonly toolFields: ToolFieldCatalog;
|
|
27
|
+
/** The harness's known event names (optional — skips the event check if absent). */
|
|
28
|
+
readonly events?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate a hook spec — the compiler half. Flags wrong-field extraction (Check 1)
|
|
32
|
+
* and effect misdeclaration (Check 2), plus an unknown event when `events` is given.
|
|
33
|
+
* Pure; the same checks `compileHook` enforces before emitting.
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateHook(spec: HookSpec, opts: ValidateHookOptions): HookIssue[];
|
|
36
|
+
/** A settings `hooks` block keyed by the spec's event (the generated artifact). */
|
|
37
|
+
export interface CompiledHook {
|
|
38
|
+
readonly hooks: Record<string, readonly {
|
|
39
|
+
readonly matcher: string;
|
|
40
|
+
readonly hooks: readonly {
|
|
41
|
+
readonly type: "command";
|
|
42
|
+
readonly command: string;
|
|
43
|
+
}[];
|
|
44
|
+
}[]>;
|
|
45
|
+
/** field → the extraction expression a generated hook would use (the typed read). */
|
|
46
|
+
readonly extractions: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export declare class HookCompileError extends Error {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compile a validated hook to its settings block. Refuses (throws) on any error-level
|
|
52
|
+
* issue — "an unsafe hook doesn't compile" — so a wrong-field read or a mutating
|
|
53
|
+
* observe-hook never ships. The `extractions` map shows the correctly-typed field
|
|
54
|
+
* reads the generated hook uses (here as jq paths) — never a hand-typed `jq` string.
|
|
55
|
+
*/
|
|
56
|
+
export declare function compileHook(spec: HookSpec, opts: ValidateHookOptions & {
|
|
57
|
+
gateCommand?: string;
|
|
58
|
+
}): CompiledHook;
|
|
59
|
+
/**
|
|
60
|
+
* The typed source for a hook over a field map `M` (tool → field-union) and the
|
|
61
|
+
* matched tool(s) `T`. `reads` is constrained to the fields of `T` — a field absent
|
|
62
|
+
* from the matched tool is a tsc error. `M` stays unconstrained so a plain interface
|
|
63
|
+
* (no index signature) works as the field map.
|
|
64
|
+
*/
|
|
65
|
+
export interface TypedHookSpec<M, T extends keyof M & string> {
|
|
66
|
+
readonly event: string;
|
|
67
|
+
readonly match: readonly T[];
|
|
68
|
+
readonly reads: readonly Extract<M[T], string>[];
|
|
69
|
+
readonly effect: HookEffect;
|
|
70
|
+
readonly run?: string;
|
|
71
|
+
}
|
|
72
|
+
/** Build a typed hook; `reads` outside the matched tool's field-union won't compile. */
|
|
73
|
+
export declare function hookFor<M, T extends keyof M & string>(spec: TypedHookSpec<M, T>): HookSpec;
|
|
74
|
+
//# sourceMappingURL=hook-spec.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HookCompileError = exports.hook = void 0;
|
|
4
|
+
exports.validateHook = validateHook;
|
|
5
|
+
exports.compileHook = compileHook;
|
|
6
|
+
exports.hookFor = hookFor;
|
|
7
|
+
/**
|
|
8
|
+
* SPIKE — typed, effect-classified HOOKS (the "type-safe bash" angle).
|
|
9
|
+
*
|
|
10
|
+
* A Claude Code / Codex hook today is hand-written shell in settings.json. Two
|
|
11
|
+
* silent footguns dominate, and both are exactly what a COMPILER catches:
|
|
12
|
+
*
|
|
13
|
+
* 1. WRONG-FIELD EXTRACTION (silent no-op). A PreToolUse event's `tool_input`
|
|
14
|
+
* shape depends on the matched tool — `Bash` carries `command`, `Edit`/`Write`
|
|
15
|
+
* carry `file_path`. A hook that matches `Bash` but does
|
|
16
|
+
* `jq '.tool_input.file_path'` extracts EMPTY forever and never fires; nothing
|
|
17
|
+
* tells you. `validateHook` makes "read a field the matched tool never provides"
|
|
18
|
+
* an error, and the typed `hookFor` builder makes it a tsc error at edit time.
|
|
19
|
+
*
|
|
20
|
+
* 2. EFFECT MISDECLARATION ("type-safe bash"). A hook declared observe-only (a
|
|
21
|
+
* read-only gate/checker) that actually RUNS a mutating command — `eslint --fix`,
|
|
22
|
+
* `git push` — is a side effect masquerading as an observation. We already own a
|
|
23
|
+
* deterministic Bash-effect classifier (`bash-effects.ts`), so the command's
|
|
24
|
+
* effect class becomes a TYPE: an `effect: "observe"` hook whose `run` command
|
|
25
|
+
* classifies as side-effecting/undecidable does NOT compile. No other plugin
|
|
26
|
+
* tool has the classifier to make that judgment.
|
|
27
|
+
*
|
|
28
|
+
* Pure core, harness-agnostic: the per-tool field catalog is INJECTED (a Codex hook
|
|
29
|
+
* has different fields), and the Bash classifier is the harness-neutral one. The CC
|
|
30
|
+
* field catalog + the typed edit-time builder live in the test (no CC literal in core).
|
|
31
|
+
*
|
|
32
|
+
* NOT wired to the CLI/public API — a spike to see whether the compile step earns its
|
|
33
|
+
* keep on the hook surface. See research/harness-protocol-flow-moat.md.
|
|
34
|
+
*/
|
|
35
|
+
const bash_effects_js_1 = require("./bash-effects.js");
|
|
36
|
+
/** Build a hook spec (the untyped on-ramp; see `hookFor` for the typed one). */
|
|
37
|
+
const hook = (spec) => spec;
|
|
38
|
+
exports.hook = hook;
|
|
39
|
+
/** Which matched tools actually carry `field`. */
|
|
40
|
+
function toolsProviding(field, match, toolFields) {
|
|
41
|
+
return match.filter((t) => (toolFields[t] ?? []).includes(field));
|
|
42
|
+
}
|
|
43
|
+
/** Check 1 — every read field exists on the matched tool(s). */
|
|
44
|
+
function fieldIssues(spec, toolFields) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const field of spec.reads) {
|
|
47
|
+
const providers = toolsProviding(field, spec.match, toolFields);
|
|
48
|
+
if (providers.length === 0) {
|
|
49
|
+
out.push({
|
|
50
|
+
severity: "error",
|
|
51
|
+
message: `reads "${field}" but no matched tool (${spec.match.join("|")}) provides it — the extraction is always empty (silent no-op)`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (providers.length < spec.match.length) {
|
|
55
|
+
const missing = spec.match.filter((t) => !providers.includes(t));
|
|
56
|
+
out.push({
|
|
57
|
+
severity: "warning",
|
|
58
|
+
message: `reads "${field}", which ${missing.join("/")} does not carry — empty on those events`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Check 2 — an observe-only hook's command must be provably read-only. */
|
|
65
|
+
function effectIssues(spec) {
|
|
66
|
+
if (spec.effect !== "observe" || spec.run === undefined)
|
|
67
|
+
return [];
|
|
68
|
+
const cls = (0, bash_effects_js_1.classifyBashCommand)(spec.run);
|
|
69
|
+
if (cls === "read-only")
|
|
70
|
+
return [];
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: `declared observe-only but its command is ${cls}: "${spec.run}" — an observe hook must not mutate (use effect:"mutate" or a read-only command)`,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate a hook spec — the compiler half. Flags wrong-field extraction (Check 1)
|
|
80
|
+
* and effect misdeclaration (Check 2), plus an unknown event when `events` is given.
|
|
81
|
+
* Pure; the same checks `compileHook` enforces before emitting.
|
|
82
|
+
*/
|
|
83
|
+
function validateHook(spec, opts) {
|
|
84
|
+
const out = [];
|
|
85
|
+
if (opts.events && !opts.events.includes(spec.event)) {
|
|
86
|
+
out.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
message: `unknown event "${spec.event}" — it will never fire`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
out.push(...fieldIssues(spec, opts.toolFields));
|
|
92
|
+
out.push(...effectIssues(spec));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
class HookCompileError extends Error {
|
|
96
|
+
}
|
|
97
|
+
exports.HookCompileError = HookCompileError;
|
|
98
|
+
/**
|
|
99
|
+
* Compile a validated hook to its settings block. Refuses (throws) on any error-level
|
|
100
|
+
* issue — "an unsafe hook doesn't compile" — so a wrong-field read or a mutating
|
|
101
|
+
* observe-hook never ships. The `extractions` map shows the correctly-typed field
|
|
102
|
+
* reads the generated hook uses (here as jq paths) — never a hand-typed `jq` string.
|
|
103
|
+
*/
|
|
104
|
+
function compileHook(spec, opts) {
|
|
105
|
+
const issues = validateHook(spec, opts);
|
|
106
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
throw new HookCompileError(`hook does not compile:\n ${errors.map((e) => e.message).join("\n ")}`);
|
|
109
|
+
}
|
|
110
|
+
const command = opts.gateCommand ?? "npx vigiles hook-runtime guard";
|
|
111
|
+
const extractions = {};
|
|
112
|
+
for (const field of spec.reads)
|
|
113
|
+
extractions[field] = `.tool_input.${field}`;
|
|
114
|
+
return {
|
|
115
|
+
hooks: {
|
|
116
|
+
[spec.event]: [
|
|
117
|
+
{
|
|
118
|
+
matcher: spec.match.join("|"),
|
|
119
|
+
hooks: [{ type: "command", command }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
extractions,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Build a typed hook; `reads` outside the matched tool's field-union won't compile. */
|
|
127
|
+
function hookFor(spec) {
|
|
128
|
+
return spec;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=hook-spec.js.map
|
package/dist/core/inline.js
CHANGED
|
@@ -53,7 +53,7 @@ const KNOWN_NON_RULE_MARKERS = new Set([
|
|
|
53
53
|
"disable",
|
|
54
54
|
"ignore",
|
|
55
55
|
"ignore-file",
|
|
56
|
-
"gate", // skill step gate (src/skill-runtime.ts)
|
|
56
|
+
"gate", // skill step gate (src/adapters/claude-code/skill-runtime.ts)
|
|
57
57
|
"result", // skill result gate
|
|
58
58
|
"symbol", // symbol reference mark (src/refs.ts)
|
|
59
59
|
]);
|
package/dist/core/mcp-tool.d.ts
CHANGED
|
@@ -40,6 +40,18 @@ export interface McpToolIssue {
|
|
|
40
40
|
* A `Tool(restriction)` suffix is stripped first.
|
|
41
41
|
*/
|
|
42
42
|
export declare function mcpToolServer(raw: string, dialect: HarnessDialect): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Split a direct `mcp__<server>__<tool>` reference into its `{ server, tool }`
|
|
45
|
+
* parts, or null when {@link mcpToolServer} declines it (non-MCP, plugin-namespaced,
|
|
46
|
+
* malformed). The tool segment is everything after `mcp__<server>__` (MCP tool
|
|
47
|
+
* names may themselves contain `__`). A `Tool(restriction)` suffix is stripped.
|
|
48
|
+
* Used by the LIVE contract-tool resolution (`verifyMcpContractTools` in mcp.ts) to
|
|
49
|
+
* check the tool actually exists on the server, not just that the server resolves.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mcpToolParts(raw: string, dialect: HarnessDialect): {
|
|
52
|
+
server: string;
|
|
53
|
+
tool: string;
|
|
54
|
+
} | null;
|
|
43
55
|
/**
|
|
44
56
|
* Verify the MCP tool references in a contract against the plugin's declared MCP
|
|
45
57
|
* servers. Returns one {@link McpToolIssue} per direct `mcp__<server>__<tool>`
|
package/dist/core/mcp-tool.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.mcpToolServer = mcpToolServer;
|
|
4
|
+
exports.mcpToolParts = mcpToolParts;
|
|
4
5
|
exports.verifyMcpToolServers = verifyMcpToolServers;
|
|
5
6
|
/**
|
|
6
7
|
* The server segment of a direct `mcp__<server>__<tool>` reference, or null when
|
|
@@ -23,6 +24,25 @@ function mcpToolServer(raw, dialect) {
|
|
|
23
24
|
return null;
|
|
24
25
|
return server;
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Split a direct `mcp__<server>__<tool>` reference into its `{ server, tool }`
|
|
29
|
+
* parts, or null when {@link mcpToolServer} declines it (non-MCP, plugin-namespaced,
|
|
30
|
+
* malformed). The tool segment is everything after `mcp__<server>__` (MCP tool
|
|
31
|
+
* names may themselves contain `__`). A `Tool(restriction)` suffix is stripped.
|
|
32
|
+
* Used by the LIVE contract-tool resolution (`verifyMcpContractTools` in mcp.ts) to
|
|
33
|
+
* check the tool actually exists on the server, not just that the server resolves.
|
|
34
|
+
*/
|
|
35
|
+
function mcpToolParts(raw, dialect) {
|
|
36
|
+
const clean = raw.split("(")[0].trim();
|
|
37
|
+
const server = mcpToolServer(clean, dialect);
|
|
38
|
+
if (server === null)
|
|
39
|
+
return null;
|
|
40
|
+
const prefix = `mcp__${server}__`;
|
|
41
|
+
if (!clean.startsWith(prefix))
|
|
42
|
+
return null;
|
|
43
|
+
const tool = clean.slice(prefix.length);
|
|
44
|
+
return tool ? { server, tool } : null;
|
|
45
|
+
}
|
|
26
46
|
/**
|
|
27
47
|
* Verify the MCP tool references in a contract against the plugin's declared MCP
|
|
28
48
|
* servers. Returns one {@link McpToolIssue} per direct `mcp__<server>__<tool>`
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -43,6 +43,19 @@ export declare function loadMcpServers(cwd: string): Record<string, McpServerCon
|
|
|
43
43
|
* to an undeclared server, an unreachable server, or a missing tool is an error.
|
|
44
44
|
*/
|
|
45
45
|
export declare function verifyMcpRefs(markdown: string, mcpServers: Record<string, McpServerConfig>, timeoutMs?: number): Promise<McpRefError[]>;
|
|
46
|
+
export type McpContractToolReason = "server-unreachable" | "tool-missing";
|
|
47
|
+
export interface McpContractToolError {
|
|
48
|
+
/** The full `mcp__server__tool` reference (restriction suffix stripped). */
|
|
49
|
+
readonly tool: string;
|
|
50
|
+
readonly server: string;
|
|
51
|
+
/** The tool segment (what's looked up on the server). */
|
|
52
|
+
readonly toolName: string;
|
|
53
|
+
readonly reason: McpContractToolReason;
|
|
54
|
+
readonly suggestions: string[];
|
|
55
|
+
}
|
|
56
|
+
export declare function verifyMcpContractTools(tools: readonly string[], servers: Record<string, McpServerConfig>, dialect: import("./dialect.js").HarnessDialect, timeoutMs?: number): Promise<McpContractToolError[]>;
|
|
57
|
+
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
58
|
+
export declare function mcpContractToolMessage(e: McpContractToolError): string;
|
|
46
59
|
/** Human-readable message for an MCP reference error (with "did you mean"). */
|
|
47
60
|
export declare function mcpRefMessage(e: McpRefError): string;
|
|
48
61
|
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/core/mcp.js
CHANGED
|
@@ -5,6 +5,8 @@ exports.verifyMcpTool = verifyMcpTool;
|
|
|
5
5
|
exports.parseMcpRefs = parseMcpRefs;
|
|
6
6
|
exports.loadMcpServers = loadMcpServers;
|
|
7
7
|
exports.verifyMcpRefs = verifyMcpRefs;
|
|
8
|
+
exports.verifyMcpContractTools = verifyMcpContractTools;
|
|
9
|
+
exports.mcpContractToolMessage = mcpContractToolMessage;
|
|
8
10
|
exports.mcpRefMessage = mcpRefMessage;
|
|
9
11
|
/**
|
|
10
12
|
* Minimal MCP client over stdio — start a server, do the JSON-RPC handshake, and
|
|
@@ -21,6 +23,7 @@ const node_fs_1 = require("node:fs");
|
|
|
21
23
|
const node_path_1 = require("node:path");
|
|
22
24
|
const refs_js_1 = require("./refs.js");
|
|
23
25
|
const hash_js_1 = require("./hash.js");
|
|
26
|
+
const mcp_tool_js_1 = require("./mcp-tool.js");
|
|
24
27
|
function dispatch(line, pending) {
|
|
25
28
|
let msg;
|
|
26
29
|
try {
|
|
@@ -229,6 +232,70 @@ async function verifyMcpRefs(markdown, mcpServers, timeoutMs = 10000) {
|
|
|
229
232
|
}
|
|
230
233
|
return all;
|
|
231
234
|
}
|
|
235
|
+
/** Group the direct, declared-server `mcp__server__tool` refs by server (de-duped). */
|
|
236
|
+
function groupContractTools(tools, servers, dialect) {
|
|
237
|
+
const byServer = new Map();
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
for (const raw of tools) {
|
|
240
|
+
const parts = (0, mcp_tool_js_1.mcpToolParts)(raw, dialect);
|
|
241
|
+
if (!parts || !(parts.server in servers))
|
|
242
|
+
continue; // skip non-MCP/undeclared
|
|
243
|
+
const full = raw.split("(")[0].trim();
|
|
244
|
+
if (seen.has(full))
|
|
245
|
+
continue;
|
|
246
|
+
seen.add(full);
|
|
247
|
+
const arr = byServer.get(parts.server) ?? [];
|
|
248
|
+
arr.push({ full, toolName: parts.tool });
|
|
249
|
+
byServer.set(parts.server, arr);
|
|
250
|
+
}
|
|
251
|
+
return byServer;
|
|
252
|
+
}
|
|
253
|
+
/** Start one server and check its group of referenced tools against the live list. */
|
|
254
|
+
async function checkServerGroup(server, group, cfg, timeoutMs) {
|
|
255
|
+
let available;
|
|
256
|
+
try {
|
|
257
|
+
available = (await listMcpTools(cfg, timeoutMs)).map((t) => t.name);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return group.map((g) => ({
|
|
261
|
+
tool: g.full,
|
|
262
|
+
server,
|
|
263
|
+
toolName: g.toolName,
|
|
264
|
+
reason: "server-unreachable",
|
|
265
|
+
suggestions: [],
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
return group
|
|
269
|
+
.filter((g) => !available.includes(g.toolName))
|
|
270
|
+
.map((g) => ({
|
|
271
|
+
tool: g.full,
|
|
272
|
+
server,
|
|
273
|
+
toolName: g.toolName,
|
|
274
|
+
reason: "tool-missing",
|
|
275
|
+
suggestions: closest(g.toolName, available),
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
async function verifyMcpContractTools(tools, servers, dialect, timeoutMs = 10000) {
|
|
279
|
+
const byServer = groupContractTools(tools, servers, dialect);
|
|
280
|
+
const errors = [];
|
|
281
|
+
for (const [server, group] of byServer) {
|
|
282
|
+
errors.push(...(await checkServerGroup(server, group, servers[server], timeoutMs)));
|
|
283
|
+
}
|
|
284
|
+
return errors;
|
|
285
|
+
}
|
|
286
|
+
/** Human-readable message for a contract-tool error (with "did you mean"). */
|
|
287
|
+
function mcpContractToolMessage(e) {
|
|
288
|
+
switch (e.reason) {
|
|
289
|
+
case "server-unreachable":
|
|
290
|
+
return `MCP tool "${e.tool}" — server "${e.server}" failed to start`;
|
|
291
|
+
case "tool-missing":
|
|
292
|
+
return `MCP tool "${e.tool}" not found on server "${e.server}"${e.suggestions.length > 0
|
|
293
|
+
? ` — did you mean ${e.suggestions.map((s) => `"${s}"`).join(", ")}?`
|
|
294
|
+
: ""}`;
|
|
295
|
+
default:
|
|
296
|
+
return (0, hash_js_1.assertNever)(e.reason);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
232
299
|
/** Human-readable message for an MCP reference error (with "did you mean"). */
|
|
233
300
|
function mcpRefMessage(e) {
|
|
234
301
|
switch (e.reason) {
|