mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
feat(tui): PR-0 — TUI-only ISOLATION .claude.json seed
First PR of the TUI-mode A-path (interactive claude → cc_entrypoint=cli → subscription pool, so the proxy stays usable for Pro subscribers post-6/15). Adds a `tui` opt-in param to prepareIsolatedEnvironment (lib/sandbox/manager.mjs). When tui:true (and only then): - chmod 700 the per-reqId dir + ephemeralRoot (credential-wall, spec §5.5) - seed ephemeral .claude.json via _seedTuiClaudeJson: read ~/.claude.json (or tuiSeedSource), STRIP projects (no owner history leak), stamp hasCompletedOnboarding + bypassPermissionsModeAccepted, preserve oauthAccount/userID, write mode 0o600, never log contents - return reqId in isolationCtx (PR-2 driver derives tmux session name — maintainer decision P1: reuse isolationCtx, no server.mjs call-site edits) DEFAULT PATH BYTE-FOR-BYTE UNCHANGED: when tui falsy, none of the above runs; the only return-shape change is the additive reqId field existing callers ignore. Reviewer verified this invariant airtight (server.mjs:1347/:1564 pass no tui arg). Seed carries NO MCP-disable weight (T6 negative control: --strict-mcp-config in PR-2 is the mechanism, not the seed). The seed also does NOT suppress the per-directory trust-folder dialog — corrected a review-fix overclaim that attributed trust-dialog suppression to bypassPermissionsModeAccepted (that flag only suppresses the bypass-permissions dialog; the trust dialog still appears and PR-2's driver MUST answer it "1", per the spikes). Tests: Suite 45 (45a default-path-unchanged, 45b tui-seed perms/markers/ strip, 45c missing-source minimal seed). 816 pass / 0 fail. ADR 0002 Amendment 9 § tuiSeed extension note added. Authority: design spec docs/superpowers/specs/2026-05-30-tui-mode-production-design.md §7.1/§5.5/§12.1; implementation plan + maintainer decisions; ADR 0002 Amd 9; claude CLI v2.1.158. Co-Authored-By: jaekwon-park <insainty21@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
jaekwon-park
Claude Opus 4.8
parent
0e334f9cac
commit
1fd27e1c11
+107
-5
@@ -47,7 +47,7 @@
|
||||
* __resetSandboxManagerForTests() — test seam: reset module state
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, symlinkSync } from 'node:fs';
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { rm } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
@@ -189,18 +189,21 @@ export function isSandboxActive() {
|
||||
* returns the legacy unsandboxed shape (identity env, identity hooks, no cleanup).
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object} params.provider — provider plugin object (may have .ISOLATION)
|
||||
* @param {string} params.keyId — OLP key identity driving this request
|
||||
* @param {string} params.reqId — per-request UUID
|
||||
* @param {object} params.provider — provider plugin object (may have .ISOLATION)
|
||||
* @param {string} params.keyId — OLP key identity driving this request
|
||||
* @param {string} params.reqId — per-request UUID
|
||||
* @param {boolean} [params.tui=false] — TUI-mode opt-in: seed .claude.json + chmod 700 (spec §7.1)
|
||||
* @param {string} [params.tuiSeedSource] — override path for the seed source (default: ~/.claude.json)
|
||||
* @returns {Promise<{
|
||||
* ephemeralRoot: string|null,
|
||||
* reqId: string,
|
||||
* envOverrides: Record<string, string>,
|
||||
* hardenedArgs: (args: string[]) => string[],
|
||||
* wrapForLayer3: (command: string) => Promise<string>,
|
||||
* cleanup: () => Promise<void>,
|
||||
* }>}
|
||||
*/
|
||||
export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
||||
export async function prepareIsolatedEnvironment({ provider, keyId, reqId, tui = false, tuiSeedSource }) {
|
||||
const isolation = provider?.ISOLATION;
|
||||
|
||||
// ── Test-context bypass ──────────────────────────────────────────────────
|
||||
@@ -317,6 +320,46 @@ export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── TUI-mode: chmod 700 + seed .claude.json (gated on tui=true) ─────────
|
||||
// Spec §7.1 + §5.5: runs ONLY when the caller opts in with tui:true.
|
||||
// The default (stream-json) path is byte-for-byte unchanged — this block
|
||||
// does not execute when tui is falsy.
|
||||
//
|
||||
// NOTE: the seed does NOT disable managed MCP. That is the spawn-argv flag
|
||||
// --strict-mcp-config (PR-2). See spec §5.2 + T6 negative control.
|
||||
if (tui) {
|
||||
// chmod 700 the per-reqId dir and ephemeralRoot so sibling spawns cannot
|
||||
// traverse into each other's ephemeral homes (§5.5 credential-wall).
|
||||
const reqIdDir = join(SPAWN_BASE_DIR, safeKeyId, safeReqId);
|
||||
chmodSync(reqIdDir, 0o700);
|
||||
chmodSync(ephemeralRoot, 0o700);
|
||||
|
||||
// Seed .claude.json with onboarding + bypass markers.
|
||||
//
|
||||
// PR-0 seeds `hasCompletedOnboarding` + `bypassPermissionsModeAccepted` only.
|
||||
// It deliberately does NOT pre-trust the spawn cwd, and the per-directory
|
||||
// **trust-folder dialog is therefore NOT suppressed** by this seed.
|
||||
//
|
||||
// Important correction to the original plan §4/§7.1 framing: there are TWO
|
||||
// distinct dialogs. `bypassPermissionsModeAccepted:true` suppresses the
|
||||
// bypass-permissions acceptance dialog ("you accept all responsibility…
|
||||
// 1.No 2.Yes") — verified on claude v2.1.158. It does NOT suppress the
|
||||
// per-directory trust-folder dialog ("Is this a project you trust? 1.Yes
|
||||
// 2.No"), which still appears for a fresh ephemeral $HOME (projects is
|
||||
// stripped). The pre-code spikes confirmed this — their playbook answers
|
||||
// the trust dialog by sending "1".
|
||||
//
|
||||
// PR-0 does not pre-trust because (a) the spawn cwd is chosen by PR-2's
|
||||
// session driver, not known here, and (b) the exact trust-field key in the
|
||||
// projects map is unverified. Therefore **PR-2's session driver MUST answer
|
||||
// the trust-folder dialog (send "1")** — the spike-validated approach.
|
||||
// Pre-trusting projects[cwd] in the seed is an optional future optimization
|
||||
// (PR-2 could thread cwd back), but the dialog-answer is the proven default.
|
||||
// See ADR 0002 Amendment 9 § tuiSeed extension note.
|
||||
const resolvedSeedSource = tuiSeedSource ?? join(homedir(), '.claude.json');
|
||||
_seedTuiClaudeJson(ephemeralRoot, resolvedSeedSource);
|
||||
}
|
||||
|
||||
// ── Compose envOverrides (Layer 1 output) ────────────────────────────────
|
||||
let envOverrides = {};
|
||||
if (typeof isolation.ephemeralEnvOverrides === 'function') {
|
||||
@@ -434,6 +477,7 @@ export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
||||
|
||||
return {
|
||||
ephemeralRoot,
|
||||
reqId: safeReqId,
|
||||
envOverrides,
|
||||
hardenedArgs,
|
||||
wrapForLayer3,
|
||||
@@ -441,6 +485,64 @@ export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── TUI-mode helper: seed ephemeral .claude.json ─────────────────────────
|
||||
|
||||
/**
|
||||
* Reads the operator's ~/.claude.json (or tuiSeedSource), strips `projects`,
|
||||
* stamps onboarding/trust/bypass markers, and writes the result to
|
||||
* join(ephemeralRoot, '.claude.json') mode 0o600.
|
||||
*
|
||||
* Called ONLY from prepareIsolatedEnvironment when tui=true. Never called on
|
||||
* the default (stream-json) path.
|
||||
*
|
||||
* Authority: claude CLI v2.1.158 first-run onboarding behavior (spec §7.1);
|
||||
* seed does NOT disable managed MCP (T6 negative control, spec §5.2).
|
||||
*
|
||||
* @param {string} ephemeralRoot — the per-request ephemeral $HOME
|
||||
* @param {string} seedSource — path to read oauthAccount/userID from
|
||||
*/
|
||||
function _seedTuiClaudeJson(ephemeralRoot, seedSource) {
|
||||
const destPath = join(ephemeralRoot, '.claude.json');
|
||||
|
||||
let seedObj;
|
||||
if (existsSync(seedSource)) {
|
||||
let raw;
|
||||
try {
|
||||
raw = JSON.parse(readFileSync(seedSource, 'utf8'));
|
||||
} catch (e) {
|
||||
throw new Error(`[sandbox/manager] Failed to parse TUI seed source ${seedSource}: ${e?.message ?? e}`);
|
||||
}
|
||||
// Strip projects to avoid leaking owner's real project history (§7.1).
|
||||
// Copy everything else (oauthAccount, userID, etc.) and stamp markers.
|
||||
const { projects: _dropped, ...rest } = raw;
|
||||
seedObj = {
|
||||
...rest,
|
||||
hasCompletedOnboarding: true,
|
||||
bypassPermissionsModeAccepted: true,
|
||||
};
|
||||
} else {
|
||||
// Source absent: write minimal seed so the session can still start.
|
||||
// OAuth may fail without a real oauthAccount; operator must supply one.
|
||||
console.warn(
|
||||
`[sandbox/manager] [WARN][tui] TUI seed source not found: ${seedSource}. ` +
|
||||
`Writing minimal seed — interactive claude may fail OAuth without a real ~/.claude.json.`,
|
||||
);
|
||||
seedObj = {
|
||||
hasCompletedOnboarding: true,
|
||||
bypassPermissionsModeAccepted: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(destPath, JSON.stringify(seedObj), { mode: 0o600 });
|
||||
} catch (e) {
|
||||
throw new Error(`[sandbox/manager] Failed to write TUI seed to ${destPath}: ${e?.message ?? e}`);
|
||||
}
|
||||
|
||||
// NEVER log the seed contents — carries oauthAccount/userID (§5.5, §7.1).
|
||||
console.info('[tui] seeded ephemeral .claude.json');
|
||||
}
|
||||
|
||||
// ── Legacy unsandboxed shape ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user