Files
ycode/scripts/import-rea-expert.mjs

78 lines
8.9 KiB
JavaScript

/** Install a SHA-verified REA recovery into YCode's managed local expert catalog. */
import fs from 'node:fs/promises'
import path from 'node:path'
import { createHash, randomUUID } from 'node:crypto'
import { pathToFileURL } from 'node:url'
import { build } from 'esbuild'
import { parse as parseYaml } from 'yaml'
const option = name => { const index = process.argv.indexOf(name); if (index < 0 || !process.argv[index + 1]) throw Error('Missing ' + name); return process.argv[index + 1] }
const userData = path.resolve(option('--user-data')), source = path.resolve(option('--source'))
if (path.basename(userData).toLowerCase() !== 'ycode' || /[\\/]\.workbuddy(?:[\\/]|$)/i.test(userData)) throw Error('Explicit YCode userData directory required')
const reportDirectory = await fs.mkdtemp(path.resolve('.tmp/rea-expert-install-'))
const sha = bytes => createHash('sha256').update(bytes).digest('hex')
function relative(value) { if (typeof value !== 'string' || !value || value.includes('\\') || /[\x00-\x1f<>:"|?*]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..' || /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw Error('Invalid recovery path'); return value }
async function directory(target, create = false) {
let current = path.parse(path.resolve(target)).root
for (const part of path.relative(current, path.resolve(target)).split(path.sep)) {
current = path.join(current, part)
try { const stat = await fs.lstat(current); if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error('Ordinary directories required') }
catch (e) { if (e.code !== 'ENOENT' || !create) throw e; await fs.mkdir(current); const stat = await fs.lstat(current); if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error('Directory changed') }
}
}
async function readOrdinary(file) { await directory(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) throw Error('Ordinary file required'); return fs.readFile(file) }
const evidence = JSON.parse(await readOrdinary(path.join(source, 'evidence-manifest.json')))
if (!/^[a-f0-9]{64}$/.test(evidence.rootSha256) || !Array.isArray(evidence.files) || !evidence.files.length || evidence.files.length > 256) throw Error('Invalid REA source manifest')
let total = 0; const files = new Map(), texts = Object.create(null), folded = new Set()
for (const item of evidence.files) {
const name = relative(item.path), key = name.normalize('NFKC').toLowerCase()
if (folded.has(key)) throw Error('Duplicate recovery file'); folded.add(key)
if (!/^ev_[a-f0-9]{64}$/.test(item.evidenceId)) throw Error('Missing file Evidence identity')
const bytes = await readOrdinary(path.join(source, name)); total += bytes.length
if (bytes.length !== item.bytes || sha(bytes) !== item.sha256 || total > 16 * 1024 * 1024) throw Error('Recovery SHA/byte budget mismatch: ' + name)
files.set(name, bytes)
if (/\.(?:md|json|txt|yaml|yml|py|js|ts|csv)$/i.test(name)) texts[name] = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
}
const manifest = JSON.parse(texts['.codebuddy-plugin/plugin.json'] ?? texts['.workbuddy-plugin/plugin.json'])
const id = manifest.name
if (!/^[a-z0-9][a-z0-9-]{0,126}[a-z0-9]$/.test(id)) throw Error('Invalid expert identifier')
if (manifest.avatar) { const avatar = files.get(relative(manifest.avatar.replace(/^\.\//, ''))); if (!avatar || avatar.length > 500 * 1024) throw Error('Recovered avatar missing/oversized') }
await build({ stdin: { contents: "export * from './src/main/capabilities/experts'", resolveDir: process.cwd(), loader: 'ts' }, outfile: path.join(reportDirectory, 'expert-service.mjs'), bundle: true, platform: 'node', format: 'esm', packages: 'external', logLevel: 'warning', plugins: [{ name: 'installation-path', setup(builder) { builder.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'installation' })); builder.onLoad({ filter: /.*/, namespace: 'installation' }, () => ({ contents: `export const app={getPath:()=>${JSON.stringify(userData)}};export const safeStorage={isEncryptionAvailable:()=>false}` })) } }] })
const service = await import(pathToFileURL(path.join(reportDirectory, 'expert-service.mjs')).href)
const validation = service.validateExpertFiles(texts, id)
if (validation.errors.length) throw Error(validation.errors.join('\n'))
const root = path.join(userData, 'plugins', 'marketplaces', 'my-experts'), packages = path.join(root, 'plugins'), target = path.join(packages, id), index = path.join(root, '.codebuddy-plugin', 'marketplace.json')
const result = { expertId: id, sourceRootSha256: evidence.rootSha256, files: evidence.files.map(({ path, bytes, sha256, evidenceId }) => ({ path, bytes, sha256, evidenceId })), bytes: total, warnings: validation.warnings, installedPath: target }
if (process.argv.includes('--validate-only')) { await fs.writeFile(path.join(reportDirectory, 'report.json'), JSON.stringify({ ...result, result: 'VALIDATED' }, null, 2)); console.log(JSON.stringify({ result: 'VALIDATED', expertId: id, files: files.size, bytes: total, directory: reportDirectory })); process.exit(0) }
await directory(packages, true); await directory(path.dirname(index), true)
let before = null
try { before = await readOrdinary(index) } catch (e) { if (e.code !== 'ENOENT') throw e }
const catalog = before ? JSON.parse(before) : { name: 'my-experts', description: 'my-experts marketplace (auto-generated)', plugins: [] }
if (!Array.isArray(catalog.plugins)) throw Error('Existing expert catalog is invalid')
if (await fs.lstat(target).then(() => true, e => { if (e.code === 'ENOENT') return false; throw e })) throw Error('Expert already exists; refusing to overwrite user files')
if (catalog.plugins.some(entry => entry.name === id || entry.source === `./plugins/${id}`)) throw Error('Existing catalog identity conflict')
if (before) await fs.writeFile(path.join(reportDirectory, 'marketplace.before.json'), before, { flag: 'wx' })
const stage = path.join(packages, '.install-' + randomUUID()), temporaryIndex = index + '.' + randomUUID() + '.tmp'
let published = false, committed = false
await fs.mkdir(stage)
try {
for (const [name, bytes] of files) { const destination = path.join(stage, name); await directory(path.dirname(destination), true); await fs.writeFile(destination, bytes, { flag: 'wx' }); if (sha(await fs.readFile(destination)) !== sha(bytes)) throw Error('Staged file mismatch') }
let current = null; try { current = await readOrdinary(index) } catch (e) { if (e.code !== 'ENOENT') throw e }
if ((current?.toString('base64') ?? null) !== (before?.toString('base64') ?? null)) throw Error('Catalog changed during import; retry after current operation completes')
await fs.rename(stage, target); published = true
const after = { ...catalog, plugins: [...catalog.plugins, { name: id, source: `./plugins/${id}`, description: manifest.description }] }
await fs.writeFile(temporaryIndex, JSON.stringify(after, null, 2), { flag: 'wx' }); await fs.rename(temporaryIndex, index); committed = true
} catch (e) { if (published && !committed) await fs.rm(target, { recursive: true, force: true }); throw e }
finally { await fs.rm(stage, { recursive: true, force: true }); await fs.rm(temporaryIndex, { force: true }) }
const document = await service.readExpert(id), binding = await service.activateExpert(id), turn = await service.expertTurnContext(id)
const frontmatter = text => parseYaml(text?.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? '') ?? {}
const registeredSkills = (manifest.skills ?? []).map(entry => { const resource = entry.replace(/^\.\//, '') + '/SKILL.md'; return { name: frontmatter(texts[resource]).name, content: texts[resource] } })
const declaredSkills = manifest.expertType === 'agent' ? frontmatter(texts[`agents/${manifest.agentName ?? id}.md`]).skills ?? [] : registeredSkills.map(skill => skill.name)
const skillPreloaded = declaredSkills.every(name => { const matches = registeredSkills.filter(skill => skill.name === name); return matches.length === 1 && turn.context.includes(matches[0].content) })
if (document.expert.error || !document.prompt || !turn.context.includes(document.prompt) || !binding.avatarUrl?.startsWith('data:image/') || !skillPreloaded) throw Error('Installed expert did not activate correctly')
for (const [name, bytes] of files) if (sha(await readOrdinary(path.join(target, name))) !== sha(bytes)) throw Error('Installed SHA mismatch')
const provenance = path.join(userData, 'capabilities', 'expert-imports'); await directory(provenance, true)
await fs.writeFile(path.join(provenance, id + '.json'), JSON.stringify({ ...result, installedAt: new Date().toISOString() }, null, 2), { flag: 'wx' })
await fs.writeFile(path.join(reportDirectory, 'report.json'), JSON.stringify({ ...result, result: 'PASS', registered: true, activated: true, assistantName: binding.profession || binding.name, skillPreloaded, installedFileShaMatched: files.size }, null, 2))
console.log(JSON.stringify({ result: 'PASS', expertId: id, files: files.size, bytes: total, directory: reportDirectory }))