@rmyndharis/aimhooman 0.1.1 → 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/.agents/rules/aimhooman.md +5 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.clinerules/aimhooman.md +5 -3
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/aimhooman.mdc +5 -3
- package/.github/copilot-instructions.md +5 -3
- package/.kiro/steering/aimhooman.md +5 -3
- package/.windsurf/rules/aimhooman.md +5 -3
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +177 -1
- package/CONTRIBUTING.md +36 -3
- package/GEMINI.md +5 -3
- package/README.md +18 -8
- package/bin/aimhooman.mjs +163 -49
- package/docs/design/frictionless-enforcement.md +28 -0
- package/package.json +1 -1
- package/rules/attribution.json +8 -8
- package/rules/markers.json +31 -5
- package/rules/paths.json +60 -33
- package/rules/secrets.json +2 -1
- package/skills/aimhooman/SKILL.md +5 -3
- package/src/atomic-write.mjs +18 -1
- package/src/git-environment.mjs +10 -0
- package/src/githooks.mjs +113 -19
- package/src/gitx.mjs +65 -270
- package/src/history-scan.mjs +8 -1
- package/src/hook.mjs +424 -47
- package/src/scan-session.mjs +11 -4
- package/src/scan-target.mjs +29 -3
package/src/scan-session.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { gitEnvironment } from './git-environment.mjs';
|
|
2
|
+
import { gitEnvironment, GIT_TIMEOUT_MS } from './git-environment.mjs';
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_SCAN_LIMITS = Object.freeze({
|
|
5
5
|
maxFileBytes: 2 << 20,
|
|
@@ -67,9 +67,11 @@ export function scanEntries(repo, engine, entries, options = {}) {
|
|
|
67
67
|
increment(stats.skipped, 'binary');
|
|
68
68
|
// Binary classification only skips text-oriented policy rules. Secret
|
|
69
69
|
// signatures are ASCII byte sequences, so latin1 preserves a
|
|
70
|
-
// one-byte-to-one-code-unit view and
|
|
71
|
-
//
|
|
72
|
-
|
|
70
|
+
// one-byte-to-one-code-unit view and keeps the existing byte limits.
|
|
71
|
+
// Stripping NULs is what defeats hiding credential material behind
|
|
72
|
+
// them: one injected NUL breaks a signature, and a multi-byte
|
|
73
|
+
// encoding like UTF-16 injects one per character.
|
|
74
|
+
matched = engine.checkContent(entry.path, blob.toString('latin1').replace(/\0/g, ''), {
|
|
73
75
|
categories: ['secret'],
|
|
74
76
|
});
|
|
75
77
|
} else {
|
|
@@ -112,6 +114,11 @@ function readObjects(repo, objectIds, expectedBytes = 0) {
|
|
|
112
114
|
input: Buffer.from(unique.join('\n') + '\n'),
|
|
113
115
|
encoding: 'buffer',
|
|
114
116
|
maxBuffer: Math.max(2 * 1024 * 1024, expectedBytes + unique.length * 256 + 1024),
|
|
117
|
+
timeout: GIT_TIMEOUT_MS,
|
|
118
|
+
// Same reason as gitBuf in gitx.mjs: without an explicit stdio,
|
|
119
|
+
// execFileSync echoes the child's stderr before it checks the exit
|
|
120
|
+
// status, so git's raw output reaches the terminal even on success.
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
115
122
|
});
|
|
116
123
|
const objects = new Map();
|
|
117
124
|
const failures = [];
|
package/src/scan-target.mjs
CHANGED
|
@@ -11,7 +11,16 @@ import { commitChanges, commitMessage, commitSnapshot, historyRange } from './hi
|
|
|
11
11
|
import { DEFAULT_SCAN_LIMITS, scanEntries } from './scan-session.mjs';
|
|
12
12
|
import { loadRulesWithDiagnostics } from './rules.mjs';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// A tiebreak for the one profile a range report has to name, not a strength
|
|
15
|
+
// lattice. Clean and compliance are not ordered against each other: compliance
|
|
16
|
+
// allows the six attribution rules clean blocks or reviews, no shipped rule runs
|
|
17
|
+
// the other way, and applyExplicitProfile refuses to move between them in either
|
|
18
|
+
// direction. No single profile is truthful for a mixed range, so this only
|
|
19
|
+
// settles which policy object gets reported — clean outranks compliance because
|
|
20
|
+
// a value read as the strongest must not name the profile that allows what the
|
|
21
|
+
// other blocks. Enforcement never reads this: each commit is scanned under its
|
|
22
|
+
// own policy and the exit code uses each finding's own scanProfile.
|
|
23
|
+
const PROFILE_RANK = { compliance: 0, clean: 1, strict: 2 };
|
|
15
24
|
const REVIEW_REQUIRED_PATH_RULES = new Set([
|
|
16
25
|
'generic.agent-instructions',
|
|
17
26
|
'generic.project-policy',
|
|
@@ -49,6 +58,7 @@ export function scanMessage(repo, text, options = {}) {
|
|
|
49
58
|
const { policy, head } = resolved;
|
|
50
59
|
const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
|
|
51
60
|
const accumulator = createAccumulator(options.limits);
|
|
61
|
+
accumulator.addSkipped(loaded.skipped);
|
|
52
62
|
accumulator.add(loaded.engine.checkMessage(text).map((finding) => decorate(finding, null, policy)));
|
|
53
63
|
accumulator.addSkipped(loaded.engine.takeSkipped());
|
|
54
64
|
return {
|
|
@@ -68,6 +78,7 @@ function scanIndex(repo, options) {
|
|
|
68
78
|
const { rawPolicy, floor, policy, head, acknowledged } = resolveStagedPolicy(repo, options.explicitProfile);
|
|
69
79
|
const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
|
|
70
80
|
const accumulator = createAccumulator(options.limits);
|
|
81
|
+
accumulator.addSkipped(loaded.skipped);
|
|
71
82
|
let entries = kind === 'tracked' ? trackedEntries(repo) : stagedEntries(repo);
|
|
72
83
|
const conflicts = unmergedPaths(repo);
|
|
73
84
|
const diagnostics = [...loaded.diagnostics];
|
|
@@ -189,6 +200,7 @@ function scanCommit(repo, options) {
|
|
|
189
200
|
reviewContexts,
|
|
190
201
|
);
|
|
191
202
|
const accumulator = createAccumulator(options.limits);
|
|
203
|
+
accumulator.addSkipped(loaded.skipped);
|
|
192
204
|
const diagnostics = [...loaded.diagnostics];
|
|
193
205
|
|
|
194
206
|
if (snapshot.shallowBoundary) {
|
|
@@ -261,6 +273,9 @@ function scanRange(repo, options) {
|
|
|
261
273
|
// only constructs an Engine and applies overrides (see engineForPolicy).
|
|
262
274
|
const preloaded = loadRulesWithDiagnostics(repo.stateDir);
|
|
263
275
|
const preloadedOverrides = loadOverrides(repo.stateDir);
|
|
276
|
+
// Counted once for the range, not once per commit: the same packs back every
|
|
277
|
+
// commit's engine, so the per-commit loaded.skipped would inflate the tally.
|
|
278
|
+
accumulator.addSkipped(packSkipped(preloaded.errors));
|
|
264
279
|
|
|
265
280
|
for (const commit of history.commits) {
|
|
266
281
|
const changes = commitChanges(repo, commit.commit, commit.commit, commit.parents);
|
|
@@ -452,7 +467,14 @@ export function engineForPolicy(
|
|
|
452
467
|
[...reviewedInstructions, ...reviewedPolicies],
|
|
453
468
|
);
|
|
454
469
|
const diagnostics = loaded.errors.map((error) => ({ level: 'warning', message: `${error.message}; pack skipped` }));
|
|
455
|
-
return { engine: loaded.engine, diagnostics };
|
|
470
|
+
return { engine: loaded.engine, diagnostics, skipped: packSkipped(loaded.errors) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// packSkipped turns rule-pack load failures into a counted skip reason. A pack
|
|
474
|
+
// that never loaded is a hole in coverage, not an empty result: strict throws
|
|
475
|
+
// above, and clean/compliance proceed but must not report a complete scan.
|
|
476
|
+
function packSkipped(errors) {
|
|
477
|
+
return errors.length ? { 'local-pack-error': errors.length } : {};
|
|
456
478
|
}
|
|
457
479
|
|
|
458
480
|
function reviewedEngineEntry(entry, ruleId, transition) {
|
|
@@ -589,6 +611,10 @@ function rangeReportPolicy(basePolicy, policies, usedStrictFloor, explicitProfil
|
|
|
589
611
|
};
|
|
590
612
|
}
|
|
591
613
|
|
|
614
|
+
// Skip reasons that mean a rule never ran, as opposed to a rule running and
|
|
615
|
+
// matching nothing. Either way the scan cannot claim to have covered its input.
|
|
616
|
+
const INCOMPLETE_SKIP_REASONS = new Set(['local-input-limit', 'local-pack-error']);
|
|
617
|
+
|
|
592
618
|
function createAccumulator(limits = {}) {
|
|
593
619
|
const effectiveLimits = { ...DEFAULT_SCAN_LIMITS, ...limits };
|
|
594
620
|
const findings = [];
|
|
@@ -644,7 +670,7 @@ function createAccumulator(limits = {}) {
|
|
|
644
670
|
addSkipped(skipped = {}) {
|
|
645
671
|
for (const [reason, count] of Object.entries(skipped)) {
|
|
646
672
|
stats.skipped[reason] = (stats.skipped[reason] || 0) + count;
|
|
647
|
-
if (reason
|
|
673
|
+
if (INCOMPLETE_SKIP_REASONS.has(reason)) complete = false;
|
|
648
674
|
}
|
|
649
675
|
},
|
|
650
676
|
markIncomplete() {
|