Files
ECC/scripts/lib/grok-source-identity.js
T

258 lines
9.0 KiB
JavaScript

'use strict';
const { execFileSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { fileURLToPath } = require('url');
const SHA_PATTERN = /^[0-9a-f]{40}$/;
const GIT_FETCH_TIMEOUT_MS = 60_000;
const TEMP_SOURCE_ROOTS = new Set();
process.once('exit', () => {
for (const tempRoot of TEMP_SOURCE_ROOTS) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
function isGitWorkTreeRoot(dir, fsModule = fs, execute = execFileSync) {
try {
const top = execute('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return fsModule.realpathSync(top) === fsModule.realpathSync(dir);
} catch {
return false;
}
}
function resolvePinnedGitSha(sourceRoot, sha) {
if (!SHA_PATTERN.test(sha || '')) {
return null;
}
try {
const resolved = execFileSync(
'git',
['-C', sourceRoot, 'rev-parse', '--verify', `${sha}^{commit}`],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
).trim();
return resolved === sha ? resolved : null;
} catch {
return null;
}
}
function copyGitArchive(sourceRoot, sha, dest, execute = execFileSync) {
fs.mkdirSync(dest, { recursive: true });
const archive = execute(
'git',
['-C', sourceRoot, 'archive', '--format=tar', sha],
{ maxBuffer: 512 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
);
execute('tar', ['-x', '-f', '-', '-C', dest], {
input: archive,
maxBuffer: 512 * 1024 * 1024,
stdio: ['pipe', 'ignore', 'pipe'],
});
}
function readPinnedMarketplaceSource(sourceRoot, pathModule = path) {
const catalogPath = pathModule.join(sourceRoot, '.grok-plugin', 'marketplace.json');
let catalog;
try {
catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
} catch (error) {
throw new Error(`Failed to read Grok marketplace source at ${catalogPath}: ${error.message}`);
}
const plugin = catalog && Array.isArray(catalog.plugins) ? catalog.plugins[0] : null;
const source = plugin && plugin.source;
if (!source || source.source !== 'url' || typeof source.url !== 'string' || !source.url.trim()) {
throw new Error('Grok marketplace source must be a Git URL');
}
if (!SHA_PATTERN.test(source.sha || '')) {
throw new Error('Grok marketplace source must pin a 40-character lowercase commit SHA');
}
if (typeof plugin.version !== 'string' || !plugin.version.trim()) {
throw new Error('Grok marketplace source must declare a plugin version');
}
return {
url: source.url,
sha: source.sha,
version: plugin.version,
};
}
function normalizeGitLocation(value, pathModule = path) {
const location = String(value || '').trim().replace(/\/+$/, '').replace(/\.git$/, '');
if (/^(?:[a-z][a-z0-9+.-]*:\/\/|git@)/i.test(location)) {
return location;
}
return pathModule.resolve(location);
}
function findRegistrySource(sourceRoot, homeDir, pathModule = path) {
const registryPath = pathModule.join(homeDir, '.grok', 'installed-plugins', 'registry.json');
let registry;
try {
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
} catch (error) {
if (error.code === 'ENOENT') return null;
throw new Error(`Failed to read Grok plugin registry at ${registryPath}: ${error.message}`);
}
const expectedPath = fs.realpathSync(sourceRoot);
for (const record of Object.values(registry.repos || {})) {
let recordPath;
try {
recordPath = fs.realpathSync(record.path);
} catch {
continue;
}
if (recordPath !== expectedPath || String(record.kind && record.kind.type).toLowerCase() !== 'git') {
continue;
}
return {
url: record.kind.url,
sha: record.kind.commit,
};
}
return null;
}
function sourceTransport(sourceUrl) {
if (typeof sourceUrl === 'string' && sourceUrl === sourceUrl.trim() && !/[\r\n\0]/.test(sourceUrl)) {
if (path.isAbsolute(sourceUrl) && !sourceUrl.startsWith('//') && !sourceUrl.startsWith('\\\\')) {
return 'file';
}
try {
const url = new URL(sourceUrl);
if (url.protocol === 'https:' && sourceUrl.startsWith('https://') && !url.username && !url.password) {
return 'https';
}
if (url.protocol === 'file:' && !url.hostname && path.isAbsolute(fileURLToPath(url))) {
return 'file';
}
} catch {
// Reject malformed URLs before Git can interpret them as a remote helper.
}
}
throw new Error('Grok source must use HTTPS or an absolute local repository');
}
function fetchPinnedGitSource(sourceUrl, sha, parentDir, execute = execFileSync) {
const protocol = sourceTransport(sourceUrl);
if (!SHA_PATTERN.test(sha || '')) {
throw new Error('Grok source must pin a 40-character lowercase commit SHA');
}
const gitDir = path.join(parentDir, 'git');
fs.mkdirSync(gitDir, { recursive: true });
execute('git', ['init', '--bare', '--quiet', gitDir], { stdio: 'ignore' });
try {
execute('git', ['-C', gitDir, 'fetch', '--quiet', '--depth=1', '--no-tags', '--', sourceUrl, sha], {
stdio: ['ignore', 'ignore', 'pipe'],
timeout: GIT_FETCH_TIMEOUT_MS,
// Enforce the transport after Git applies URL rewrites, even if user config allows helpers.
env: { ...process.env, GIT_ALLOW_PROTOCOL: protocol },
});
} catch (error) {
const detail = String(error.stderr || error.message || error).trim();
throw new Error(`Failed to fetch pinned Grok source ${sha}${detail ? `: ${detail}` : ''}`);
}
const fetched = execute('git', ['-C', gitDir, 'rev-parse', 'FETCH_HEAD^{commit}'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
if (fetched !== sha) {
throw new Error(`Fetched Grok source commit ${fetched} does not match pinned SHA ${sha}`);
}
return gitDir;
}
function assertNoSymlinks(root) {
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const entryPath = path.join(root, entry.name);
if (entry.isSymbolicLink()) {
throw new Error(`Pinned Grok source contains unsupported symlink: ${entryPath}`);
}
if (entry.isDirectory()) assertNoSymlinks(entryPath);
}
}
function preparePinnedGrokSource(options = {}) {
const sourceRoot = path.resolve(options.sourceRoot || '');
const homeDir = path.resolve(options.homeDir || os.homedir());
const catalogSource = readPinnedMarketplaceSource(sourceRoot);
const sourceUrl = options.sourceUrl || catalogSource.url;
const sourceSha = options.sourceSha || catalogSource.sha;
if (normalizeGitLocation(sourceUrl) !== normalizeGitLocation(catalogSource.url)) {
throw new Error('Recorded Grok source URL does not match the marketplace source');
}
if (!SHA_PATTERN.test(sourceSha || '')) {
throw new Error('Recorded Grok source must pin a 40-character lowercase commit SHA');
}
let gitSourceRoot = null;
if (isGitWorkTreeRoot(sourceRoot) && resolvePinnedGitSha(sourceRoot, sourceSha)) {
gitSourceRoot = sourceRoot;
} else {
const registrySource = findRegistrySource(sourceRoot, homeDir);
if (!registrySource
|| normalizeGitLocation(registrySource.url) !== normalizeGitLocation(sourceUrl)
|| !SHA_PATTERN.test(registrySource.sha || '')) {
throw new Error('Non-Git Grok source is unverifiable without matching registry URL/commit evidence');
}
}
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-grok-source-'));
TEMP_SOURCE_ROOTS.add(tempRoot);
const snapshotRoot = path.join(tempRoot, 'snapshot');
try {
if (!gitSourceRoot) {
gitSourceRoot = fetchPinnedGitSource(sourceUrl, sourceSha, tempRoot);
}
copyGitArchive(gitSourceRoot, sourceSha, snapshotRoot);
assertNoSymlinks(snapshotRoot);
const snapshotManifest = JSON.parse(
fs.readFileSync(path.join(snapshotRoot, '.grok-plugin', 'plugin.json'), 'utf8')
);
// The current catalog version proves the default pin. An explicit
// rollback pin may intentionally resolve an older release, whose version
// must come from that immutable snapshot instead of today's catalog.
if (sourceSha === catalogSource.sha && snapshotManifest.version !== catalogSource.version) {
throw new Error(
`Pinned Grok plugin version ${snapshotManifest.version || '(missing)'} does not match marketplace version ${catalogSource.version}`
);
}
fs.writeFileSync(path.join(snapshotRoot, '.ecc-source.json'), `${JSON.stringify({
source: 'url',
url: sourceUrl,
sha: sourceSha,
}, null, 2)}\n`);
return {
sourceRoot: snapshotRoot,
sourceUrl,
sourceSha,
sourceVersion: snapshotManifest.version,
cleanup: () => {
TEMP_SOURCE_ROOTS.delete(tempRoot);
fs.rmSync(tempRoot, { recursive: true, force: true });
},
};
} catch (error) {
fs.rmSync(tempRoot, { recursive: true, force: true });
TEMP_SOURCE_ROOTS.delete(tempRoot);
throw error;
}
}
module.exports = {
SHA_PATTERN,
copyGitArchive,
fetchPinnedGitSource,
isGitWorkTreeRoot,
resolvePinnedGitSha,
readPinnedMarketplaceSource,
preparePinnedGrokSource,
};