crewhaus 0.1.3
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 +84 -0
- package/package.json +117 -0
- package/src/doctor-checks.test.ts +122 -0
- package/src/doctor-checks.ts +220 -0
- package/src/egress-matcher.test.ts +273 -0
- package/src/egress-matcher.ts +112 -0
- package/src/eval.test.ts +130 -0
- package/src/index.test.ts +699 -0
- package/src/index.ts +2513 -0
- package/src/justification-gate.test.ts +292 -0
- package/src/justification-gate.ts +124 -0
- package/src/scope-audit.test.ts +207 -0
- package/src/scope-audit.ts +89 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type ScopeFinding, auditToolScopes, isOutwardName } from "@crewhaus/tool-builder";
|
|
2
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FR-002 — Pillar 3 sink-side build-time gate. The canonical per-tool
|
|
6
|
+
* `auditToolScopes` (and its `ScopeFinding` type) now live in
|
|
7
|
+
* `@crewhaus/tool-builder`, next to `isOutwardName` and `buildTool` — the two
|
|
8
|
+
* facts the gate keys on — so every consumer (this CLI's `compile --strict`
|
|
9
|
+
* and `doctor --philosophy-alignment`, plus the `compile()` library and the
|
|
10
|
+
* compiler-worker) audits identically rather than re-deriving the rule. They
|
|
11
|
+
* are re-exported here so the CLI's existing import sites and tests are
|
|
12
|
+
* unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export { type ScopeFinding, auditToolScopes };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FR-002 — the `compile --strict` audit over the *spec-level* tool names a
|
|
18
|
+
* lowered IR references (the IR only carries names, never RegisteredTools).
|
|
19
|
+
*
|
|
20
|
+
* For each name we ask `resolve` (the offline built-in tool map) for the
|
|
21
|
+
* concrete RegisteredTool:
|
|
22
|
+
*
|
|
23
|
+
* - Resolved → run the same per-tool `auditToolScopes` check (capability or
|
|
24
|
+
* outward-name vs scope). This is how the six built-ins are verified.
|
|
25
|
+
* - Unresolved AND outward-by-name (`mcp__*` or a known outward built-in
|
|
26
|
+
* name not in the offline map) → FINDING. The name is statically known to
|
|
27
|
+
* be an external sink (the FR lists MCP calls among the definitionally-
|
|
28
|
+
* outward kinds, and `buildTool` infers `external` for `mcp__*`), but the
|
|
29
|
+
* compiler cannot prove offline that its scope is `"external"`. `--strict`
|
|
30
|
+
* refuses to emit a bundle that reaches an outward sink whose external
|
|
31
|
+
* scope it cannot verify — this is the criterion's "I/O-capable tool left
|
|
32
|
+
* at an unspecified scope" from the compiler's offline vantage. Without
|
|
33
|
+
* this, a spec referencing `mcp__evil__exfiltrate` slipped through.
|
|
34
|
+
* - Unresolved and NOT outward-by-name → skipped. A name the offline map
|
|
35
|
+
* doesn't know and whose name carries no outward signal is either a
|
|
36
|
+
* pure-compute custom tool registered in code or a typo; the offline gate
|
|
37
|
+
* has no capability fact to assert (the live `doctor --philosophy-
|
|
38
|
+
* alignment` audit, which sees real RegisteredTools, covers the registry).
|
|
39
|
+
*
|
|
40
|
+
* `resolve` is injected so this is a pure function unit-testable without the
|
|
41
|
+
* CLI importing the (heavy, side-effectful) built-in tool packages.
|
|
42
|
+
*/
|
|
43
|
+
export function auditSpecToolNames(
|
|
44
|
+
names: readonly string[],
|
|
45
|
+
resolve: (name: string) => RegisteredTool | undefined,
|
|
46
|
+
): ScopeFinding[] {
|
|
47
|
+
const findings: ScopeFinding[] = [];
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const tool = resolve(name);
|
|
50
|
+
if (tool) {
|
|
51
|
+
findings.push(...auditToolScopes([tool]));
|
|
52
|
+
} else if (isOutwardName(name)) {
|
|
53
|
+
findings.push({
|
|
54
|
+
toolName: name,
|
|
55
|
+
reason:
|
|
56
|
+
'is an outward-reaching sink by name but could not be resolved to a scope:"external" tool at compile time (dynamic/MCP sinks must be vetted, not assumed) — its egress scope is unverifiable offline',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return findings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* FR-002 — collect every tool NAME referenced anywhere in a lowered IR,
|
|
65
|
+
* variant-agnostically. The IR is a JSON-serializable discriminated union
|
|
66
|
+
* (14 variants); some carry tools at the top level (`IrV0.tools`), others
|
|
67
|
+
* nest them under steps / nodes / stages / sub-agents. Rather than couple to
|
|
68
|
+
* each variant's shape, walk the serialized object and gather every string
|
|
69
|
+
* under a `tools` key. Deterministic and dedup'd.
|
|
70
|
+
*/
|
|
71
|
+
export function collectToolNames(ir: unknown): string[] {
|
|
72
|
+
const names = new Set<string>();
|
|
73
|
+
const visit = (node: unknown): void => {
|
|
74
|
+
if (Array.isArray(node)) {
|
|
75
|
+
for (const item of node) visit(item);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (node !== null && typeof node === "object") {
|
|
79
|
+
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
|
80
|
+
if (key === "tools" && Array.isArray(value)) {
|
|
81
|
+
for (const v of value) if (typeof v === "string") names.add(v);
|
|
82
|
+
}
|
|
83
|
+
visit(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
visit(ir);
|
|
88
|
+
return [...names];
|
|
89
|
+
}
|