zephyr-agent
Version:
Zephyr plugin agent
393 lines (392 loc) • 15.3 kB
JavaScript
import "node:module";
import { execFileSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, writeFileSync } from "node:fs";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { committedRanges, readGitAiWorkingAttribution, workingRanges } from "./git-ai.mjs";
import { readSessionMetadata, sessionForOrigin } from "./sessions.mjs";
import { readAttributionConfig } from "./config.mjs";
const digest = (data)=>createHash('sha256').update(data).digest('hex');
function git(root, args, encoding) {
const options = {
cwd: root,
timeout: 5000,
maxBuffer: 67108864,
stdio: [
'ignore',
'pipe',
'ignore'
]
};
return encoding ? execFileSync('git', args, {
...options,
encoding
}) : execFileSync('git', args, options);
}
function attributionRepository(directory) {
const root = git(resolve(directory), [
'rev-parse',
'--show-toplevel'
], 'utf8').trim();
const gitDir = git(root, [
'rev-parse',
'--absolute-git-dir'
], 'utf8').trim();
return {
root,
gitDir
};
}
function excluded(file, config) {
return file.split('/').some((part)=>[
'node_modules',
'.git',
'dist',
'build',
'.next',
'.nuxt',
'.output',
'.turbo',
'coverage',
'.npmrc',
'.netrc'
].includes(part) || /^\.env(?:\.|$)/.test(part) || /\.(?:pem|key|p12|pfx)$/i.test(part)) || (config.exclude ?? []).some((prefix)=>file === prefix || file.startsWith(`${prefix.replace(/\/$/, '')}/`));
}
function privateDirectory(gitDir) {
const directory = join(gitDir, 'zephyr-attribution');
mkdirSync(directory, {
recursive: true,
mode: 448
});
return directory;
}
function captureSource(directory, phase = 'publication') {
let repository;
try {
repository = attributionRepository(directory);
} catch {
return;
}
const { root, gitDir } = repository;
try {
const config = readAttributionConfig(root, gitDir);
if (!config?.enabled) return;
let baseCommit = '';
try {
baseCommit = git(root, [
'rev-parse',
'--verify',
'HEAD'
], 'utf8').trim();
} catch {}
const paths = [
...new Set(git(root, [
'ls-files',
'-z',
'--cached',
'--others',
'--exclude-standard'
], 'utf8').split('\0').filter(Boolean))
].filter((file)=>!excluded(file, config)).sort();
if (paths.length > 20000) throw new Error('Source capture exceeds 20,000 files; configure exclude prefixes');
const head = new Map(commitEntries(root, baseCommit, config).map((entry)=>[
entry.file,
entry.oid
]));
const working = readGitAiWorkingAttribution(gitDir, baseCommit);
const metadata = readSessionMetadata(gitDir);
const sessions = Object.create(null);
const files = Object.create(null);
let bytes = 0;
for (const file of paths){
const absolute = resolve(root, file);
if (isAbsolute(relative(root, absolute)) || relative(root, absolute).startsWith(`..${sep}`)) throw new Error('Source path escapes repository');
const parents = file.split('/').slice(0, -1);
let parent = root;
for (const part of parents){
parent = join(parent, part);
if (existsSync(parent) && lstatSync(parent).isSymbolicLink()) throw new Error('Source parent is a symlink');
}
let stat;
try {
stat = lstatSync(absolute);
} catch (error) {
if ('ENOENT' === error.code) continue;
throw error;
}
if (stat.isDirectory()) throw new Error(`Submodule or directory entry needs an exclusion: ${file}`);
if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error(`Unsupported source entry: ${file}`);
bytes += stat.size;
if (bytes > 52428800) throw new Error('Source capture exceeds 50 MiB; configure exclude prefixes');
const content = stat.isSymbolicLink() ? Buffer.from(readlinkSync(absolute)) : readFileSync(absolute);
const hash = digest(content);
const mode = stat.isSymbolicLink() ? '120000' : 73 & stat.mode ? '100755' : '100644';
const attribution = workingRanges(working.entries.get(file), hash, working.authors);
const oid = head.get(file);
const committed = void 0 === attribution && !working.entries.has(file) && Boolean(oid && blobId(content, oid.length) === oid) && !stat.isSymbolicLink();
for (const range of attribution ?? []){
const match = sessionForOrigin(range.origin, metadata);
if (!match) continue;
sessions[match.key] = match.session;
const session = match.session;
range.origin = {
...range.origin,
agent: session.agent,
harness: session.harness,
provider: session.provider,
reasoningEffort: session.reasoningEffort,
turn: session.turn,
turnAssociation: session.turnAssociation,
promptInitiator: session.promptInitiator
};
}
files[file] = {
hash,
mode,
content: content.toString('base64'),
attribution: attribution ?? [],
...committed ? {
committed: true
} : {}
};
}
const fingerprint = digest(JSON.stringify(Object.entries(files).map(([file, value])=>[
file,
value.mode,
value.hash
])));
let workspaceHuman;
try {
workspaceHuman = git(root, [
'var',
'GIT_AUTHOR_IDENT'
], 'utf8').trim().replace(/ \d+ [+-]\d{4}$/, '');
} catch {}
const record = {
schemaVersion: 1,
id: `source-${randomUUID()}`,
fingerprint,
baseCommit,
dirty: Boolean(git(root, [
'status',
'--porcelain',
'--untracked-files=normal'
], 'utf8').trim()),
capturedAt: new Date().toISOString(),
workspaceHuman,
sessions,
files
};
writeFileSync(join(privateDirectory(gitDir), `${record.id}.json`), JSON.stringify(record), {
flag: 'wx',
mode: 384
});
return {
...repository,
phase,
record,
storage: config.storage ?? 'local'
};
} catch (error) {
return {
...repository,
phase,
reason: error.message
};
}
}
function finishSourceCapture(directory, start, version) {
const end = captureSource(directory);
if (!end && !start) return;
const record = end?.record;
if (!record) return {
schemaVersion: 1,
status: 'unavailable',
reason: end?.reason ?? 'Attribution disabled during build',
scope: 'git-working-tree',
consistency: 'unavailable',
identity: 'self-reported'
};
const summary = {
schemaVersion: 1,
status: 'captured',
scope: 'git-working-tree',
identity: 'self-reported',
storage: end.storage ?? 'local',
sourceId: record.id,
sourceFingerprint: record.fingerprint,
baseCommit: record.baseCommit,
dirty: record.dirty,
capturedAt: record.capturedAt,
workspaceHuman: record.workspaceHuman,
sessions: record.sessions,
reason: start?.reason,
startSourceId: start?.record?.id,
startSourceFingerprint: start?.record?.fingerprint,
consistency: start?.reason ? 'unavailable' : start?.record && 'build-start' === start.phase ? start.record.fingerprint === record.fingerprint ? 'boundary-match' : 'changed-during-build' : 'publication-only',
files: Object.fromEntries(Object.entries(record.files).map(([file, value])=>[
file,
{
hash: value.hash,
mode: value.mode,
attribution: value.attribution
}
]))
};
try {
writeFileSync(join(privateDirectory(end.gitDir), `version-${digest(version)}.json`), JSON.stringify({
version,
summary
}), {
flag: 'wx',
mode: 384
});
} catch (error) {
return {
schemaVersion: 1,
status: 'unavailable',
scope: 'git-working-tree',
consistency: 'unavailable',
identity: 'self-reported',
reason: 'EEXIST' === error.code ? `A source receipt already exists for version ${version}` : 'Unable to save the private version receipt'
};
}
return summary;
}
function loadSourceRecord(directory, id) {
const { gitDir } = attributionRepository(directory);
const storage = privateDirectory(gitDir);
let sourceId = id;
if (!/^source-[a-f0-9-]{36}$/.test(id)) {
const receipt = JSON.parse(readFileSync(join(storage, `version-${digest(id)}.json`), 'utf8'));
sourceId = receipt.summary.sourceId;
}
if (!/^source-[a-f0-9-]{36}$/.test(sourceId)) throw new Error('Invalid source record id');
const record = JSON.parse(readFileSync(join(storage, `${sourceId}.json`), 'utf8'));
if (1 !== record.schemaVersion || record.id !== sourceId) throw new Error('Unsupported source record');
return record;
}
function loadVersionAttribution(directory, version) {
const { gitDir } = attributionRepository(directory);
const receipt = JSON.parse(readFileSync(join(privateDirectory(gitDir), `version-${digest(version)}.json`), 'utf8'));
if (receipt.version !== version || receipt.summary?.schemaVersion !== 1) throw new Error('Invalid private version receipt');
return receipt.summary;
}
function blobId(content, oidLength) {
return createHash(64 === oidLength ? 'sha256' : 'sha1').update(`blob ${content.length}\0`).update(content).digest('hex');
}
function commitEntries(root, commit, config) {
const entries = (commit ? git(root, [
'ls-tree',
'-rz',
'--full-tree',
commit
], 'utf8') : '').split('\0').filter(Boolean).map((entry)=>{
const tab = entry.indexOf('\t');
const [mode, type, oid] = entry.slice(0, tab).split(' ');
return {
mode,
type,
oid,
file: entry.slice(tab + 1)
};
}).filter(({ file })=>!excluded(file, config));
if (entries.length > 20000) throw new Error('Git baseline exceeds 20,000 entries');
if (entries.some(({ type })=>'blob' !== type)) throw new Error('Git baseline contains a submodule; configure exclusions');
return entries;
}
function comparisonFile(directory, record, file) {
const value = record.files[file];
if (!value?.committed || !record.baseCommit || value.attribution.length) return value;
const { root, gitDir } = attributionRepository(directory);
const config = readAttributionConfig(root, gitDir);
return {
...value,
attribution: committedRanges(root, config?.gitAiPath ?? 'git-ai', file, record.baseCommit)
};
}
function sourceCommitBaseline(directory, captured) {
const { root, gitDir } = attributionRepository(directory);
const config = readAttributionConfig(root, gitDir);
if (!config?.enabled) throw new Error('Attribution is disabled');
const files = Object.create(null);
const entries = commitEntries(root, captured.baseCommit, config);
const reused = new Map();
const missing = new Set();
for (const { file, oid } of entries){
const current = captured.files[file];
const content = current && Buffer.from(current.content, 'base64');
if (content && blobId(content, oid.length) === oid) reused.set(oid, content);
else missing.add(oid);
}
const blobs = new Map(reused);
const requested = [
...missing
].filter((oid)=>!blobs.has(oid));
if (requested.length) {
const output = execFileSync('git', [
'cat-file',
'--batch'
], {
cwd: root,
input: requested.join('\n') + '\n',
timeout: 5000,
maxBuffer: 67108864,
stdio: [
'pipe',
'pipe',
'ignore'
]
});
let offset = 0;
let fetched = 0;
for (const oid of requested){
const end = output.indexOf(10, offset);
const header = output.subarray(offset, end).toString('ascii');
const match = /^([a-f0-9]{40,64}) blob (\d+)$/.exec(header);
if (end < 0 || !match || match[1] !== oid) throw new Error('Invalid Git blob batch');
const size = Number(match[2]);
fetched += size;
if (fetched > 52428800) throw new Error('Git baseline exceeds 50 MiB');
offset = end + 1;
if (!Number.isSafeInteger(size) || offset + size >= output.length || 10 !== output[offset + size]) throw new Error('Incomplete Git blob batch');
blobs.set(oid, output.subarray(offset, offset + size));
offset += size + 1;
}
}
let bytes = 0;
for (const { mode, oid, file } of entries){
const content = blobs.get(oid);
bytes += content.length;
if (bytes > 52428800) throw new Error('Git baseline exceeds 50 MiB');
const hash = digest(content);
const current = captured.files[file];
const unchanged = hash === current?.hash;
files[file] = {
hash,
mode,
content: content.toString('base64'),
attribution: unchanged ? current.attribution : [],
...unchanged ? current.committed ? {
committed: true
} : {} : {
committed: '120000' !== mode
}
};
}
return {
schemaVersion: 1,
id: `head-${captured.baseCommit || 'unborn'}`,
fingerprint: digest(JSON.stringify(Object.entries(files).map(([file, value])=>[
file,
value.mode,
value.hash
]))),
baseCommit: captured.baseCommit,
dirty: false,
capturedAt: captured.capturedAt,
files
};
}
export { attributionRepository, captureSource, comparisonFile, finishSourceCapture, loadSourceRecord, loadVersionAttribution, readAttributionConfig, sourceCommitBaseline };
//# sourceMappingURL=source.mjs.map