// Surface fingerprint of a live endpoint (docs/21 §Applying surfaces to a live endpoint). // Invariant: whatever writes the configuration a running native process read at launch — a capability // sweep, the memory provider's own provision/update verbs, `native-memory`, an operator — is applied at // the next turn boundary. Providers write their surfaces WITHOUT going through the core, so the core // cannot rely on being told: it records what the launch read and compares it on the supervise tick. // // Only files the runtime reads at launch AND that the runtime itself never writes while it runs are // fingerprinted — a self-written file would turn every turn into a reload. Host-global configuration // (~/.codex/config.toml, ~/.claude.json) is written by the runtimes themselves and is NOT watched; the // core verbs that write it request a reload explicitly. import { createHash } from 'node:crypto' import { existsSync, readdirSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { writeFileAtomic } from '../storage/index.ts' import type { LifecycleConfig } from '../lifecycle/index.ts' import { opencodePrivateDir } from '../storage/index.ts' /** Files in the OpenCode private dir that are runtime state or regenerated by the core on every launch. */ const OPENCODE_NOT_SURFACE = new Set(['state.json', 'events.jsonl', 'control.sock', 'iapeer-plugin.mjs', 'peer-mcp.json']) export function surfaceFiles(runtime: string, cwd: string, personality: string, env: NodeJS.ProcessEnv): string[] { if (runtime === 'claude') return [join(cwd, '.mcp.json'), join(cwd, '.claude', 'settings.json')] if (runtime === 'codex') return [join(cwd, '.codex', 'config.toml'), join(cwd, '.codex', 'hooks.json')] if (runtime === 'opencode') { const dir = opencodePrivateDir(personality, { env }) const own = existsSync(dir) ? readdirSync(dir).filter(n => !OPENCODE_NOT_SURFACE.has(n) && /\.(json|mjs)$/.test(n)).sort().map(n => join(dir, n)) : [] // The core projects the peer's claude/codex MCP declarations into OpenCode at launch (peer-mcp.json): // their sources are OpenCode surfaces too. return [...own, join(cwd, '.mcp.json'), join(cwd, '.codex', 'config.toml')] } return [] } export function surfaceFingerprint(runtime: string, cwd: string, personality: string, env: NodeJS.ProcessEnv): string { const h = createHash('sha256') for (const f of surfaceFiles(runtime, cwd, personality, env)) { h.update(f).update('\0') try { h.update(readFileSync(f)) } catch { h.update('\u0000absent') } h.update('\0') } return h.digest('hex') } const baselinePath = (cfg: LifecycleConfig, identity: string): string => join(cfg.stateDir, `${identity}.surfaces`) export function readSurfaceBaseline(cfg: LifecycleConfig, identity: string): string | undefined { try { return readFileSync(baselinePath(cfg, identity), 'utf8').trim() || undefined } catch { return undefined } } export function writeSurfaceBaseline(cfg: LifecycleConfig, identity: string, fingerprint: string): void { writeFileAtomic(baselinePath(cfg, identity), fingerprint + '\n') }