crewhaus 0.1.8 → 0.2.1
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 +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +173 -0
- package/dist/dataset-mine.js +404 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +9 -2
- package/dist/feedback.js +17 -7
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11664 -963
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/route.d.ts +19 -0
- package/dist/route.js +90 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +174 -0
- package/dist/security-digest.js +656 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +300 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +90 -65
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 49 — scope-audit drift watch: the persistence + baseline-diff +
|
|
3
|
+
* boundary-drift core behind `crewhaus doctor --philosophy-alignment
|
|
4
|
+
* --json/--baseline/--accept-baseline`, factored out of the entry file
|
|
5
|
+
* `index.ts` (which runs a top-level argv switch and so cannot be imported
|
|
6
|
+
* by a test without executing the CLI). Side-effect-free on import and
|
|
7
|
+
* directly unit-testable, mirroring `scope-audit.ts` / `security-digest.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The problem this closes: `doctor --philosophy-alignment` audited boundary
|
|
10
|
+
* call sites and tool scopes but was run-and-forget print-only — a
|
|
11
|
+
* regression (or a NEW un-classified ingress) looked identical to
|
|
12
|
+
* known-accepted findings, so nobody could gate CI on it. Three pieces:
|
|
13
|
+
*
|
|
14
|
+
* 1. Stable finding identity. Every audit finding now carries a
|
|
15
|
+
* (class, file, symbol) triple; `findingId` hashes it so a finding
|
|
16
|
+
* keeps its id across runs even when the human-facing label or reason
|
|
17
|
+
* wording changes. Labels/reasons are deliberately NOT hashed. Renaming
|
|
18
|
+
* or moving a file (or renaming a symbol) mints a NEW id by design —
|
|
19
|
+
* the gate fails closed on the "new" finding and the baseline must be
|
|
20
|
+
* re-accepted with --accept-baseline.
|
|
21
|
+
*
|
|
22
|
+
* 2. Baselined diffs, following `@crewhaus/regression-runner`'s `gate()`
|
|
23
|
+
* shape (verdict + reason + report). `diffScopeAuditSnapshots` splits
|
|
24
|
+
* current findings into new / accepted / resolved against
|
|
25
|
+
* `.crewhaus/scope-audit/baseline.json`; the verdict fails ONLY on new
|
|
26
|
+
* findings — legacy accepted findings never block, so the gate can be
|
|
27
|
+
* adopted on a repo with known warts.
|
|
28
|
+
*
|
|
29
|
+
* 3. Boundary-site drift detection. The audit's canonical boundary list
|
|
30
|
+
* is hardcoded (six sites in index.ts) and checks only that KNOWN
|
|
31
|
+
* sites still call `classifyBoundary`; nothing caught a NEW
|
|
32
|
+
* cross-trust ingress that skipped classification. `detectBoundaryDrift`
|
|
33
|
+
* extends the same mechanism (read package sources, substring-match)
|
|
34
|
+
* with a small set of conservative ingress signals derived from the
|
|
35
|
+
* boundary-classifier TrustOrigin taxonomy, and flags packages that
|
|
36
|
+
* match a signal but never reference the classification fabric.
|
|
37
|
+
* Findings are REPORT-ONLY (warn, pass: true) in plain mode; only the
|
|
38
|
+
* --baseline gate fails on them, and only when they are NEW.
|
|
39
|
+
*/
|
|
40
|
+
/** Finding classes the philosophy audit emits. `boundary-drift` is the new
|
|
41
|
+
* detector; the rest map 1:1 onto the pre-existing printed checks. */
|
|
42
|
+
export type PhilosophyFindingClass = "pillar-doc" | "package-presence" | "boundary-site" | "tool-scope" | "contributor-doc" | "boundary-drift";
|
|
43
|
+
export type PhilosophyFinding = {
|
|
44
|
+
readonly class: PhilosophyFindingClass;
|
|
45
|
+
/** Repo-relative path the finding is about ("" for repo-level checks). */
|
|
46
|
+
readonly file: string;
|
|
47
|
+
/** Stable symbol within (class, file) — a site/tool/signal name. */
|
|
48
|
+
readonly symbol: string;
|
|
49
|
+
readonly label: string;
|
|
50
|
+
readonly pass: boolean;
|
|
51
|
+
/** warn+pass renders as "~" — informational, never fails plain doctor. */
|
|
52
|
+
readonly warn?: boolean;
|
|
53
|
+
readonly reason?: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Stable finding id: sha256 over the (class, file, symbol) identity triple,
|
|
57
|
+
* truncated to 12 hex chars (48 bits — collision-safe at this cardinality
|
|
58
|
+
* and short enough to read in a diff). Labels and reasons are excluded on
|
|
59
|
+
* purpose: rewording a message must not orphan a baselined finding.
|
|
60
|
+
*
|
|
61
|
+
* The NUL separator makes the triple unambiguous (no field can contain it).
|
|
62
|
+
* It MUST stay the `\u0000` ESCAPE SEQUENCE in this source: a literal NUL
|
|
63
|
+
* byte makes git treat the whole module as a binary blob (unreviewable
|
|
64
|
+
* diffs) and invites silent id-invalidation by NUL-stripping tools. The
|
|
65
|
+
* escape and the literal produce identical runtime bytes, so every existing
|
|
66
|
+
* findingId and accepted baseline stays valid — scope-audit-drift.test.ts
|
|
67
|
+
* pins both the source byte-hygiene and a known id value.
|
|
68
|
+
*/
|
|
69
|
+
export declare function findingId(f: Pick<PhilosophyFinding, "class" | "file" | "symbol">): string;
|
|
70
|
+
/** A finding as persisted in a snapshot/baseline file. */
|
|
71
|
+
export type PersistedFinding = PhilosophyFinding & {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
};
|
|
74
|
+
export type ScopeAuditSnapshot = {
|
|
75
|
+
readonly version: 1;
|
|
76
|
+
readonly generatedAt: string;
|
|
77
|
+
/** Actionable findings only — see `isActionableFinding`. */
|
|
78
|
+
readonly findings: ReadonlyArray<PersistedFinding>;
|
|
79
|
+
};
|
|
80
|
+
export declare const SCOPE_AUDIT_RELPATH = ".crewhaus/scope-audit";
|
|
81
|
+
export declare const SCOPE_AUDIT_BASELINE_FILENAME = "baseline.json";
|
|
82
|
+
export declare function scopeAuditDir(rootDir: string): string;
|
|
83
|
+
export declare function scopeAuditBaselinePath(rootDir: string): string;
|
|
84
|
+
/** Dated snapshot path: `.crewhaus/scope-audit/<YYYY-MM-DD>.json` (UTC).
|
|
85
|
+
* Re-running on the same day overwrites — the snapshot is a deterministic
|
|
86
|
+
* function of the tree, so the latest run of the day is the record. */
|
|
87
|
+
export declare function scopeAuditSnapshotPath(rootDir: string, now: () => number): string;
|
|
88
|
+
/**
|
|
89
|
+
* What gets persisted (and therefore baselined/gated): hard failures, plus
|
|
90
|
+
* boundary-drift reports (which are warn-tier in plain mode but exactly the
|
|
91
|
+
* findings the drift watch exists to gate). Green checks and benign
|
|
92
|
+
* environment warns (e.g. "sibling ../docs not cloned") are excluded — they
|
|
93
|
+
* are not findings, and baselining them would make an absent docs checkout
|
|
94
|
+
* flip the gate.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isActionableFinding(f: PhilosophyFinding): boolean;
|
|
97
|
+
export declare function buildScopeAuditSnapshot(findings: ReadonlyArray<PhilosophyFinding>, now?: () => number): ScopeAuditSnapshot;
|
|
98
|
+
/** Thrown on a malformed baseline/snapshot file. The CLI routes it through
|
|
99
|
+
* `die()` — a gate whose accepted-set is unreadable must fail loudly, not
|
|
100
|
+
* silently treat everything (or nothing) as new. */
|
|
101
|
+
export declare class ScopeAuditBaselineError extends Error {
|
|
102
|
+
readonly name = "ScopeAuditBaselineError";
|
|
103
|
+
}
|
|
104
|
+
/** Read + validate a snapshot file; undefined when it does not exist. */
|
|
105
|
+
export declare function loadScopeAuditSnapshot(path: string): ScopeAuditSnapshot | undefined;
|
|
106
|
+
/** Gate verdict, following regression-runner's `gate()` shape
|
|
107
|
+
* (verdict / reason / report arrays). */
|
|
108
|
+
export type ScopeAuditGateVerdict = {
|
|
109
|
+
readonly verdict: "pass" | "fail";
|
|
110
|
+
readonly reason?: string;
|
|
111
|
+
/** In current but not the baseline — the ONLY thing that fails the gate. */
|
|
112
|
+
readonly newFindings: ReadonlyArray<PersistedFinding>;
|
|
113
|
+
/** In both — legacy accepted findings; never block. */
|
|
114
|
+
readonly acceptedFindings: ReadonlyArray<PersistedFinding>;
|
|
115
|
+
/** In the baseline but no longer current — fixed since acceptance. */
|
|
116
|
+
readonly resolvedFindings: ReadonlyArray<PersistedFinding>;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Diff the current snapshot against the last accepted baseline. Identity is
|
|
120
|
+
* the stable finding id, so wording changes don't churn the diff. A missing
|
|
121
|
+
* baseline (`undefined`) fails IF there is anything to accept — a gate that
|
|
122
|
+
* silently passed forever because nobody ever ran --accept-baseline would
|
|
123
|
+
* fabricate assurance — and passes on a genuinely clean tree.
|
|
124
|
+
*/
|
|
125
|
+
export declare function diffScopeAuditSnapshots(baseline: ScopeAuditSnapshot | undefined, current: ScopeAuditSnapshot): ScopeAuditGateVerdict;
|
|
126
|
+
/**
|
|
127
|
+
* Scan `<repoRoot>/packages/<name>/src` for cross-trust ingress signals and
|
|
128
|
+
* flag packages that match one but never reference the classification
|
|
129
|
+
* fabric ({@link CLASSIFICATION_MARKERS}). Report-only by design: findings
|
|
130
|
+
* come back `pass: true, warn: true` so plain `doctor --philosophy-alignment`
|
|
131
|
+
* keeps exiting 0 on them; the --baseline gate is what fails, and only on
|
|
132
|
+
* findings that are NEW relative to the accepted baseline.
|
|
133
|
+
*
|
|
134
|
+
* Returns [] when `<repoRoot>/packages` does not exist (the audit is
|
|
135
|
+
* factory-repo-shaped, like the six-site check it extends).
|
|
136
|
+
*/
|
|
137
|
+
export declare function detectBoundaryDrift(repoRoot: string): PhilosophyFinding[];
|
|
138
|
+
/** Render the gate verdict as the CLI's summary lines (baseline mode). */
|
|
139
|
+
export declare function renderGateReport(diff: ScopeAuditGateVerdict): ReadonlyArray<string>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Stable finding id: sha256 over the (class, file, symbol) identity triple,
|
|
6
|
+
* truncated to 12 hex chars (48 bits — collision-safe at this cardinality
|
|
7
|
+
* and short enough to read in a diff). Labels and reasons are excluded on
|
|
8
|
+
* purpose: rewording a message must not orphan a baselined finding.
|
|
9
|
+
*
|
|
10
|
+
* The NUL separator makes the triple unambiguous (no field can contain it).
|
|
11
|
+
* It MUST stay the `\u0000` ESCAPE SEQUENCE in this source: a literal NUL
|
|
12
|
+
* byte makes git treat the whole module as a binary blob (unreviewable
|
|
13
|
+
* diffs) and invites silent id-invalidation by NUL-stripping tools. The
|
|
14
|
+
* escape and the literal produce identical runtime bytes, so every existing
|
|
15
|
+
* findingId and accepted baseline stays valid — scope-audit-drift.test.ts
|
|
16
|
+
* pins both the source byte-hygiene and a known id value.
|
|
17
|
+
*/
|
|
18
|
+
export function findingId(f) {
|
|
19
|
+
return createHash("sha256")
|
|
20
|
+
.update(`${f.class}\u0000${f.file}\u0000${f.symbol}`)
|
|
21
|
+
.digest("hex")
|
|
22
|
+
.slice(0, 12);
|
|
23
|
+
}
|
|
24
|
+
export const SCOPE_AUDIT_RELPATH = ".crewhaus/scope-audit";
|
|
25
|
+
export const SCOPE_AUDIT_BASELINE_FILENAME = "baseline.json";
|
|
26
|
+
export function scopeAuditDir(rootDir) {
|
|
27
|
+
return join(rootDir, SCOPE_AUDIT_RELPATH);
|
|
28
|
+
}
|
|
29
|
+
export function scopeAuditBaselinePath(rootDir) {
|
|
30
|
+
return join(scopeAuditDir(rootDir), SCOPE_AUDIT_BASELINE_FILENAME);
|
|
31
|
+
}
|
|
32
|
+
/** Dated snapshot path: `.crewhaus/scope-audit/<YYYY-MM-DD>.json` (UTC).
|
|
33
|
+
* Re-running on the same day overwrites — the snapshot is a deterministic
|
|
34
|
+
* function of the tree, so the latest run of the day is the record. */
|
|
35
|
+
export function scopeAuditSnapshotPath(rootDir, now) {
|
|
36
|
+
return join(scopeAuditDir(rootDir), `${new Date(now()).toISOString().slice(0, 10)}.json`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What gets persisted (and therefore baselined/gated): hard failures, plus
|
|
40
|
+
* boundary-drift reports (which are warn-tier in plain mode but exactly the
|
|
41
|
+
* findings the drift watch exists to gate). Green checks and benign
|
|
42
|
+
* environment warns (e.g. "sibling ../docs not cloned") are excluded — they
|
|
43
|
+
* are not findings, and baselining them would make an absent docs checkout
|
|
44
|
+
* flip the gate.
|
|
45
|
+
*/
|
|
46
|
+
export function isActionableFinding(f) {
|
|
47
|
+
return !f.pass || f.class === "boundary-drift";
|
|
48
|
+
}
|
|
49
|
+
export function buildScopeAuditSnapshot(findings, now = () => Date.now()) {
|
|
50
|
+
const actionable = findings
|
|
51
|
+
.filter(isActionableFinding)
|
|
52
|
+
.map((f) => ({ ...f, id: findingId(f) }))
|
|
53
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
54
|
+
return { version: 1, generatedAt: new Date(now()).toISOString(), findings: actionable };
|
|
55
|
+
}
|
|
56
|
+
/** Thrown on a malformed baseline/snapshot file. The CLI routes it through
|
|
57
|
+
* `die()` — a gate whose accepted-set is unreadable must fail loudly, not
|
|
58
|
+
* silently treat everything (or nothing) as new. */
|
|
59
|
+
export class ScopeAuditBaselineError extends Error {
|
|
60
|
+
name = "ScopeAuditBaselineError";
|
|
61
|
+
}
|
|
62
|
+
/** Read + validate a snapshot file; undefined when it does not exist. */
|
|
63
|
+
export function loadScopeAuditSnapshot(path) {
|
|
64
|
+
if (!existsSync(path))
|
|
65
|
+
return undefined;
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
throw new ScopeAuditBaselineError(`${path} is not valid JSON (${err.message}) — re-accept with --accept-baseline`);
|
|
72
|
+
}
|
|
73
|
+
const snap = parsed;
|
|
74
|
+
if (snap.version !== 1 || !Array.isArray(snap.findings)) {
|
|
75
|
+
throw new ScopeAuditBaselineError(`${path} is not a v1 scope-audit snapshot — re-accept with --accept-baseline`);
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Diff the current snapshot against the last accepted baseline. Identity is
|
|
81
|
+
* the stable finding id, so wording changes don't churn the diff. A missing
|
|
82
|
+
* baseline (`undefined`) fails IF there is anything to accept — a gate that
|
|
83
|
+
* silently passed forever because nobody ever ran --accept-baseline would
|
|
84
|
+
* fabricate assurance — and passes on a genuinely clean tree.
|
|
85
|
+
*/
|
|
86
|
+
export function diffScopeAuditSnapshots(baseline, current) {
|
|
87
|
+
if (baseline === undefined) {
|
|
88
|
+
if (current.findings.length === 0) {
|
|
89
|
+
return { verdict: "pass", newFindings: [], acceptedFindings: [], resolvedFindings: [] };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
verdict: "fail",
|
|
93
|
+
reason: `no accepted baseline and ${current.findings.length} finding(s) present — review them and run \`crewhaus doctor --philosophy-alignment --accept-baseline\``,
|
|
94
|
+
newFindings: [...current.findings],
|
|
95
|
+
acceptedFindings: [],
|
|
96
|
+
resolvedFindings: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const baselineById = new Map(baseline.findings.map((f) => [f.id, f]));
|
|
100
|
+
const currentIds = new Set(current.findings.map((f) => f.id));
|
|
101
|
+
const newFindings = current.findings.filter((f) => !baselineById.has(f.id));
|
|
102
|
+
const acceptedFindings = current.findings.filter((f) => baselineById.has(f.id));
|
|
103
|
+
const resolvedFindings = baseline.findings.filter((f) => !currentIds.has(f.id));
|
|
104
|
+
if (newFindings.length > 0) {
|
|
105
|
+
return {
|
|
106
|
+
verdict: "fail",
|
|
107
|
+
reason: `${newFindings.length} NEW finding(s) not in the accepted baseline (${newFindings
|
|
108
|
+
.map((f) => f.id)
|
|
109
|
+
.join(", ")})`,
|
|
110
|
+
newFindings,
|
|
111
|
+
acceptedFindings,
|
|
112
|
+
resolvedFindings,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { verdict: "pass", newFindings, acceptedFindings, resolvedFindings };
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Boundary-site drift detection
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Markers that a package participates in the classification fabric. Any of
|
|
122
|
+
* these appearing in a package's sources exempts it from drift findings:
|
|
123
|
+
* - classifyBoundary / boundary-classifier — the chokepoint itself;
|
|
124
|
+
* - classifyInbound — channel-adapter-base's wrapper around it;
|
|
125
|
+
* - chain-adapter-base — whose shared ingress helper classifies centrally,
|
|
126
|
+
* so concrete chain adapters routing through it are covered.
|
|
127
|
+
* Same substring mechanism as the existing six-site check in index.ts.
|
|
128
|
+
*/
|
|
129
|
+
const CLASSIFICATION_MARKERS = [
|
|
130
|
+
"classifyBoundary",
|
|
131
|
+
"boundary-classifier",
|
|
132
|
+
"classifyInbound",
|
|
133
|
+
"chain-adapter-base",
|
|
134
|
+
];
|
|
135
|
+
/**
|
|
136
|
+
* Conservative cross-trust ingress signals, derived from the
|
|
137
|
+
* boundary-classifier TrustOrigin taxonomy. Deliberately small: each signal
|
|
138
|
+
* is a strong indicator that server/peer/user-controlled content enters the
|
|
139
|
+
* process. (Weak signals — e.g. the literal "SKILL.md", which appears in
|
|
140
|
+
* emitters and doc comments across the tree — are excluded; the baseline
|
|
141
|
+
* mechanism handles honest false positives, but the detector should not
|
|
142
|
+
* manufacture them.)
|
|
143
|
+
*/
|
|
144
|
+
const DRIFT_SIGNALS = [
|
|
145
|
+
{
|
|
146
|
+
symbol: "mcp-sdk-ingress",
|
|
147
|
+
origin: "mcp",
|
|
148
|
+
description: "imports the raw MCP SDK (server-controlled payloads enter here)",
|
|
149
|
+
contentNeedle: "@modelcontextprotocol/",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
symbol: "federation-peer-ingress",
|
|
153
|
+
origin: "federation",
|
|
154
|
+
description: "consumes federation peer payloads (mTLS authenticates WHO, not WHAT)",
|
|
155
|
+
contentNeedle: "@crewhaus/federation-protocol",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
symbol: "channel-inbound-ingress",
|
|
159
|
+
origin: "channel",
|
|
160
|
+
description: "normalizes inbound channel webhooks/messages",
|
|
161
|
+
packagePattern: /^channel-adapter-(?!base$).+/,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
symbol: "chain-content-ingress",
|
|
165
|
+
origin: "chain",
|
|
166
|
+
description: "decodes chain RPC/event content",
|
|
167
|
+
packagePattern: /^chain-adapter-(?!base$).+/,
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
/** Concatenate a package's non-test .ts sources (recursively under src/). */
|
|
171
|
+
function readPackageSources(srcDir) {
|
|
172
|
+
const chunks = [];
|
|
173
|
+
const walk = (dir) => {
|
|
174
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
175
|
+
const full = join(dir, entry);
|
|
176
|
+
let isDir = false;
|
|
177
|
+
try {
|
|
178
|
+
isDir = statSync(full).isDirectory();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
continue; // raced away / unreadable — skip, never crash the audit
|
|
182
|
+
}
|
|
183
|
+
if (isDir) {
|
|
184
|
+
walk(full);
|
|
185
|
+
}
|
|
186
|
+
else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".d.ts")) {
|
|
187
|
+
try {
|
|
188
|
+
chunks.push(readFileSync(full, "utf8"));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Unreadable source — skip; the audit reports on what it can see.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
walk(srcDir);
|
|
197
|
+
return chunks.join("\n");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Scan `<repoRoot>/packages/<name>/src` for cross-trust ingress signals and
|
|
201
|
+
* flag packages that match one but never reference the classification
|
|
202
|
+
* fabric ({@link CLASSIFICATION_MARKERS}). Report-only by design: findings
|
|
203
|
+
* come back `pass: true, warn: true` so plain `doctor --philosophy-alignment`
|
|
204
|
+
* keeps exiting 0 on them; the --baseline gate is what fails, and only on
|
|
205
|
+
* findings that are NEW relative to the accepted baseline.
|
|
206
|
+
*
|
|
207
|
+
* Returns [] when `<repoRoot>/packages` does not exist (the audit is
|
|
208
|
+
* factory-repo-shaped, like the six-site check it extends).
|
|
209
|
+
*/
|
|
210
|
+
export function detectBoundaryDrift(repoRoot) {
|
|
211
|
+
const packagesDir = join(repoRoot, "packages");
|
|
212
|
+
if (!existsSync(packagesDir))
|
|
213
|
+
return [];
|
|
214
|
+
const findings = [];
|
|
215
|
+
for (const pkgName of readdirSync(packagesDir).sort()) {
|
|
216
|
+
const srcDir = join(packagesDir, pkgName, "src");
|
|
217
|
+
if (!existsSync(srcDir))
|
|
218
|
+
continue;
|
|
219
|
+
const source = readPackageSources(srcDir);
|
|
220
|
+
if (source === "")
|
|
221
|
+
continue;
|
|
222
|
+
const classified = CLASSIFICATION_MARKERS.some((m) => source.includes(m));
|
|
223
|
+
if (classified)
|
|
224
|
+
continue;
|
|
225
|
+
for (const signal of DRIFT_SIGNALS) {
|
|
226
|
+
const matches = signal.contentNeedle !== undefined
|
|
227
|
+
? source.includes(signal.contentNeedle)
|
|
228
|
+
: signal.packagePattern.test(pkgName);
|
|
229
|
+
if (!matches)
|
|
230
|
+
continue;
|
|
231
|
+
const file = `packages/${pkgName}/src`;
|
|
232
|
+
findings.push({
|
|
233
|
+
class: "boundary-drift",
|
|
234
|
+
file,
|
|
235
|
+
symbol: signal.symbol,
|
|
236
|
+
label: `Pillar 3 drift — ${pkgName} ${signal.description} without classifyBoundary`,
|
|
237
|
+
pass: true,
|
|
238
|
+
warn: true,
|
|
239
|
+
reason: `${file} matches the "${signal.symbol}" cross-trust ingress signal (origin: ${signal.origin}) but never references the boundary-classification fabric — a new ingress that skips classifyBoundary is a security regression (AGENTS.md Pillar 3). If this ingress is classified downstream by design, accept it into the baseline with --accept-baseline.`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return findings;
|
|
244
|
+
}
|
|
245
|
+
/** Render the gate verdict as the CLI's summary lines (baseline mode). */
|
|
246
|
+
export function renderGateReport(diff) {
|
|
247
|
+
const lines = [];
|
|
248
|
+
for (const f of diff.newFindings) {
|
|
249
|
+
lines.push(`✗ NEW [${f.id}] ${f.label}${f.reason !== undefined ? ` — ${f.reason}` : ""}`);
|
|
250
|
+
}
|
|
251
|
+
for (const f of diff.acceptedFindings) {
|
|
252
|
+
lines.push(`• accepted [${f.id}] ${f.label}`);
|
|
253
|
+
}
|
|
254
|
+
for (const f of diff.resolvedFindings) {
|
|
255
|
+
lines.push(`✓ resolved [${f.id}] ${f.label} — no longer present; re-accept to shrink the baseline`);
|
|
256
|
+
}
|
|
257
|
+
lines.push(`baseline gate: ${diff.verdict}${diff.reason !== undefined ? ` — ${diff.reason}` : ""} ` +
|
|
258
|
+
`(${diff.newFindings.length} new, ${diff.acceptedFindings.length} accepted, ${diff.resolvedFindings.length} resolved)`);
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { type PromptInjectionResult } from "@crewhaus/prompt-injection-detector";
|
|
2
|
+
/** Every real regex-rule id in the current detector. A parsed notice id must
|
|
3
|
+
* be a member of this set to be admitted (drops forged/renamed ids). */
|
|
4
|
+
export declare const RULE_IDS: ReadonlySet<string>;
|
|
5
|
+
export type SinceWindow = {
|
|
6
|
+
readonly sinceMs: number;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
};
|
|
9
|
+
/** `<N>d` trailing window (default all-time when undefined) — a small local
|
|
10
|
+
* parser so the module has no dependency on security-digest's copy. */
|
|
11
|
+
export declare function parseCorpusSince(value: string | undefined, now?: () => number): SinceWindow;
|
|
12
|
+
export type ObservedRuleHit = {
|
|
13
|
+
readonly rule: string;
|
|
14
|
+
/** How many redaction notices in the window named this rule. */
|
|
15
|
+
readonly count: number;
|
|
16
|
+
};
|
|
17
|
+
export type HarvestResult = {
|
|
18
|
+
/** Session event logs scanned. */
|
|
19
|
+
readonly sessionsScanned: number;
|
|
20
|
+
/** Redaction notices (malicious verdicts) found in the window. */
|
|
21
|
+
readonly redactions: number;
|
|
22
|
+
/** Per-rule observed block counts, ranked by count then id. */
|
|
23
|
+
readonly ruleHits: ReadonlyArray<ObservedRuleHit>;
|
|
24
|
+
/**
|
|
25
|
+
* Ids parsed out of notices that are NOT current rules (forged, or a rule
|
|
26
|
+
* that was renamed/removed since the notice was written). Reported so a
|
|
27
|
+
* removed-rule regression is visible; never turned into a corpus case.
|
|
28
|
+
*/
|
|
29
|
+
readonly unknownIds: ReadonlyArray<string>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Scan `<rootDir>/.crewhaus/sessions` for prompt-injection redaction notices
|
|
33
|
+
* and tally which detector rules fired. A missing dir yields an empty harvest
|
|
34
|
+
* (no throw — the command reports emptiness, it is not a gate on presence).
|
|
35
|
+
*/
|
|
36
|
+
export declare function harvestBlockedAttempts(opts: {
|
|
37
|
+
readonly rootDir: string;
|
|
38
|
+
readonly window: SinceWindow;
|
|
39
|
+
}): HarvestResult;
|
|
40
|
+
/**
|
|
41
|
+
* A small, deterministic exemplar STRING that a given rule matches, built at
|
|
42
|
+
* runtime from parts so no attack literal is stored in this source (and so a
|
|
43
|
+
* secret-shaped token in the exemplar is assembled, not committed — GitHub
|
|
44
|
+
* push-protection). Each entry is a minimal phrase the corresponding
|
|
45
|
+
* REGEX_RULES pattern hits; the mapping is asserted by the tests, so a rule
|
|
46
|
+
* whose exemplar stops matching (a pattern edit) is caught immediately.
|
|
47
|
+
*
|
|
48
|
+
* Rules without a hand-written exemplar fall back to `undefined`; the corpus
|
|
49
|
+
* still records the observed rule but the check treats it as
|
|
50
|
+
* "exemplar-unavailable" rather than a regression (a missing exemplar is a
|
|
51
|
+
* corpus gap, not a detector failure).
|
|
52
|
+
*/
|
|
53
|
+
export declare function exemplarForRule(ruleId: string): string | undefined;
|
|
54
|
+
export type CorpusCase = {
|
|
55
|
+
/** Detector rule this case guards against regressing. */
|
|
56
|
+
readonly rule: string;
|
|
57
|
+
/** Observed production block count that motivated the case. */
|
|
58
|
+
readonly observed: number;
|
|
59
|
+
/** Deterministic positive fixture the detector must still block. Assembled
|
|
60
|
+
* at runtime; never a harvested attack payload. */
|
|
61
|
+
readonly exemplar: string;
|
|
62
|
+
/**
|
|
63
|
+
* The exemplar's classification tier AT BUILD TIME (F2). `check` compares
|
|
64
|
+
* the CURRENT classification against this recorded baseline rather than a
|
|
65
|
+
* fixed "not clean" floor — a rule that used to earn `malicious` and now
|
|
66
|
+
* only reaches `suspicious` is real protection lost (only `malicious`
|
|
67
|
+
* redacts at runtime; `suspicious` content is passed through verbatim), so
|
|
68
|
+
* that drift must fail even though `suspicious !== clean`. A case built
|
|
69
|
+
* BEFORE this field existed (no `baselineTier` in the loaded JSON) falls
|
|
70
|
+
* back to the pre-F2 "not clean" floor — see `checkSecurityCorpus`.
|
|
71
|
+
*/
|
|
72
|
+
readonly baselineTier: PromptInjectionResult["classification"];
|
|
73
|
+
};
|
|
74
|
+
export type SecurityCorpus = {
|
|
75
|
+
readonly version: 1;
|
|
76
|
+
readonly generatedAt: string;
|
|
77
|
+
readonly windowLabel: string;
|
|
78
|
+
/** Rules with a canonical exemplar that were observed blocking. */
|
|
79
|
+
readonly cases: ReadonlyArray<CorpusCase>;
|
|
80
|
+
/** Rules observed blocking but with no exemplar yet (corpus gaps). */
|
|
81
|
+
readonly withoutExemplar: ReadonlyArray<ObservedRuleHit>;
|
|
82
|
+
/** Notice ids that are not current rules (removed/renamed/forged). */
|
|
83
|
+
readonly unknownIds: ReadonlyArray<string>;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Build the regression corpus from a harvest. One case per observed rule that
|
|
87
|
+
* has a canonical exemplar; observed-but-exemplar-less rules are kept
|
|
88
|
+
* separately as gaps. Deterministic given (harvest, generatedAt) — `async`
|
|
89
|
+
* because each case's `baselineTier` (F2) is the exemplar's CURRENT
|
|
90
|
+
* classification, recorded once at build time so `check` can detect a later
|
|
91
|
+
* downward drift rather than only an absolute drop to `clean`.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildSecurityCorpus(harvest: HarvestResult, windowLabel: string, generatedAt?: string): Promise<SecurityCorpus>;
|
|
94
|
+
export declare function corpusDir(rootDir: string): string;
|
|
95
|
+
export declare function corpusPath(rootDir: string): string;
|
|
96
|
+
export declare function candidateRulesPath(rootDir: string): string;
|
|
97
|
+
/** Thrown on a malformed corpus file; the CLI routes it through `die()`. */
|
|
98
|
+
export declare class SecurityCorpusError extends Error {
|
|
99
|
+
readonly name = "SecurityCorpusError";
|
|
100
|
+
}
|
|
101
|
+
export declare function loadSecurityCorpus(path: string): SecurityCorpus | undefined;
|
|
102
|
+
export type CorpusCheckCaseResult = {
|
|
103
|
+
readonly rule: string;
|
|
104
|
+
/** Current detector classification of the exemplar. */
|
|
105
|
+
readonly classification: PromptInjectionResult["classification"];
|
|
106
|
+
/** The tier this case was pinned at when the corpus was built (F2). Cases
|
|
107
|
+
* loaded from a pre-F2 corpus file have no recorded baseline; see
|
|
108
|
+
* `checkSecurityCorpus` for the fallback in that case. */
|
|
109
|
+
readonly baselineTier: PromptInjectionResult["classification"] | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Passed = the CURRENT tier has not dropped below the RECORDED baseline
|
|
112
|
+
* tier (F2: drift, not an absolute `=== malicious` floor).
|
|
113
|
+
*
|
|
114
|
+
* At runtime only `malicious` redacts a tool output; `suspicious` content
|
|
115
|
+
* is passed through to the model verbatim (only logged/warned). So a rule
|
|
116
|
+
* that used to earn `malicious` and has drifted down to `suspicious` is
|
|
117
|
+
* real protection lost, even though the exemplar is still "not clean" —
|
|
118
|
+
* `stillBlocked` catches that drift where a bare `!== "clean"` check would
|
|
119
|
+
* pass it. A rule whose baseline was only ever `suspicious` (a single
|
|
120
|
+
* medium-severity rule, or a multi-rule payload that only reaches
|
|
121
|
+
* `suspicious` on its own — the detector's probabilistic-OR score makes the
|
|
122
|
+
* exact tier of a canonical single-rule exemplar an implementation detail)
|
|
123
|
+
* still passes as long as it holds at `suspicious`; it only fails if it
|
|
124
|
+
* drops all the way to `clean`. Pre-F2 corpus cases with no recorded
|
|
125
|
+
* baseline fall back to that same "not clean" floor.
|
|
126
|
+
*/
|
|
127
|
+
readonly stillBlocked: boolean;
|
|
128
|
+
};
|
|
129
|
+
export type CorpusCheckResult = {
|
|
130
|
+
readonly verdict: "pass" | "fail";
|
|
131
|
+
readonly checked: number;
|
|
132
|
+
readonly regressions: ReadonlyArray<CorpusCheckCaseResult>;
|
|
133
|
+
readonly holding: ReadonlyArray<CorpusCheckCaseResult>;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Run every corpus case against the CURRENT detector and fail if any case's
|
|
137
|
+
* classification has DRIFTED DOWN from its recorded `baselineTier` (F2) — a
|
|
138
|
+
* detector regression, whether that drop is malicious→suspicious (real
|
|
139
|
+
* protection lost: only `malicious` redacts at runtime) or a drop all the way
|
|
140
|
+
* to `clean` (the rule stopped catching the attack entirely). A case with no
|
|
141
|
+
* recorded baseline (loaded from a pre-F2 corpus file) falls back to the
|
|
142
|
+
* original "not clean" floor. Exit-code-usable: the CLI maps
|
|
143
|
+
* `verdict === "fail"` to a non-zero exit. Uses `classifyText` with
|
|
144
|
+
* production defaults (no LLM layer), so the check is deterministic and
|
|
145
|
+
* offline.
|
|
146
|
+
*/
|
|
147
|
+
export declare function checkSecurityCorpus(corpus: SecurityCorpus): Promise<CorpusCheckResult>;
|
|
148
|
+
/**
|
|
149
|
+
* A tool_result the detector scored `suspicious` (not blocked) but which
|
|
150
|
+
* carries the shape of an attack — a NEAR-MISS. The runtime keeps suspicious
|
|
151
|
+
* content verbatim (it only warns), so unlike malicious redactions the
|
|
152
|
+
* suspicious payloads ARE durable in the session log. We cannot know which
|
|
153
|
+
* were truly hostile, so these feed CANDIDATE rules for HUMAN review — never
|
|
154
|
+
* an auto-merge into REGEX_RULES.
|
|
155
|
+
*/
|
|
156
|
+
export type NearMiss = {
|
|
157
|
+
/** The (sanitized, redacted) suspicious content snippet. */
|
|
158
|
+
readonly snippet: string;
|
|
159
|
+
/** Structural signal that made it a near-miss (see NEAR_MISS_SIGNALS). */
|
|
160
|
+
readonly signal: string;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Deterministic structural signals for near-miss clustering. Each is a strong
|
|
164
|
+
* "looks like an instruction-injection attempt" shape that the current
|
|
165
|
+
* REGEX_RULES do NOT already hard-block. Kept conservative — a candidate rule
|
|
166
|
+
* a human ignores is cheap; a false candidate that gets merged is not (which
|
|
167
|
+
* is why merge is never automatic).
|
|
168
|
+
*/
|
|
169
|
+
export declare const NEAR_MISS_SIGNALS: ReadonlyArray<{
|
|
170
|
+
signal: string;
|
|
171
|
+
test: RegExp;
|
|
172
|
+
}>;
|
|
173
|
+
/**
|
|
174
|
+
* Redact anything PII/secret-shaped from a near-miss snippet, run through the
|
|
175
|
+
* SAME strong detector set the dataset/knowledge synthesis paths use
|
|
176
|
+
* (`SYNTHESIZE_PII_DETECTORS`, plus JWT/PEM — see `CANDIDATE_SECRET_DETECTORS`)
|
|
177
|
+
* rather than the narrow ad hoc email/blob/digit patterns this used to apply.
|
|
178
|
+
* This is used ONLY for in-memory clustering (dedup/support counting); the
|
|
179
|
+
* PERSISTED `candidate-rules.json` never stores this snippet at all — see
|
|
180
|
+
* `hashDescriptor` and F1. Deterministic, no raw values retained.
|
|
181
|
+
*/
|
|
182
|
+
export declare function redactSnippet(text: string): string;
|
|
183
|
+
/** First 16 hex chars of SHA-256(redacted snippet) — a stable, non-reversible
|
|
184
|
+
* descriptor for the reviewer's dedup/audit trail. Deliberately NOT an HMAC
|
|
185
|
+
* (no per-tenant secret is plumbed into this module; this hash is a display
|
|
186
|
+
* aid, not an allow-list key like `pii-redactor`'s `hmacValue`), and hashed
|
|
187
|
+
* AFTER redaction so it identifies a "shape" (redacted form), never a raw
|
|
188
|
+
* secret/PII value. */
|
|
189
|
+
export declare function hashDescriptor(redacted: string): string;
|
|
190
|
+
export type CandidateRule = {
|
|
191
|
+
/** Proposed stable rule id (namespaced `candidate-<signal>`). */
|
|
192
|
+
readonly id: string;
|
|
193
|
+
readonly signal: string;
|
|
194
|
+
/** How many distinct near-miss snippets clustered under this signal. */
|
|
195
|
+
readonly support: number;
|
|
196
|
+
/**
|
|
197
|
+
* Up to a few short, fully-redacted descriptors of clustered snippets, for
|
|
198
|
+
* the reviewer's context — NEVER a raw or lightly-redacted sample. Each
|
|
199
|
+
* entry is `hash:<16-hex>` `[<kinds redacted>]` `<short prefix>` where the
|
|
200
|
+
* prefix has already passed through `redactSnippet`'s strong secret+PII
|
|
201
|
+
* detector set and is clamped to `SAMPLE_PREFIX_LENGTH` chars. See F1.
|
|
202
|
+
*/
|
|
203
|
+
readonly samples: ReadonlyArray<string>;
|
|
204
|
+
/** A conservative proposed regex SOURCE (string, never compiled/merged). */
|
|
205
|
+
readonly proposedPattern: string;
|
|
206
|
+
readonly note: string;
|
|
207
|
+
};
|
|
208
|
+
/** Build the persisted, hash-first descriptor for one clustered snippet. The
|
|
209
|
+
* snippet has ALREADY passed through `redactSnippet`'s strong detector set;
|
|
210
|
+
* this additionally truncates hard to `SAMPLE_PREFIX_LENGTH` so no full-length
|
|
211
|
+
* secret — redacted or not — round-trips into `candidate-rules.json`. */
|
|
212
|
+
export declare function describeSample(redactedSnippet: string): string;
|
|
213
|
+
export type CandidateRulesFile = {
|
|
214
|
+
readonly version: 1;
|
|
215
|
+
readonly generatedAt: string;
|
|
216
|
+
/** Minimum cluster support required to emit a candidate. */
|
|
217
|
+
readonly minSupport: number;
|
|
218
|
+
readonly candidates: ReadonlyArray<CandidateRule>;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Harvest suspicious (near-miss) tool outputs from durable session logs. These
|
|
222
|
+
* are `tool_result` contents that are NOT redaction notices but match a
|
|
223
|
+
* NEAR_MISS_SIGNAL. Content is sanitized + redacted at parse time.
|
|
224
|
+
*/
|
|
225
|
+
export declare function harvestNearMisses(opts: {
|
|
226
|
+
readonly rootDir: string;
|
|
227
|
+
readonly window: SinceWindow;
|
|
228
|
+
}): NearMiss[];
|
|
229
|
+
/**
|
|
230
|
+
* Cluster near-misses by structural signal and emit CANDIDATE detector rules
|
|
231
|
+
* for any signal with >= minSupport DISTINCT snippets. Deterministic: the same
|
|
232
|
+
* near-misses always yield the same candidates. Output is a reviewed file the
|
|
233
|
+
* operator reads — it is NEVER merged into REGEX_RULES automatically.
|
|
234
|
+
*/
|
|
235
|
+
export declare function clusterCandidateRules(nearMisses: ReadonlyArray<NearMiss>, minSupport?: number, generatedAt?: string): CandidateRulesFile;
|
|
236
|
+
export declare function renderCorpusBuildLines(corpus: SecurityCorpus): ReadonlyArray<string>;
|
|
237
|
+
export declare function renderCorpusCheckLines(result: CorpusCheckResult): ReadonlyArray<string>;
|