|
| 1 | +import { createHash as nuphosHomeHash } from 'node:crypto' |
| 2 | +import { mkdirSync as nuphosMkdir, lstatSync as nuphosLstat } from 'node:fs' |
| 3 | +import { homedir as nuphosHostHome } from 'node:os' |
| 4 | +import { join as nuphosJoin, delimiter as nuphosPathDelimiter } from 'node:path' |
| 5 | + |
| 6 | +// Configuration isolation, not a filesystem security boundary: sessions still |
| 7 | +// run as the same OS user. Never mutate process.env or copy ambient credentials. |
| 8 | +// Use the conversation ID, not the ACP process/turn ID, so load/resume and token |
| 9 | +// refresh keep the same home. Hash it so metadata cannot supply a filesystem path. |
| 10 | +export function nuphosSessionHomeEnv(sessionEnv = {}, runtimeEnv = process.env) { |
| 11 | + const id = sessionEnv.NUPHOS_SESSION_ID |
| 12 | + if (typeof id !== 'string' || !id.trim()) { |
| 13 | + if (sessionEnv.NUPHOS_TOKEN || sessionEnv.NUPHOS_TEAM_ID) |
| 14 | + throw new Error('Nuphos sessions require NUPHOS_SESSION_ID for CLI configuration isolation') |
| 15 | + return {} // Ordinary ACP clients without a Nuphos conversation keep their defaults. |
| 16 | + } |
| 17 | + const runtimeHome = runtimeEnv.HOME || runtimeEnv.USERPROFILE || nuphosHostHome() |
| 18 | + const root = nuphosJoin(runtimeHome, '.nuphos', 'session-homes') |
| 19 | + const home = nuphosJoin(root, nuphosHomeHash('sha256').update(id).digest('hex')) |
| 20 | + for (const dir of [root, home]) { |
| 21 | + nuphosMkdir(dir, { recursive: true, mode: 0o700 }) |
| 22 | + if (!nuphosLstat(dir).isDirectory()) throw new Error('Session home must be a directory') |
| 23 | + } |
| 24 | + const config = nuphosJoin(home, '.config') |
| 25 | + return { |
| 26 | + HOME: home, |
| 27 | + USERPROFILE: home, |
| 28 | + APPDATA: nuphosJoin(home, 'AppData', 'Roaming'), |
| 29 | + LOCALAPPDATA: nuphosJoin(home, 'AppData', 'Local'), |
| 30 | + XDG_CONFIG_HOME: config, |
| 31 | + XDG_CACHE_HOME: nuphosJoin(home, '.cache'), |
| 32 | + XDG_DATA_HOME: nuphosJoin(home, '.local', 'share'), |
| 33 | + XDG_STATE_HOME: nuphosJoin(home, '.local', 'state'), |
| 34 | + CLOUDSDK_CONFIG: nuphosJoin(config, 'gcloud'), |
| 35 | + GH_CONFIG_DIR: nuphosJoin(config, 'gh'), |
| 36 | + AWS_SHARED_CREDENTIALS_FILE: nuphosJoin(home, '.aws', 'credentials'), |
| 37 | + AWS_CONFIG_FILE: nuphosJoin(home, '.aws', 'config'), |
| 38 | + KUBECONFIG: nuphosJoin(home, '.kube', 'config'), |
| 39 | + AZURE_CONFIG_DIR: nuphosJoin(home, '.azure'), |
| 40 | + DOCKER_CONFIG: nuphosJoin(home, '.docker'), |
| 41 | + GIT_CONFIG_GLOBAL: nuphosJoin(home, '.gitconfig'), |
| 42 | + GIT_CONFIG_NOSYSTEM: '1', |
| 43 | + NPM_CONFIG_USERCONFIG: nuphosJoin(home, '.npmrc'), |
| 44 | + GNUPGHOME: nuphosJoin(home, '.gnupg'), |
| 45 | + PATH: [nuphosJoin(home, '.local', 'bin'), runtimeEnv.PATH] |
| 46 | + .filter(Boolean) |
| 47 | + .join(nuphosPathDelimiter), |
| 48 | + NUPHOS_SESSION_HOME: home, |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +// Claude's own login/session store belongs to the runtime. Pin its location |
| 53 | +// before giving the SDK child (and its shell tools) the conversation's HOME. |
| 54 | +export function nuphosClaudeSessionEnv(env, sessionEnv, runtimeEnv = process.env) { |
| 55 | + const isolated = nuphosSessionHomeEnv(sessionEnv, runtimeEnv) |
| 56 | + if (!isolated.HOME) return env |
| 57 | + return { |
| 58 | + ...env, |
| 59 | + CLAUDE_CONFIG_DIR: |
| 60 | + runtimeEnv.CLAUDE_CONFIG_DIR || |
| 61 | + nuphosJoin(runtimeEnv.HOME || runtimeEnv.USERPROFILE || nuphosHostHome(), '.claude'), |
| 62 | + ...isolated, |
| 63 | + } |
| 64 | +} |
0 commit comments