@rmyndharis/aimhooman 0.1.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/.agents/rules/aimhooman.md +53 -0
- package/.claude-plugin/marketplace.json +23 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.clinerules/aimhooman.md +53 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/aimhooman.mdc +59 -0
- package/.gemini/settings.json +7 -0
- package/.github/copilot-instructions.md +53 -0
- package/.github/hooks/aimhooman.json +22 -0
- package/.kiro/steering/aimhooman.md +53 -0
- package/.windsurf/rules/aimhooman.md +57 -0
- package/AGENTS.md +53 -0
- package/CHANGELOG.md +153 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +144 -0
- package/GEMINI.md +53 -0
- package/LICENSE +21 -0
- package/README.md +472 -0
- package/SECURITY.md +74 -0
- package/bin/aimhooman.mjs +1589 -0
- package/docs/design/agent-portability.md +73 -0
- package/docs/design/frictionless-enforcement.md +135 -0
- package/docs/hosts.json +164 -0
- package/docs/logo/aimhooman-logo.png +0 -0
- package/docs/logo/aimhooman.png +0 -0
- package/hooks/hooks.json +29 -0
- package/package.json +77 -0
- package/rules/attribution.json +142 -0
- package/rules/markers.json +88 -0
- package/rules/paths.json +407 -0
- package/rules/secrets.json +90 -0
- package/schemas/overrides.schema.json +134 -0
- package/schemas/project-policy.schema.json +12 -0
- package/schemas/rule-pack.schema.json +126 -0
- package/schemas/scan-report.schema.json +165 -0
- package/skills/aimhooman/SKILL.md +65 -0
- package/src/args.mjs +72 -0
- package/src/atomic-write.mjs +285 -0
- package/src/codex-manifest.mjs +65 -0
- package/src/exclude.mjs +125 -0
- package/src/git-environment.mjs +5 -0
- package/src/git-path.mjs +5 -0
- package/src/githooks.mjs +813 -0
- package/src/gitx.mjs +721 -0
- package/src/history-scan.mjs +295 -0
- package/src/hook.mjs +2602 -0
- package/src/policy-resolver.mjs +200 -0
- package/src/report.mjs +63 -0
- package/src/rules.mjs +509 -0
- package/src/ruleset-text.mjs +29 -0
- package/src/scan-session.mjs +152 -0
- package/src/scan-target.mjs +697 -0
- package/src/scan.mjs +325 -0
- package/src/state.mjs +360 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { GitRevisionError } from './gitx.mjs';
|
|
5
|
+
import { gitEnvironment } from './git-environment.mjs';
|
|
6
|
+
|
|
7
|
+
export const EMPTY_HISTORY_OID = '0'.repeat(40);
|
|
8
|
+
|
|
9
|
+
function gitBuffer(repo, args, input) {
|
|
10
|
+
return execFileSync('git', args, {
|
|
11
|
+
cwd: repo.root,
|
|
12
|
+
env: gitEnvironment(),
|
|
13
|
+
encoding: 'buffer',
|
|
14
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
15
|
+
...(input === undefined ? {} : { input }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function gitString(repo, args) {
|
|
20
|
+
return gitBuffer(repo, args).toString('utf8').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gitStringQuiet(repo, args) {
|
|
24
|
+
return execFileSync('git', args, {
|
|
25
|
+
cwd: repo.root,
|
|
26
|
+
env: gitEnvironment(),
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
}).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertRevision(value, label) {
|
|
34
|
+
if (typeof value !== 'string' || !value || value.startsWith('-') || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
35
|
+
throw new TypeError(`${label} must be a non-empty Git revision`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseHistoryRange(value) {
|
|
40
|
+
assertRevision(value, 'range');
|
|
41
|
+
const match = /^(.+?)(\.\.\.?)(.+)$/.exec(value);
|
|
42
|
+
if (!match || match[1].includes('..') || match[3].includes('..')) {
|
|
43
|
+
throw new TypeError('range must contain both endpoints exactly once, for example base..head');
|
|
44
|
+
}
|
|
45
|
+
const [, base, operator, head] = match;
|
|
46
|
+
assertRevision(base, 'range base');
|
|
47
|
+
assertRevision(head, 'range head');
|
|
48
|
+
return { base, head, operator };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveCommit(repo, revision) {
|
|
52
|
+
assertRevision(revision, 'commit');
|
|
53
|
+
let commit;
|
|
54
|
+
try {
|
|
55
|
+
commit = gitStringQuiet(repo, [
|
|
56
|
+
'rev-parse', '--verify', '--quiet', '--end-of-options', `${revision}^{commit}`,
|
|
57
|
+
]);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.status === 1) {
|
|
60
|
+
throw new GitRevisionError(revision, 'does not resolve to a commit', error);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(commit)) {
|
|
65
|
+
throw new GitRevisionError(revision, 'Git returned an invalid commit object ID');
|
|
66
|
+
}
|
|
67
|
+
return commit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function commitMessage(repo, revision, resolvedCommit) {
|
|
71
|
+
const commit = resolvedCommit || resolveCommit(repo, revision);
|
|
72
|
+
const object = gitBuffer(repo, ['cat-file', 'commit', commit]);
|
|
73
|
+
const separator = object.indexOf(Buffer.from('\n\n'));
|
|
74
|
+
return {
|
|
75
|
+
commit,
|
|
76
|
+
message: separator < 0 ? '' : object.subarray(separator + 2).toString('utf8'),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function commitParents(repo, revision) {
|
|
81
|
+
const commit = resolveCommit(repo, revision);
|
|
82
|
+
const line = gitString(repo, ['rev-list', '--parents', '-n', '1', commit]);
|
|
83
|
+
const [, ...parents] = line.split(' ');
|
|
84
|
+
return { commit, parents, shallowBoundary: isShallowBoundary(repo, commit) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function commitChanges(repo, revision, resolvedCommit, resolvedParents) {
|
|
88
|
+
// `resolvedParents || []` conflated "omitted" with "root commit": a merge
|
|
89
|
+
// passed via the 3-arg form would diff against nothing and silently scan as
|
|
90
|
+
// empty. Re-derive parents when the caller passed a commit but no parents.
|
|
91
|
+
const { commit, parents } = resolvedCommit
|
|
92
|
+
? { commit: resolvedCommit, parents: resolvedParents ?? commitParents(repo, revision).parents }
|
|
93
|
+
: commitParents(repo, revision);
|
|
94
|
+
const comparisons = parents.length ? parents : [null];
|
|
95
|
+
const grouped = new Map();
|
|
96
|
+
|
|
97
|
+
for (const parent of comparisons) {
|
|
98
|
+
const args = [
|
|
99
|
+
'diff-tree', '--no-commit-id', '--raw', '--no-abbrev', '-r', '-z',
|
|
100
|
+
'--find-renames', '--diff-filter=ACMRTD',
|
|
101
|
+
];
|
|
102
|
+
if (!parent) args.push('--root', commit);
|
|
103
|
+
else args.push(parent, commit);
|
|
104
|
+
args.push('--');
|
|
105
|
+
for (const entry of parseRawDiff(gitBuffer(repo, args))) {
|
|
106
|
+
const key = [entry.path, entry.sourcePath || '', entry.oid || '', entry.mode || '', entry.status].join('\0');
|
|
107
|
+
const current = grouped.get(key);
|
|
108
|
+
if (current) {
|
|
109
|
+
if (parent && !current.parents.includes(parent)) current.parents.push(parent);
|
|
110
|
+
} else {
|
|
111
|
+
grouped.set(key, {
|
|
112
|
+
...entry,
|
|
113
|
+
commit,
|
|
114
|
+
parents: parent ? [parent] : [],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const entries = [...grouped.values()].sort(compareEntries);
|
|
121
|
+
const sizes = objectSizes(repo, entries.filter((entry) => entry.type === 'blob').map((entry) => entry.oid));
|
|
122
|
+
for (const entry of entries) entry.size = entry.type === 'blob' ? sizes.get(entry.oid) ?? null : null;
|
|
123
|
+
return {
|
|
124
|
+
commit,
|
|
125
|
+
parents,
|
|
126
|
+
entries,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function commitSnapshot(repo, revision) {
|
|
131
|
+
const { commit, parents, shallowBoundary } = commitParents(repo, revision);
|
|
132
|
+
const records = gitBuffer(repo, ['ls-tree', '-r', '-z', '--full-tree', commit, '--'])
|
|
133
|
+
.toString('utf8').split('\0').filter(Boolean);
|
|
134
|
+
const entries = records.map((record) => {
|
|
135
|
+
const tab = record.indexOf('\t');
|
|
136
|
+
const match = /^(\d+) (\w+) ([0-9a-f]+)$/.exec(record.slice(0, tab));
|
|
137
|
+
if (tab < 0 || !match) throw new Error('unexpected output from git ls-tree');
|
|
138
|
+
return {
|
|
139
|
+
mode: match[1],
|
|
140
|
+
type: match[2],
|
|
141
|
+
oid: match[3],
|
|
142
|
+
path: record.slice(tab + 1),
|
|
143
|
+
status: 'S',
|
|
144
|
+
commit,
|
|
145
|
+
parents,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const sizes = objectSizes(repo, entries.filter((entry) => entry.type === 'blob').map((entry) => entry.oid));
|
|
149
|
+
for (const entry of entries) entry.size = entry.type === 'blob' ? sizes.get(entry.oid) ?? null : null;
|
|
150
|
+
const { message } = commitMessage(repo, commit, commit);
|
|
151
|
+
const changes = commitChanges(repo, commit, commit, parents).entries;
|
|
152
|
+
return { commit, parents, shallowBoundary, message, entries, changes };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function historyRange(repo, value) {
|
|
156
|
+
const parsed = parseHistoryRange(value);
|
|
157
|
+
const bootstrap = isZeroObjectId(parsed.base);
|
|
158
|
+
const baseCommit = bootstrap ? parsed.base : resolveCommit(repo, parsed.base);
|
|
159
|
+
const headCommit = resolveCommit(repo, parsed.head);
|
|
160
|
+
let scanBase = baseCommit;
|
|
161
|
+
if (!bootstrap && parsed.operator === '...') {
|
|
162
|
+
try {
|
|
163
|
+
scanBase = gitStringQuiet(repo, ['merge-base', baseCommit, headCommit]);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error?.status === 1) {
|
|
166
|
+
throw new GitRevisionError(value, 'range endpoints do not share a merge base', error);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(scanBase)) {
|
|
171
|
+
throw new GitRevisionError(value, 'Git returned an invalid merge base');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const revision = bootstrap ? headCommit : `${scanBase}..${headCommit}`;
|
|
175
|
+
const lines = gitString(repo, ['rev-list', '--reverse', '--topo-order', '--parents', revision]);
|
|
176
|
+
// rev-list --parents emits "<commit> <parent1> <parent2> ..." per line, so
|
|
177
|
+
// each commit's parents arrive with the walk at no extra cost. Commit bodies
|
|
178
|
+
// (changes, message) are fetched lazily by the scanner per commit rather
|
|
179
|
+
// than held in memory for the whole range up front.
|
|
180
|
+
const commits = lines ? lines.split('\n').map((line) => {
|
|
181
|
+
const [commit, ...parents] = line.split(' ');
|
|
182
|
+
return { commit, parents };
|
|
183
|
+
}) : [];
|
|
184
|
+
// A shallow clone (e.g. CI fetch-depth: 1) may not contain every commit in
|
|
185
|
+
// the range, so completeness cannot be proven. Callers warn or fail closed.
|
|
186
|
+
const shallow = gitStringQuiet(repo, ['rev-parse', '--is-shallow-repository']) === 'true';
|
|
187
|
+
const reversed = !bootstrap
|
|
188
|
+
&& baseCommit !== headCommit
|
|
189
|
+
&& commits.length === 0
|
|
190
|
+
&& isAncestor(repo, headCommit, scanBase);
|
|
191
|
+
return {
|
|
192
|
+
input: value,
|
|
193
|
+
operator: parsed.operator,
|
|
194
|
+
base: baseCommit,
|
|
195
|
+
scanBase,
|
|
196
|
+
head: headCommit,
|
|
197
|
+
bootstrap,
|
|
198
|
+
shallow,
|
|
199
|
+
reversed,
|
|
200
|
+
commits,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isShallowBoundary(repo, commit) {
|
|
205
|
+
if (gitStringQuiet(repo, ['rev-parse', '--is-shallow-repository']) !== 'true') return false;
|
|
206
|
+
let contents;
|
|
207
|
+
try {
|
|
208
|
+
contents = readFileSync(join(repo.commonDir, 'shallow'), 'utf8');
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error?.code === 'ENOENT') {
|
|
211
|
+
throw new Error('Git reports a shallow repository but its shallow boundary file is missing', {
|
|
212
|
+
cause: error,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
return contents.split(/\r?\n/).includes(commit);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isAncestor(repo, ancestor, descendant) {
|
|
221
|
+
try {
|
|
222
|
+
execFileSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
223
|
+
cwd: repo.root,
|
|
224
|
+
env: gitEnvironment(),
|
|
225
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
226
|
+
});
|
|
227
|
+
return true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error?.status === 1) return false;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isZeroObjectId(value) {
|
|
235
|
+
return /^(?:0{40}|0{64})$/.test(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseRawDiff(buffer) {
|
|
239
|
+
const fields = buffer.toString('utf8').split('\0').filter(Boolean);
|
|
240
|
+
const entries = [];
|
|
241
|
+
for (let index = 0; index < fields.length;) {
|
|
242
|
+
const header = fields[index++];
|
|
243
|
+
const match = /^:(\d+) (\d+) ([0-9a-f]+) ([0-9a-f]+) ([ACMRTD])(\d*)$/.exec(header);
|
|
244
|
+
if (!match || index >= fields.length) throw new Error('unexpected output from git diff-tree');
|
|
245
|
+
const sourcePath = fields[index++];
|
|
246
|
+
let path = sourcePath;
|
|
247
|
+
if (match[5] === 'R' || match[5] === 'C') {
|
|
248
|
+
if (index >= fields.length) throw new Error('unexpected rename output from git diff-tree');
|
|
249
|
+
path = fields[index++];
|
|
250
|
+
}
|
|
251
|
+
const deleted = match[5] === 'D';
|
|
252
|
+
entries.push({
|
|
253
|
+
path,
|
|
254
|
+
...(path === sourcePath ? {} : { sourcePath }),
|
|
255
|
+
oid: deleted ? null : match[4],
|
|
256
|
+
mode: deleted ? null : match[2],
|
|
257
|
+
type: deleted ? 'deleted' : typeForMode(match[2]),
|
|
258
|
+
status: match[5],
|
|
259
|
+
size: null,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return entries;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function objectSizes(repo, objectIds) {
|
|
266
|
+
const unique = [...new Set(objectIds)];
|
|
267
|
+
if (!unique.length) return new Map();
|
|
268
|
+
const lines = gitBuffer(
|
|
269
|
+
repo,
|
|
270
|
+
['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)'],
|
|
271
|
+
Buffer.from(unique.join('\n') + '\n'),
|
|
272
|
+
).toString('utf8').trimEnd().split('\n');
|
|
273
|
+
const sizes = new Map();
|
|
274
|
+
for (let index = 0; index < unique.length; index++) {
|
|
275
|
+
const fields = (lines[index] || '').split(' ');
|
|
276
|
+
if (fields[1] === 'blob' && /^\d+$/.test(fields[2])) sizes.set(unique[index], Number(fields[2]));
|
|
277
|
+
}
|
|
278
|
+
return sizes;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function typeForMode(mode) {
|
|
282
|
+
if (mode === '160000') return 'commit';
|
|
283
|
+
if (mode === '040000') return 'tree';
|
|
284
|
+
return 'blob';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function compareEntries(left, right) {
|
|
288
|
+
return compareText(left.path, right.path)
|
|
289
|
+
|| compareText(String(left.oid), String(right.oid))
|
|
290
|
+
|| compareText(left.status, right.status);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function compareText(left, right) {
|
|
294
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
295
|
+
}
|