'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, };