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.
@@ -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
+ }