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
@@ -677,4 +677,8 @@ The full test list is captured in ADR 0014 Amendment 1's PR-B-revised test suite
|
|||||||
- **`ALIGNMENT.md` Rule 1 (Cite First)** — every per-field design choice is cited above. Every per-provider concrete instance is cited to the underlying CLI authority.
|
- **`ALIGNMENT.md` Rule 1 (Cite First)** — every per-field design choice is cited above. Every per-provider concrete instance is cited to the underlying CLI authority.
|
||||||
- **`ALIGNMENT.md` Rule 2 (No Invention)** — no invented env vars, no invented CLI flags. The mistral `crossTenantReadProtection: 'none'` declaration is the explicit honest acknowledgment that no protection regime has been established, rather than invention of one.
|
- **`ALIGNMENT.md` Rule 2 (No Invention)** — no invented env vars, no invented CLI flags. The mistral `crossTenantReadProtection: 'none'` declaration is the explicit honest acknowledgment that no protection regime has been established, rather than invention of one.
|
||||||
- **`ALIGNMENT.md` Rule 4 (Unalignable Plugins / Fields Are Deleted)** — see § Rule 4 compliance above for the explicit reasoning that OPTIONAL `ISOLATION` is not "feature-flagging" but rather "honestly transitional."
|
- **`ALIGNMENT.md` Rule 4 (Unalignable Plugins / Fields Are Deleted)** — see § Rule 4 compliance above for the explicit reasoning that OPTIONAL `ISOLATION` is not "feature-flagging" but rather "honestly transitional."
|
||||||
|
|
||||||
|
#### tuiSeed extension note (PR-0 co-merge, 2026-05-30)
|
||||||
|
|
||||||
|
`lib/sandbox/manager.mjs` adds a `tui` opt-in param to `prepareIsolatedEnvironment`. When `tui:true`, the orchestrator (a) chmod 700s the per-reqId dir and ephemeralRoot (spec §5.5 credential-wall), and (b) calls the module-private helper `_seedTuiClaudeJson(ephemeralRoot, seedSource)`. The helper reads `~/.claude.json` (or the override `tuiSeedSource`) once, copies `oauthAccount` and `userID` via object spread, strips `projects` entirely to avoid leaking the operator's real project history (spec §7.1), stamps `hasCompletedOnboarding:true` and `bypassPermissionsModeAccepted:true`, and writes the result to `join(ephemeralRoot, '.claude.json')` at mode 0o600. The seed carries NO MCP-disable weight — it does not set `claudeAiMcpEverConnected` or `mcpServers`; that is the spawn-argv flag `--strict-mcp-config` landed in PR-2 (spec §5.2 T6 negative control). The `projects` field is stripped unconditionally and is NOT re-populated with a pre-trusted `cwd` entry. **Correction to the original plan framing:** `bypassPermissionsModeAccepted:true` suppresses the bypass-permissions acceptance dialog only — it does **NOT** suppress the per-directory **trust-folder** dialog ("Is this a project you trust?"), which still appears for a fresh ephemeral `$HOME`. The pre-code spikes confirmed this (their playbook answers the trust dialog by sending "1"). PR-0 does not pre-trust because the spawn `cwd` is a PR-2 session-driver concern (not known at seed time) and the exact trust-field key is unverified; therefore **PR-2's session driver MUST answer the trust-folder dialog** (the spike-validated approach). Pre-trusting `projects[cwd]` in the seed remains an optional future optimization. Authority: claude CLI v2.1.158 first-run onboarding behavior (spec §7.1); the default (stream-json) code path is byte-for-byte unchanged.
|
||||||
- **`ALIGNMENT.md` Amendment Procedure** — this section (Amendment 9) is the PR-required citation of evidence (the 2026-05-27 incident memory, the ADR 0014 PoC spike report at `/tmp/sandbox-spike/report.md` on PI231) and the structural amendment of the Provider contract documented in this ADR's § Decision.
|
- **`ALIGNMENT.md` Amendment Procedure** — this section (Amendment 9) is the PR-required citation of evidence (the 2026-05-27 incident memory, the ADR 0014 PoC spike report at `/tmp/sandbox-spike/report.md` on PI231) and the structural amendment of the Provider contract documented in this ADR's § Decision.
|
||||||
|
|||||||
@@ -1726,4 +1726,15 @@ export const ISOLATION = {
|
|||||||
// 1). No additional CLI flags are needed at the orchestrator level.
|
// 1). No additional CLI flags are needed at the orchestrator level.
|
||||||
// Per ADR 0002 Amendment 9 § 7: absence means the orchestrator passes args
|
// Per ADR 0002 Amendment 9 § 7: absence means the orchestrator passes args
|
||||||
// through unchanged from spawn().
|
// through unchanged from spawn().
|
||||||
|
|
||||||
|
// TUI-mode reuses this ISOLATION shape unchanged for Layers 1+2.
|
||||||
|
// The .claude.json seed extension (onboarding/trust/bypass markers) is an
|
||||||
|
// orchestrator-side opt-in: the session driver (PR-2/PR-3) calls
|
||||||
|
// prepareIsolatedEnvironment({ ..., tui:true }) to seed the ephemeral home.
|
||||||
|
// The seed logic lives in lib/sandbox/manager.mjs _seedTuiClaudeJson(),
|
||||||
|
// gated on the tui param so the default stream-json path is unchanged.
|
||||||
|
//
|
||||||
|
// NOTE (T6 negative control): the seed does NOT disable managed MCP — that
|
||||||
|
// is the spawn-argv flag --strict-mcp-config (PR-2). See spec §5.2 + §7.1.
|
||||||
|
// Authority: claude CLI v2.1.158 + ADR 0002 Amendment 9 (tuiSeed extension).
|
||||||
};
|
};
|
||||||
|
|||||||
+107
-5
@@ -47,7 +47,7 @@
|
|||||||
* __resetSandboxManagerForTests() — test seam: reset module state
|
* __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 { rm } from 'node:fs/promises';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
@@ -189,18 +189,21 @@ export function isSandboxActive() {
|
|||||||
* returns the legacy unsandboxed shape (identity env, identity hooks, no cleanup).
|
* returns the legacy unsandboxed shape (identity env, identity hooks, no cleanup).
|
||||||
*
|
*
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {object} params.provider — provider plugin object (may have .ISOLATION)
|
* @param {object} params.provider — provider plugin object (may have .ISOLATION)
|
||||||
* @param {string} params.keyId — OLP key identity driving this request
|
* @param {string} params.keyId — OLP key identity driving this request
|
||||||
* @param {string} params.reqId — per-request UUID
|
* @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<{
|
* @returns {Promise<{
|
||||||
* ephemeralRoot: string|null,
|
* ephemeralRoot: string|null,
|
||||||
|
* reqId: string,
|
||||||
* envOverrides: Record<string, string>,
|
* envOverrides: Record<string, string>,
|
||||||
* hardenedArgs: (args: string[]) => string[],
|
* hardenedArgs: (args: string[]) => string[],
|
||||||
* wrapForLayer3: (command: string) => Promise<string>,
|
* wrapForLayer3: (command: string) => Promise<string>,
|
||||||
* cleanup: () => Promise<void>,
|
* cleanup: () => Promise<void>,
|
||||||
* }>}
|
* }>}
|
||||||
*/
|
*/
|
||||||
export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
export async function prepareIsolatedEnvironment({ provider, keyId, reqId, tui = false, tuiSeedSource }) {
|
||||||
const isolation = provider?.ISOLATION;
|
const isolation = provider?.ISOLATION;
|
||||||
|
|
||||||
// ── Test-context bypass ──────────────────────────────────────────────────
|
// ── 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) ────────────────────────────────
|
// ── Compose envOverrides (Layer 1 output) ────────────────────────────────
|
||||||
let envOverrides = {};
|
let envOverrides = {};
|
||||||
if (typeof isolation.ephemeralEnvOverrides === 'function') {
|
if (typeof isolation.ephemeralEnvOverrides === 'function') {
|
||||||
@@ -434,6 +477,7 @@ export async function prepareIsolatedEnvironment({ provider, keyId, reqId }) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ephemeralRoot,
|
ephemeralRoot,
|
||||||
|
reqId: safeReqId,
|
||||||
envOverrides,
|
envOverrides,
|
||||||
hardenedArgs,
|
hardenedArgs,
|
||||||
wrapForLayer3,
|
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 ──────────────────────────────────────────────
|
// ── Legacy unsandboxed shape ──────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18318,3 +18318,209 @@ describe('Suite 44 — sandbox Layer 3 E2E test (PI231 only) [SKIP: awaiting Tas
|
|||||||
assert.ok(true, 'placeholder — real test lands in Task #9');
|
assert.ok(true, 'placeholder — real test lands in Task #9');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Suite 45 — PR-0 TUI ISOLATION seed ────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Tests for the tui opt-in param of prepareIsolatedEnvironment:
|
||||||
|
// T-a default-path-unchanged (tui omitted / false)
|
||||||
|
// T-b tui-seed (tui:true, valid fixture source)
|
||||||
|
// T-c missing-source (tui:true, non-existent source → minimal seed)
|
||||||
|
//
|
||||||
|
// Authority:
|
||||||
|
// spec §7.1 + §5.2 (T6 negative control) — seed does NOT disable managed MCP
|
||||||
|
// spec §5.5 — chmod 700 + mode-600 seed
|
||||||
|
// ADR 0002 Amendment 9 (ISOLATION contract, tuiSeed extension)
|
||||||
|
// claude CLI v2.1.158 first-run onboarding behavior
|
||||||
|
|
||||||
|
import { ISOLATION as _ISOLATION_PR0 } from './lib/providers/anthropic.mjs';
|
||||||
|
import { mkdtempSync as _mkdtemp45, rmSync as _rmSync45, statSync as _statSync45, writeFileSync as _wfs45, existsSync as _exists45, readFileSync as _rfs45 } from 'node:fs';
|
||||||
|
import { join as _join45 } from 'node:path';
|
||||||
|
import { tmpdir as _tmpdir45 } from 'node:os';
|
||||||
|
|
||||||
|
describe('Suite 45 — PR-0 TUI ISOLATION seed', () => {
|
||||||
|
|
||||||
|
// ── Shared: build a mock provider that has the real anthropic ISOLATION shape
|
||||||
|
// (credentialMounts is overridden to [] so we don't symlink ~/.claude/.credentials.json).
|
||||||
|
function _mockAnthropicProvider() {
|
||||||
|
return {
|
||||||
|
name: 'mock-anthropic-pr0',
|
||||||
|
ISOLATION: {
|
||||||
|
..._ISOLATION_PR0,
|
||||||
|
// Override credentialMounts to avoid accessing the real home during tests.
|
||||||
|
credentialMounts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── T-a: default-path-unchanged ──────────────────────────────────────────
|
||||||
|
// With tui omitted (or false), prepareIsolatedEnvironment must NOT write
|
||||||
|
// .claude.json in the ephemeralRoot. The return shape must equal the
|
||||||
|
// existing shape PLUS the additive reqId field.
|
||||||
|
|
||||||
|
it('45a: default-path-unchanged — no .claude.json written when tui is falsy', async () => {
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
globalThis.__OLP_FORCE_ISOLATION_IN_TEST = true;
|
||||||
|
const provider = _mockAnthropicProvider();
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
ctx = await prepareIsolatedEnvironment({
|
||||||
|
provider,
|
||||||
|
keyId: 'test-key-45a',
|
||||||
|
reqId: 'test-req-45a',
|
||||||
|
// tui intentionally omitted (defaults to false)
|
||||||
|
});
|
||||||
|
|
||||||
|
// ephemeralRoot must exist and be under /tmp/olp-spawn/
|
||||||
|
assert.ok(typeof ctx.ephemeralRoot === 'string' && ctx.ephemeralRoot.length > 0,
|
||||||
|
'ephemeralRoot must be a non-empty string');
|
||||||
|
assert.ok(ctx.ephemeralRoot.startsWith('/tmp/olp-spawn/'),
|
||||||
|
`ephemeralRoot must be under /tmp/olp-spawn/; got ${ctx.ephemeralRoot}`);
|
||||||
|
|
||||||
|
// NO .claude.json must be present
|
||||||
|
const claudeJsonPath = _join45(ctx.ephemeralRoot, '.claude.json');
|
||||||
|
assert.equal(_exists45(claudeJsonPath), false,
|
||||||
|
'.claude.json MUST NOT be present on the default (non-tui) path');
|
||||||
|
|
||||||
|
// reqId is present (additive — does not break existing callers who ignore it)
|
||||||
|
assert.ok('reqId' in ctx, 'reqId must be present in the returned ctx');
|
||||||
|
assert.equal(typeof ctx.reqId, 'string', 'reqId must be a string');
|
||||||
|
|
||||||
|
// Return shape has all original fields
|
||||||
|
assert.equal(typeof ctx.envOverrides, 'object', 'envOverrides must be present');
|
||||||
|
assert.equal(typeof ctx.hardenedArgs, 'function', 'hardenedArgs must be a function');
|
||||||
|
assert.equal(typeof ctx.wrapForLayer3, 'function', 'wrapForLayer3 must be a function');
|
||||||
|
assert.equal(typeof ctx.cleanup, 'function', 'cleanup must be a function');
|
||||||
|
|
||||||
|
await ctx.cleanup();
|
||||||
|
} finally {
|
||||||
|
delete globalThis.__OLP_FORCE_ISOLATION_IN_TEST;
|
||||||
|
if (ctx?.cleanup) { try { await ctx.cleanup(); } catch { /* already cleaned */ } }
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T-b: tui-seed ────────────────────────────────────────────────────────
|
||||||
|
// With tui:true + a fixture tuiSeedSource: .claude.json must be written
|
||||||
|
// mode 600, projects stripped, markers present, oauthAccount/userID present.
|
||||||
|
// The <reqId> dir and ephemeralRoot must be mode 700.
|
||||||
|
|
||||||
|
it('45b: tui-seed — .claude.json written mode 600, markers present, projects stripped, dirs mode 700', async () => {
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
globalThis.__OLP_FORCE_ISOLATION_IN_TEST = true;
|
||||||
|
|
||||||
|
// Write a fixture .claude.json to a temp dir (never touches real ~/.claude.json).
|
||||||
|
const fixtureDir = _mkdtemp45(_join45(_tmpdir45(), 'olp-pr0-fixture-'));
|
||||||
|
const fixtureSeedPath = _join45(fixtureDir, 'fixture-claude.json');
|
||||||
|
const fixtureSource = {
|
||||||
|
oauthAccount: { emailAddress: 'test@example.com', organizationUuid: 'org-uuid-fake' },
|
||||||
|
userID: 'user-id-fake',
|
||||||
|
projects: { '/some/real/project': { hasTrustDialogAccepted: true } },
|
||||||
|
someOtherField: 'keep-me',
|
||||||
|
};
|
||||||
|
_wfs45(fixtureSeedPath, JSON.stringify(fixtureSource), 'utf8');
|
||||||
|
|
||||||
|
const provider = _mockAnthropicProvider();
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
ctx = await prepareIsolatedEnvironment({
|
||||||
|
provider,
|
||||||
|
keyId: 'test-key-45b',
|
||||||
|
reqId: 'test-req-45b',
|
||||||
|
tui: true,
|
||||||
|
tuiSeedSource: fixtureSeedPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// .claude.json must exist
|
||||||
|
const claudeJsonPath = _join45(ctx.ephemeralRoot, '.claude.json');
|
||||||
|
assert.equal(_exists45(claudeJsonPath), true,
|
||||||
|
'.claude.json must be present when tui:true');
|
||||||
|
|
||||||
|
// mode 600
|
||||||
|
const seedMode = _statSync45(claudeJsonPath).mode & 0o777;
|
||||||
|
assert.equal(seedMode, 0o600,
|
||||||
|
`.claude.json must have mode 0o600; got ${seedMode.toString(8)}`);
|
||||||
|
|
||||||
|
// Parse and check content
|
||||||
|
const seedContent = JSON.parse(_rfs45(claudeJsonPath, 'utf8'));
|
||||||
|
assert.equal(seedContent.hasCompletedOnboarding, true,
|
||||||
|
'hasCompletedOnboarding must be true');
|
||||||
|
assert.equal(seedContent.bypassPermissionsModeAccepted, true,
|
||||||
|
'bypassPermissionsModeAccepted must be true');
|
||||||
|
assert.equal('projects' in seedContent, false,
|
||||||
|
'projects must NOT be present in the seed (stripped to avoid leaking owner history)');
|
||||||
|
assert.deepEqual(seedContent.oauthAccount, fixtureSource.oauthAccount,
|
||||||
|
'oauthAccount must be copied from source');
|
||||||
|
assert.equal(seedContent.userID, fixtureSource.userID,
|
||||||
|
'userID must be copied from source');
|
||||||
|
assert.equal(seedContent.someOtherField, 'keep-me',
|
||||||
|
'other fields must be preserved');
|
||||||
|
|
||||||
|
// Negative assertion: no MCP-disable fields (T6 negative control)
|
||||||
|
assert.equal('claudeAiMcpEverConnected' in seedContent, false,
|
||||||
|
'seed must NOT contain claudeAiMcpEverConnected (MCP disable is spawn-argv, not seed)');
|
||||||
|
assert.equal('mcpServers' in seedContent, false,
|
||||||
|
'seed must NOT contain mcpServers');
|
||||||
|
|
||||||
|
// reqId present
|
||||||
|
assert.ok('reqId' in ctx, 'reqId must be present');
|
||||||
|
assert.equal(ctx.reqId, 'test-req-45b', 'reqId must match the sanitized input');
|
||||||
|
|
||||||
|
// <reqId> dir and ephemeralRoot must be mode 700 (§5.5)
|
||||||
|
const reqIdDir = _join45('/tmp/olp-spawn', 'test-key-45b', 'test-req-45b');
|
||||||
|
const reqIdMode = _statSync45(reqIdDir).mode & 0o777;
|
||||||
|
assert.equal(reqIdMode, 0o700,
|
||||||
|
`<reqId> dir must have mode 0o700; got ${reqIdMode.toString(8)}`);
|
||||||
|
const rootMode = _statSync45(ctx.ephemeralRoot).mode & 0o777;
|
||||||
|
assert.equal(rootMode, 0o700,
|
||||||
|
`ephemeralRoot must have mode 0o700; got ${rootMode.toString(8)}`);
|
||||||
|
|
||||||
|
await ctx.cleanup();
|
||||||
|
} finally {
|
||||||
|
delete globalThis.__OLP_FORCE_ISOLATION_IN_TEST;
|
||||||
|
if (ctx?.cleanup) { try { await ctx.cleanup(); } catch { /* already cleaned */ } }
|
||||||
|
try { _rmSync45(fixtureDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T-c: missing-source ──────────────────────────────────────────────────
|
||||||
|
// With tui:true and a non-existent tuiSeedSource, _seedTuiClaudeJson should
|
||||||
|
// write a minimal seed (markers only, no oauthAccount) and NOT throw.
|
||||||
|
|
||||||
|
it('45c: missing-source — minimal seed written when tuiSeedSource does not exist', async () => {
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
globalThis.__OLP_FORCE_ISOLATION_IN_TEST = true;
|
||||||
|
|
||||||
|
const provider = _mockAnthropicProvider();
|
||||||
|
const nonExistentSource = _join45(_tmpdir45(), 'olp-pr0-nonexistent-' + Date.now() + '.json');
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
// Must not throw even though source is absent
|
||||||
|
ctx = await prepareIsolatedEnvironment({
|
||||||
|
provider,
|
||||||
|
keyId: 'test-key-45c',
|
||||||
|
reqId: 'test-req-45c',
|
||||||
|
tui: true,
|
||||||
|
tuiSeedSource: nonExistentSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
const claudeJsonPath = _join45(ctx.ephemeralRoot, '.claude.json');
|
||||||
|
assert.equal(_exists45(claudeJsonPath), true,
|
||||||
|
'minimal seed must be written even when source is absent');
|
||||||
|
|
||||||
|
const seedContent = JSON.parse(_rfs45(claudeJsonPath, 'utf8'));
|
||||||
|
assert.equal(seedContent.hasCompletedOnboarding, true,
|
||||||
|
'minimal seed must have hasCompletedOnboarding:true');
|
||||||
|
assert.equal(seedContent.bypassPermissionsModeAccepted, true,
|
||||||
|
'minimal seed must have bypassPermissionsModeAccepted:true');
|
||||||
|
assert.equal('oauthAccount' in seedContent, false,
|
||||||
|
'minimal seed must NOT have oauthAccount (source was absent)');
|
||||||
|
|
||||||
|
await ctx.cleanup();
|
||||||
|
} finally {
|
||||||
|
delete globalThis.__OLP_FORCE_ISOLATION_IN_TEST;
|
||||||
|
if (ctx?.cleanup) { try { await ctx.cleanup(); } catch { /* already cleaned */ } }
|
||||||
|
await _resetSandboxMgr();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user