'use strict'; const fs = require('fs'); const crypto = require('crypto'); const os = require('os'); const path = require('path'); const { assertSafeInstallOperation } = require('./install/apply'); const { assertWithinTrustedRoot, realpathNearestExisting } = require('./path-safety'); const VALID_CLAUDE_SCOPES = new Set(['user', 'project', 'local']); const VALID_CLAUDE_HOOKS = new Set(['off', 'minimal', 'standard', 'strict']); const VALID_PROFILES = new Set(['minimal', 'core', 'developer', 'security', 'research', 'full']); function catalogHelpers() { return require('./harness-capabilities'); } function normalizeGuidedInstallRequest(input = {}) { const { normalizeHarnessSelection } = catalogHelpers(); const harnesses = normalizeHarnessSelection(input.harnesses || []); if (harnesses.length === 0) { throw new Error('Choose at least one guided harness: Claude, Codex, or Kimi.'); } const includesClaude = harnesses.includes('claude'); const includesKimi = harnesses.includes('kimi'); if (!includesClaude && (input.claudeScope !== undefined || input.claudeHooks !== undefined)) { throw new Error('Claude scope and hook options require Claude to be selected.'); } if (!includesKimi && input.profile !== undefined) { throw new Error('The managed install profile requires Kimi to be selected.'); } const claudeScope = includesClaude ? (input.claudeScope || 'user') : undefined; const claudeHooks = includesClaude ? (input.claudeHooks || 'standard') : undefined; const profile = includesKimi ? (input.profile || 'core') : undefined; if (claudeScope && !VALID_CLAUDE_SCOPES.has(claudeScope)) { throw new Error(`Invalid Claude scope: ${claudeScope}`); } if (claudeHooks && !VALID_CLAUDE_HOOKS.has(claudeHooks)) { throw new Error(`Invalid Claude hooks preference: ${claudeHooks}`); } if (profile && !VALID_PROFILES.has(profile)) { throw new Error(`Invalid Kimi install profile: ${profile}`); } return { harnesses, ...(claudeHooks ? { claudeHooks } : {}), ...(claudeScope ? { claudeScope } : {}), dryRun: Boolean(input.dryRun), json: Boolean(input.json), ...(profile ? { profile } : {}), yes: Boolean(input.yes), }; } function canonicalPath(filePath) { return realpathNearestExisting(filePath); } function pathsMatch(left, right) { return canonicalPath(left) === canonicalPath(right); } function fingerprintFile(filePath) { if (!fs.existsSync(filePath)) return { exists: false, sha256: null }; return { exists: true, sha256: crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'), }; } function operationIdentityMatches(stateOperation, plannedOperation) { return [ 'kind', 'moduleId', 'sourceRelativePath', 'strategy', 'scaffoldOnly', ].every(field => stateOperation[field] === plannedOperation[field]); } function assertInstallStateUnchanged(plan, expectedFingerprint) { const currentFingerprint = fingerprintFile(plan.installStatePath); if ( currentFingerprint.exists !== expectedFingerprint.exists || currentFingerprint.sha256 !== expectedFingerprint.sha256 ) { throw new Error( `Refusing to overwrite an unowned or changed install-state at ${plan.installStatePath}. ` + 'Re-run the guided preview and review the existing state before retrying.' ); } } function assertPriorInstallStateMatchesPlan(state, plan) { const target = state.target || {}; const adapter = plan.adapter || {}; if ( target.id !== adapter.id || target.target !== adapter.target || target.kind !== adapter.kind ) { throw new Error( `Refusing to trust managed install-state at ${plan.installStatePath}: ` + 'target identity does not match the current Kimi install plan.' ); } if (!pathsMatch(target.root, plan.targetRoot)) { throw new Error( `Refusing to trust managed install-state at ${plan.installStatePath}: ` + 'recorded root does not match the current install root.' ); } if (!pathsMatch(target.installStatePath, plan.installStatePath)) { throw new Error( `Refusing to trust managed install-state at ${plan.installStatePath}: ` + 'recorded install-state path does not match the current install-state path.' ); } } function readOwnedDestinations(plan, dependencies) { if (!plan.installStatePath) { return { destinations: new Set(), stateFingerprint: { exists: false, sha256: null } }; } try { assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath }); } catch (error) { throw new Error(`Refusing to trust managed install-state path: ${error.message}`); } if (!fs.existsSync(plan.installStatePath)) { return { destinations: new Set(), stateFingerprint: { exists: false, sha256: null } }; } const readState = dependencies.readInstallState || require('./install-state').readInstallState; const initialFingerprint = fingerprintFile(plan.installStatePath); const state = readState(plan.installStatePath); const validatedFingerprint = fingerprintFile(plan.installStatePath); if ( initialFingerprint.exists !== validatedFingerprint.exists || initialFingerprint.sha256 !== validatedFingerprint.sha256 ) { throw new Error( `Refusing to trust install-state that changed during validation: ${plan.installStatePath}.` ); } assertPriorInstallStateMatchesPlan(state, plan); const plannedByDestination = new Map(plan.operations.map(operation => [ canonicalPath(operation.destinationPath), operation, ])); const destinations = new Set(); for (const operation of state.operations || []) { if (operation.ownership !== 'managed') { throw new Error( `Refusing to trust non-managed ownership from install-state at ${plan.installStatePath}.` ); } const destinationPath = operation.destinationPath; assertWithinTrustedRoot(destinationPath, plan.targetRoot, 'trust install-state ownership'); const canonicalDestination = canonicalPath(destinationPath); const plannedOperation = plannedByDestination.get(canonicalDestination); if (!plannedOperation) continue; if (!operationIdentityMatches(operation, plannedOperation)) { throw new Error( `Refusing unverified ownership from install-state at ${plan.installStatePath}: ` + `operation identity does not match the current plan for ${destinationPath}.` ); } const currentFingerprint = fingerprintFile(destinationPath); if ( !currentFingerprint.exists || !/^[a-f0-9]{64}$/i.test(operation.contentSha256 || '') || currentFingerprint.sha256 !== operation.contentSha256.toLowerCase() ) { throw new Error( `Refusing unverified ownership from install-state at ${plan.installStatePath}: ` + `content digest does not match ${destinationPath}.` ); } destinations.add(canonicalDestination); } return { destinations, stateFingerprint: validatedFingerprint }; } function assertMergeDestination(destinationPath) { if (!fs.existsSync(destinationPath)) return null; let current; try { current = JSON.parse(fs.readFileSync(destinationPath, 'utf8')); } catch (error) { throw new Error(`Cannot merge ECC configuration into invalid JSON at ${destinationPath}: ${error.message}`); } if (!current || typeof current !== 'object' || Array.isArray(current)) { throw new Error(`Cannot merge ECC configuration at ${destinationPath}: expected a JSON object.`); } return current; } function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function findJsonConflicts(current, patch, prefix = '') { if (!isPlainObject(patch)) return []; return Object.entries(patch).flatMap(([key, patchValue]) => { if (!Object.prototype.hasOwnProperty.call(current, key)) return []; const currentValue = current[key]; const field = prefix ? `${prefix}.${key}` : key; if (isPlainObject(currentValue) && isPlainObject(patchValue)) { return findJsonConflicts(currentValue, patchValue, field); } return JSON.stringify(currentValue) === JSON.stringify(patchValue) ? [] : [field]; }); } function classifyManagedOperation(operation, ownedDestinations) { const destinationPath = operation.destinationPath; if (!fs.existsSync(destinationPath)) return 'create'; const canonicalDestination = canonicalPath(destinationPath); if (operation.kind === 'merge-json') { const current = assertMergeDestination(destinationPath); if (ownedDestinations.has(canonicalDestination)) return 'managed-json-update'; const conflicts = findJsonConflicts(current, operation.mergePayload); if (conflicts.length > 0) { throw new Error( `Refusing to overwrite unowned JSON fields at ${destinationPath}: ${conflicts.join(', ')}` ); } return 'json-merge'; } if (ownedDestinations.has(canonicalDestination)) return 'managed-update'; if ( operation.kind === 'copy-file' && typeof operation.sourcePath === 'string' && fs.existsSync(operation.sourcePath) && fs.statSync(destinationPath).isFile() && fs.readFileSync(operation.sourcePath).equals(fs.readFileSync(destinationPath)) ) { return 'identical'; } throw new Error(`Refusing to replace unowned existing file: ${destinationPath}`); } function writableRequirement(destinationPath) { if (fs.existsSync(destinationPath)) { const mode = fs.statSync(destinationPath).isDirectory() ? fs.constants.W_OK | fs.constants.X_OK : fs.constants.W_OK; return { candidatePath: destinationPath, mode }; } let candidatePath = path.dirname(destinationPath); while (!fs.existsSync(candidatePath)) { const parentPath = path.dirname(candidatePath); if (parentPath === candidatePath) break; candidatePath = parentPath; } return { candidatePath, mode: fs.constants.W_OK | fs.constants.X_OK, }; } function assertManagedDestinationsWritable(plan, dependencies) { const accessSync = dependencies.accessSync || fs.accessSync; const destinationPaths = [ ...plan.operations.map(operation => operation.destinationPath), ...(plan.installStatePath ? [plan.installStatePath] : []), ]; const requirements = new Map(); for (const destinationPath of destinationPaths) { const requirement = writableRequirement(destinationPath); const existingMode = requirements.get(requirement.candidatePath) || 0; requirements.set(requirement.candidatePath, existingMode | requirement.mode); } for (const [candidatePath, mode] of requirements) { try { accessSync(candidatePath, mode); } catch (_error) { const label = plan.target === 'kimi' ? 'Kimi' : 'Managed install'; throw new Error( `${label} destination is not writable by the current user: ${candidatePath}. ` + 'Fix the project ownership or permissions, then retry.' ); } } } function preflightManagedPlan(plan, dependencies = {}) { if (!plan || !Array.isArray(plan.operations)) { throw new Error('A managed install plan with operations is required.'); } const ownership = readOwnedDestinations(plan, dependencies); const operations = plan.operations.map(operation => { assertSafeInstallOperation(plan, operation); return { destinationPath: operation.destinationPath, kind: operation.kind, classification: classifyManagedOperation(operation, ownership.destinations), }; }); assertManagedDestinationsWritable(plan, dependencies); return { plan, operations, ownershipSnapshot: { destinations: [...ownership.destinations], stateFingerprint: ownership.stateFingerprint, }, }; } async function applyPreflightedManagedPlan(entry) { const preview = entry.preview && entry.preview.ownershipSnapshot ? entry.preview : preflightManagedPlan(entry.preview.plan); const ownedDestinations = new Set(preview.ownershipSnapshot.destinations); const expectedStateFingerprint = preview.ownershipSnapshot.stateFingerprint; let operationIndex = 0; const assertStateUnchanged = () => ( assertInstallStateUnchanged(preview.plan, expectedStateFingerprint) ); const result = require('./install-executor').applyInstallPlan(preview.plan, { beforeOperationWrite({ operation }) { assertStateUnchanged(); const expected = preview.operations[operationIndex]; const currentClassification = classifyManagedOperation(operation, ownedDestinations); const destination = canonicalPath(operation.destinationPath); if ( !expected || expected.kind !== operation.kind || canonicalPath(expected.destinationPath) !== destination || expected.classification !== currentClassification ) { throw new Error( `Refusing to write ${operation.destinationPath}: destination changed after Kimi preflight.` ); } ownedDestinations.add(destination); operationIndex += 1; }, beforeInstallStateWrite: assertStateUnchanged, }); const { projectCanonicalInstallState } = require('./install-state-store-sync'); const installStateProjection = await projectCanonicalInstallState(result.statePreview); return { ...result, installStateProjection, warnings: installStateProjection.warning ? [...result.warnings, `Install health projection warning: ${installStateProjection.warning.message}`] : result.warnings, }; } function defaultDependencies(options = {}) { return { previewClaude: request => require('../setup').reconcileClaudePlugin( { dryRun: true, hooks: request.claudeHooks, scope: request.claudeScope } ), previewCodex: () => require('./codex-plugin-setup').reconcileCodexPlugin({ dryRun: true }), createManagedPlan: request => require('./install/runtime').createInstallPlanFromRequest( require('./install/request').normalizeInstallRequest({ profileId: request.profile, target: 'kimi', }), { homeDir: options.homeDir || process.env.HOME || os.homedir(), projectRoot: options.projectRoot || process.cwd(), sourceRoot: options.sourceRoot, } ), preflightManaged: preflightManagedPlan, applyClaude: request => require('../setup').reconcileClaudePlugin( { dryRun: false, hooks: request.claudeHooks, scope: request.claudeScope } ), applyCodex: () => require('./codex-plugin-setup').reconcileCodexPlugin({ dryRun: false }), applyManaged: applyPreflightedManagedPlan, }; } async function createMultiHarnessPlan(request, injected = {}, options = {}) { const dependencies = { ...defaultDependencies(options), ...injected }; let entries = []; for (const id of request.harnesses) { if (id === 'claude') { entries = [...entries, { id, channel: 'native-plugin', preview: await dependencies.previewClaude(request) }]; } else if (id === 'codex') { entries = [...entries, { id, channel: 'native-plugin', preview: await dependencies.previewCodex(request) }]; } else if (id === 'kimi') { const managedPlan = await dependencies.createManagedPlan(request); entries = [...entries, { id, channel: 'managed-project', preview: await dependencies.preflightManaged(managedPlan), }]; } else { throw new Error(`Unsupported guided harness: ${id}`); } } return { harnesses: entries, request }; } async function applyMultiHarnessPlan(plan, injected = {}, options = {}) { const dependencies = { ...defaultDependencies(options), ...injected }; if (plan.request.dryRun) { return { status: 'preview', completed: [], retryHarnesses: [...plan.request.harnesses] }; } let completed = []; for (let index = 0; index < plan.harnesses.length; index += 1) { const entry = plan.harnesses[index]; try { let result; if (entry.id === 'claude') result = await dependencies.applyClaude(plan.request, entry); else if (entry.id === 'codex') result = await dependencies.applyCodex(plan.request, entry); else if (entry.preview && entry.preview.plan) { const latestPreview = dependencies.preflightManaged(entry.preview.plan); result = await dependencies.applyManaged( { ...entry, preview: latestPreview }, plan.request ); } else { result = await dependencies.applyManaged(entry, plan.request); } completed = [...completed, { id: entry.id, result }]; } catch (error) { return { status: completed.length > 0 ? 'partial' : 'failed', completed, failure: { id: entry.id, message: error.message }, retryHarnesses: plan.harnesses.slice(index).map(item => item.id), }; } } return { status: 'complete', completed, retryHarnesses: [] }; } module.exports = { VALID_CLAUDE_HOOKS, VALID_CLAUDE_SCOPES, VALID_PROFILES, applyMultiHarnessPlan, createMultiHarnessPlan, normalizeGuidedInstallRequest, preflightManagedPlan, findJsonConflicts, };