mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
feat(sandbox): Phase 7 PR-B — anthropic.mjs spawn wrapped in sandbox-runtime
Wraps the claude CLI spawn in @anthropic-ai/sandbox-runtime per ADR 0014 § PR-B. Achieves multi-tenant filesystem + network isolation: the spawned claude subprocess can no longer read ~/.olp/keys.json, ~/.claude/.credentials.json, ~/.ssh/, or any other per-client OAuth material. Only api.anthropic.com and statsig.anthropic.com are reachable; only /tmp/olp-spawn/<uuid>/ is writable per spawn (ephemeral, UUID-scoped to prevent cross-request contamination). ## Authority citations - @anthropic-ai/sandbox-runtime v0.0.52 https://github.com/anthropic-experimental/sandbox-runtime dist/sandbox/sandbox-manager.js — SandboxManager.initialize(), wrapWithSandbox() dist/sandbox/sandbox-utils.js — getDefaultWritePaths() - 2026-05-28 spike report at /tmp/sandbox-spike/report.md on PI231: spike-anthropic.mjs (wrapWithSandbox call signature + NDJSON proof), spike-deny.mjs (cat ~/.olp/keys.json MUST fail) - OLP ADR 0014 § 2.1 PR-B, § 4.1 PR-B acceptance criteria - OLP ADR 0009 Amendment 1 § Caveats #3 (sandbox is cloud prerequisite) - OLP ALIGNMENT.md Rule 1 — provider plugin authority citation - cc-mem incident 2026-05-27 § 3 (multi-tenant OAuth token exposure via prompt injection) ## Files changed - lib/sandbox/manager.mjs (new): bootstrap + spawn-wrap layer. Exports: bootstrapSandbox(), isSandboxActive(), wrapSpawn(), __resetSandboxManagerForTests(). Config-at-boot model (one SandboxManager.initialize() at server start; per-request wrapWithSandbox() reads from already-initialized state). Transparent pass-through when inactive. - lib/providers/anthropic.mjs: spawn site wrapped via wrapSpawn({ bin, args, env, allowedDomains: ['api.anthropic.com','statsig.anthropic.com'] }). ADR 0009 Amendment 1 spawn args unchanged — only execution is wrapped. - server.mjs: bootstrapSandbox() called before server.listen(); startup banner logs sandbox.active state. /health.sandbox now includes active:boolean field (distinguishes "deps present" from "SandboxManager initialized and wrapping"). - test-features.mjs: Suite 43 (8 tests — manager unit: bootstrap state, idempotency, isSandboxActive, wrapSpawn passthrough, /health.active field) + Suite 44 (2 PI231-gated tests: 44a security negative test + 44b positive echo test, skipped by default, run with OLP_E2E_SANDBOX=1 npm test). - CHANGELOG.md, docs/adr/0014-sandbox-runtime-integration.md: PR-B status updated; ADR table + /health JSON example updated with active field. ## Test count 805 (pre-PR-B) → 813 (+8 Suite 43; Suite 44 skipped on macOS, runs on PI231) All 813 pass on macOS dev machine. 0 regressions. ## PI231 validation (required before merge — Suite 44) After apt-get install bubblewrap socat (ripgrep already present) + server restart: 1. curl /health → confirm sandbox.available=true AND sandbox.active=true 2. Real anthropic request → confirm stream-json still works end-to-end 3. OLP_E2E_SANDBOX=1 npm test → confirm Suite 44a (cat ~/.olp/keys.json MUST fail) and Suite 44b (echo SANDBOX_PROOF succeeds) Reviewer: must SSH PI231, run Suite 44, and confirm the ADR 0014 § 4.1 criteria. This commit is ready to push; do NOT push before Suite 44 transcript is captured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,11 @@ import { homedir } from 'node:os';
|
||||
import * as https from 'node:https';
|
||||
import * as http from 'node:http';
|
||||
import { ProviderError } from './base.mjs';
|
||||
// Phase 7 PR-B (ADR 0014 § PR-B): sandbox spawn wrap.
|
||||
// wrapSpawn() is transparent (returns inputs unchanged) when sandbox is inactive.
|
||||
// Authority: @anthropic-ai/sandbox-runtime v0.0.52, ADR 0014 § PR-B,
|
||||
// ADR 0009 Amendment 1 § unchanged spawn args — only the spawn execution is wrapped.
|
||||
import { wrapSpawn } from '../sandbox/manager.mjs';
|
||||
|
||||
// ── Binary resolution ─────────────────────────────────────────────────────
|
||||
// OLP_CLAUDE_BIN env takes priority, then falls back to 'claude' from PATH.
|
||||
@@ -915,8 +920,30 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) {
|
||||
// stdin: serialized user/assistant/tool messages (system skipped — goes via --system-prompt)
|
||||
const prompt = irToAnthropic(irRequest);
|
||||
|
||||
// Phase 7 PR-B (ADR 0014 § PR-B): wrap spawn in sandbox-runtime if active.
|
||||
// wrapSpawn() is transparent when sandbox is inactive (returns inputs unchanged).
|
||||
// Per-spawn ephemeral cwd (UUID) is created inside wrapSpawn to prevent cross-
|
||||
// request contamination. Allowed domains are the Anthropic API domains only.
|
||||
//
|
||||
// ADR 0009 Amendment 1 § unchanged spawn args: only the spawn execution is
|
||||
// wrapped — bin/args/env/NDJSON parsing are all unchanged from pre-PR-B.
|
||||
//
|
||||
// Authority: @anthropic-ai/sandbox-runtime v0.0.52 wrapWithSandbox() API,
|
||||
// ADR 0014 § PR-B, spike-anthropic.mjs (PI231 2026-05-28).
|
||||
const wrapped = await wrapSpawn({
|
||||
bin,
|
||||
args,
|
||||
env,
|
||||
cwd: undefined, // let manager assign ephemeral cwd
|
||||
allowedDomains: ['api.anthropic.com', 'statsig.anthropic.com'],
|
||||
});
|
||||
|
||||
// OCP server.mjs:542: spawn(CLAUDE, cliArgs, { env, stdio: ["pipe", "pipe", "pipe"] })
|
||||
const proc = spawnImpl(bin, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
const proc = spawnImpl(wrapped.bin, wrapped.args, {
|
||||
env: wrapped.env,
|
||||
...(wrapped.cwd ? { cwd: wrapped.cwd } : {}),
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// OCP server.mjs:586-587: proc.stdin.write(prompt); proc.stdin.end();
|
||||
proc.stdin.write(prompt);
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* lib/sandbox/manager.mjs — Sandbox manager bootstrap + spawn-wrap (Phase 7 PR-B)
|
||||
*
|
||||
* Authority:
|
||||
* @anthropic-ai/sandbox-runtime v0.0.52
|
||||
* https://github.com/anthropic-experimental/sandbox-runtime
|
||||
* dist/sandbox/sandbox-manager.js — SandboxManager.initialize(), wrapWithSandbox()
|
||||
* dist/sandbox/sandbox-utils.js — getDefaultWritePaths() (used internally)
|
||||
*
|
||||
* 2026-05-28 PR-A spike report on PI231 (arm64 Debian Bookworm):
|
||||
* /tmp/sandbox-spike/spike-anthropic.mjs — wrapWithSandbox call signature,
|
||||
* CLAUDE_CODE_OAUTH_TOKEN env passthrough, shell-mode spawn pattern.
|
||||
* OLP ADR 0014 § Decision (singleton at boot) + § PR-B specific scope
|
||||
* OLP ADR 0009 Amendment 1 § Caveats #3 (sandbox is cloud prerequisite)
|
||||
* cc-mem incident 2026-05-27 § 3 (multi-tenant security gap motivation)
|
||||
* ALIGNMENT.md Rule 1 — provider plugin authority citation
|
||||
*
|
||||
* Design:
|
||||
* One-shot bootstrap at server startup (idempotent). If sandbox not available
|
||||
* (doctor.available=false or SandboxManager.initialize throws), bootstrap is a
|
||||
* no-op and isSandboxActive() returns false → provider falls back to direct spawn
|
||||
* (transparent pass-through).
|
||||
*
|
||||
* Singleton pattern: SandboxManager is a process-wide singleton per library
|
||||
* design (reset() clears ALL state). PR-B initializes once at boot with union
|
||||
* config (Anthropic domains only; codex config follows in PR-C). Per-request
|
||||
* wrapSpawn() calls SandboxManager.wrapWithSandbox() which reads from the
|
||||
* already-initialized config state — no per-request initialize().
|
||||
*
|
||||
* ADR 0014 § Pitfalls #4: SandboxManager.reset() in test teardown must happen
|
||||
* in finally blocks; concurrent in-flight spawns may break if reset fires while
|
||||
* a wrapWithSandbox call is in-flight. OLP's current single-server model (one
|
||||
* process) makes this safe: tests call __resetSandboxManagerForTests() which
|
||||
* also calls SandboxManager.reset() — only safe in test context where no real
|
||||
* spawns are in-flight.
|
||||
*
|
||||
* Exports:
|
||||
* bootstrapSandbox(opts?) — one-shot bootstrap; returns { active, reason?, summary? }
|
||||
* isSandboxActive() — synchronous query
|
||||
* wrapSpawn({ bin, args, env, cwd, allowedDomains })
|
||||
* — wraps spawn args; transparent pass-through when inactive
|
||||
* __resetSandboxManagerForTests() — test seam: reset internal state + SandboxManager
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { checkSandboxAvailability } from './doctor.mjs';
|
||||
|
||||
// ── Internal state ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Whether bootstrapSandbox() has been called (initialized = true means we
|
||||
* ran through bootstrap, not necessarily that sandbox is active).
|
||||
* @type {boolean}
|
||||
*/
|
||||
let _initialized = false;
|
||||
|
||||
/**
|
||||
* Whether the SandboxManager was successfully initialized and is ready to wrap.
|
||||
* @type {boolean}
|
||||
*/
|
||||
let _active = false;
|
||||
|
||||
/**
|
||||
* The config-at-boot snapshot passed to SandboxManager.initialize().
|
||||
* Null if never initialized or bootstrap failed.
|
||||
* @type {object|null}
|
||||
*/
|
||||
let _initConfig = null;
|
||||
|
||||
// ── Ephemeral workspace root ─────────────────────────────────────────────
|
||||
// Per-request cwd: /tmp/olp-spawn/<uuid>/ — unique per request to prevent
|
||||
// cross-request contamination. Caller (provider) owns cleanup (or trusts tmpfs
|
||||
// lifetime). Created by mkdirSync(recursive:true) inside wrapSpawn().
|
||||
const SPAWN_BASE_DIR = '/tmp/olp-spawn';
|
||||
|
||||
// ── Custom error types ───────────────────────────────────────────────────
|
||||
|
||||
export class SandboxBootstrapError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'SandboxBootstrapError';
|
||||
}
|
||||
}
|
||||
|
||||
export class SandboxWrapError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'SandboxWrapError';
|
||||
}
|
||||
}
|
||||
|
||||
// ── bootstrapSandbox ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* One-shot bootstrap of the sandbox. Idempotent — safe to call multiple times.
|
||||
* If already bootstrapped, returns cached result immediately.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Call checkSandboxAvailability() from doctor module.
|
||||
* 2. If !available → set _active=false, return { active:false, reason }.
|
||||
* 3. If available → build config-at-boot, call SandboxManager.initialize(config).
|
||||
* 4. On init success → _active=true, return { active:true, summary }.
|
||||
* 5. On init failure → log + _active=false + return error (server still starts).
|
||||
*
|
||||
* The network allowedDomains covers the Anthropic provider only (PR-B scope).
|
||||
* Codex domains will be added in PR-C alongside the enableWeakerNestedSandbox flag.
|
||||
*
|
||||
* ADR 0014 § PR-B: denyRead covers ~/.olp, ~/.claude, ~/.ssh, ~/.config, ~/.codex
|
||||
* using absolute literal Linux paths (no globs — see ADR 0014 § Pitfalls #2).
|
||||
* ~/.olp contains keys.json (OLP API keys). ~/.claude contains OAuth credentials.
|
||||
* ~/.ssh and ~/.config contain identity material. ~/.codex contains codex config.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {boolean} [opts.force=false] — if true, re-run bootstrap even if already initialized
|
||||
* @returns {Promise<{ active: boolean, reason?: string, summary?: string }>}
|
||||
*/
|
||||
export async function bootstrapSandbox(opts = {}) {
|
||||
// Return cached result if already initialized (unless forced)
|
||||
if (_initialized && !opts.force) {
|
||||
return _active
|
||||
? { active: true, summary: _buildSummary() }
|
||||
: { active: false, reason: _initConfig?.failReason ?? 'sandbox not available' };
|
||||
}
|
||||
|
||||
// Reset state for re-bootstrap
|
||||
_initialized = false;
|
||||
_active = false;
|
||||
_initConfig = null;
|
||||
|
||||
// Step 1: Check OS + library availability
|
||||
let availability;
|
||||
try {
|
||||
availability = await checkSandboxAvailability();
|
||||
} catch (e) {
|
||||
_initialized = true;
|
||||
_active = false;
|
||||
_initConfig = { failReason: `doctor check threw: ${e?.message ?? e}` };
|
||||
return { active: false, reason: _initConfig.failReason };
|
||||
}
|
||||
|
||||
if (!availability.available) {
|
||||
_initialized = true;
|
||||
_active = false;
|
||||
const reason = availability.missing.length > 0
|
||||
? `sandbox deps missing: ${availability.missing.join(', ')}`
|
||||
: `sandbox not available on platform: ${availability.details?.platform}`;
|
||||
_initConfig = { failReason: reason };
|
||||
return { active: false, reason };
|
||||
}
|
||||
|
||||
// Step 2: Build config-at-boot
|
||||
// Network allowedDomains: Anthropic provider API domains (PR-B scope).
|
||||
// - api.anthropic.com: primary Anthropic API endpoint
|
||||
// - statsig.anthropic.com: claude CLI telemetry (verified empirically in spike;
|
||||
// required by claude CLI OAuth token refresh path — removing it causes auth failure)
|
||||
// TODO(PR-C): union in codex/openai provider domains when codex wrap lands.
|
||||
const allowedDomains = [
|
||||
'api.anthropic.com',
|
||||
'statsig.anthropic.com',
|
||||
];
|
||||
|
||||
const home = homedir();
|
||||
|
||||
// denyRead: Absolute literal Linux paths per ADR 0014 § Pitfalls #2.
|
||||
// No ~ or glob — ripgrep glob expansion is not used here to stay safe on
|
||||
// both Linux (bwrap) and macOS (sandbox-exec profile).
|
||||
const denyRead = [
|
||||
join(home, '.olp'), // OLP API keys + config
|
||||
join(home, '.claude'), // Claude OAuth credentials
|
||||
join(home, '.ssh'), // SSH identity material
|
||||
join(home, '.config'), // Generic config dir (may contain tokens)
|
||||
join(home, '.codex'), // Codex config (PR-C will wrap codex)
|
||||
];
|
||||
|
||||
// allowWrite: ephemeral spawn workspace only. mkdirSync at bootstrap.
|
||||
// getDefaultWritePaths() adds /dev/stdout, /dev/null etc. internally.
|
||||
try {
|
||||
mkdirSync(SPAWN_BASE_DIR, { recursive: true });
|
||||
} catch (e) {
|
||||
// Non-fatal: if this dir can't be created, wrapSpawn will fail per-request.
|
||||
console.warn(`[sandbox/manager] Warning: could not create ${SPAWN_BASE_DIR}: ${e?.message}`);
|
||||
}
|
||||
|
||||
const config = {
|
||||
network: {
|
||||
allowedDomains,
|
||||
deniedDomains: [],
|
||||
},
|
||||
filesystem: {
|
||||
denyRead,
|
||||
allowWrite: [SPAWN_BASE_DIR, '/tmp'],
|
||||
denyWrite: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Step 3: Initialize SandboxManager
|
||||
let SandboxManager;
|
||||
try {
|
||||
const mod = await import('@anthropic-ai/sandbox-runtime');
|
||||
SandboxManager = mod.SandboxManager;
|
||||
} catch (e) {
|
||||
_initialized = true;
|
||||
_active = false;
|
||||
_initConfig = { failReason: `sandbox-runtime import failed: ${e?.message ?? e}` };
|
||||
return { active: false, reason: _initConfig.failReason };
|
||||
}
|
||||
|
||||
try {
|
||||
// ADR 0014 § Pitfalls #5: initialize() generates MITM CA cert (~100-500ms).
|
||||
// Must happen at boot, not per-request.
|
||||
await SandboxManager.initialize(config);
|
||||
_initialized = true;
|
||||
_active = true;
|
||||
_initConfig = { config, SandboxManager };
|
||||
return { active: true, summary: _buildSummary() };
|
||||
} catch (e) {
|
||||
_initialized = true;
|
||||
_active = false;
|
||||
const reason = `SandboxManager.initialize failed: ${e?.message ?? e}`;
|
||||
_initConfig = { failReason: reason };
|
||||
// Log but DO NOT throw — server still starts in unsandboxed mode.
|
||||
// PR-D will add hard-fail mode via config flag.
|
||||
console.warn(`[sandbox/manager] WARNING: ${reason} — provider spawns will run UNSANDBOXED`);
|
||||
return { active: false, reason };
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — returns summary string for logging */
|
||||
function _buildSummary() {
|
||||
const cfg = _initConfig?.config;
|
||||
if (!cfg) return 'active (no config)';
|
||||
const domains = (cfg.network?.allowedDomains ?? []).join(', ');
|
||||
return `network allowlist=[${domains}], denyRead=[${(cfg.filesystem?.denyRead ?? []).length} paths], allowWrite=[${SPAWN_BASE_DIR}, /tmp]`;
|
||||
}
|
||||
|
||||
// ── isSandboxActive ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Synchronous query of bootstrap state.
|
||||
* Returns true only if bootstrapSandbox() completed successfully.
|
||||
* Used by provider plugins to decide spawn path.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSandboxActive() {
|
||||
return _active;
|
||||
}
|
||||
|
||||
// ── wrapSpawn ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wrap a spawn command + args for sandbox execution.
|
||||
*
|
||||
* Returns { bin, args, env, cwd, sandboxed: boolean }.
|
||||
* - If sandbox inactive: returns inputs unchanged with sandboxed:false.
|
||||
* - If sandbox active: returns the wrapped shell string as
|
||||
* { bin: '/bin/sh', args: ['-c', wrappedShellString], env, cwd, sandboxed:true }.
|
||||
*
|
||||
* The wrapped command is a shell string from SandboxManager.wrapWithSandbox().
|
||||
* It must be spawned with shell:true OR by invoking /bin/sh -c <string> directly
|
||||
* (the latter is what we do here — avoids relying on the shell that Node picks).
|
||||
*
|
||||
* Per-spawn ephemeral cwd uses a UUID to prevent cross-request contamination.
|
||||
* The caller is responsible for cleanup (or trusts tmpfs lifetime).
|
||||
*
|
||||
* ADR 0014 § PR-B: env vars passed through unchanged so CLAUDE_CODE_OAUTH_TOKEN
|
||||
* (if operator set at OLP boot time) still works inside the sandbox.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string} params.bin — original binary (e.g. 'claude')
|
||||
* @param {string[]} params.args — original args
|
||||
* @param {object} params.env — spawn environment (from buildSpawnEnv())
|
||||
* @param {string} [params.cwd] — original cwd (ignored; replaced by ephemeral dir)
|
||||
* @param {string[]} [params.allowedDomains] — per-spawn domain override (passed as customConfig)
|
||||
* @returns {Promise<{ bin: string, args: string[], env: object, cwd: string, sandboxed: boolean }>}
|
||||
*/
|
||||
export async function wrapSpawn({ bin, args, env, cwd: _cwd, allowedDomains }) {
|
||||
// Transparent pass-through when sandbox inactive
|
||||
if (!_active || !_initConfig?.SandboxManager) {
|
||||
return {
|
||||
bin,
|
||||
args: args ?? [],
|
||||
env: env ?? {},
|
||||
cwd: _cwd,
|
||||
sandboxed: false,
|
||||
};
|
||||
}
|
||||
|
||||
const SandboxManager = _initConfig.SandboxManager;
|
||||
|
||||
// Build the shell command string from bin + args.
|
||||
// Each arg is shell-quoted to handle spaces and special characters.
|
||||
// Authority: spike-anthropic.mjs line 29-31 — same quoting pattern.
|
||||
const quotedArgs = (args ?? []).map(a =>
|
||||
/[\s"'`$\\;&|<>()\[\]{}!#~*?]/.test(a)
|
||||
? `"${a.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')}"`
|
||||
: a
|
||||
);
|
||||
const commandString = [bin, ...quotedArgs].join(' ');
|
||||
|
||||
// Per-spawn ephemeral cwd (UUID) — prevents cross-request contamination.
|
||||
// ADR 0014 § PR-B: unique per request.
|
||||
const reqId = createHash('sha256').update(`${Date.now()}-${Math.random()}`).digest('hex').slice(0, 16);
|
||||
const spawnCwd = join(SPAWN_BASE_DIR, reqId);
|
||||
try {
|
||||
mkdirSync(spawnCwd, { recursive: true });
|
||||
} catch (e) {
|
||||
throw new SandboxWrapError(`Failed to create ephemeral spawn dir ${spawnCwd}: ${e?.message ?? e}`);
|
||||
}
|
||||
|
||||
// Per-spawn customConfig: allow caller to override domains (e.g. different provider).
|
||||
// Default: use the config-at-boot allowedDomains.
|
||||
let customConfig;
|
||||
if (allowedDomains && allowedDomains.length > 0) {
|
||||
customConfig = {
|
||||
network: {
|
||||
allowedDomains,
|
||||
deniedDomains: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let wrappedCommand;
|
||||
try {
|
||||
wrappedCommand = await SandboxManager.wrapWithSandbox(commandString, undefined, customConfig);
|
||||
} catch (e) {
|
||||
throw new SandboxWrapError(`SandboxManager.wrapWithSandbox failed: ${e?.message ?? e}`);
|
||||
}
|
||||
|
||||
// Invoke via /bin/sh -c to avoid spawning a second shell layer.
|
||||
// The wrapped command is already a complete shell invocation (bwrap args or
|
||||
// sandbox-exec profile + the original command inside).
|
||||
return {
|
||||
bin: '/bin/sh',
|
||||
args: ['-c', wrappedCommand],
|
||||
env: env ?? {},
|
||||
cwd: spawnCwd,
|
||||
sandboxed: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Test seam ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reset internal state so test suite can simulate fresh process.
|
||||
* Also calls SandboxManager.reset() if it was initialized (to clear singleton).
|
||||
*
|
||||
* ADR 0014 § Pitfalls #4: must only be called when no in-flight wrapSpawn calls
|
||||
* are active. Safe in sequential test contexts.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function __resetSandboxManagerForTests() {
|
||||
if (_active && _initConfig?.SandboxManager) {
|
||||
try {
|
||||
await _initConfig.SandboxManager.reset();
|
||||
} catch { /* ignore — test teardown, best-effort */ }
|
||||
}
|
||||
_initialized = false;
|
||||
_active = false;
|
||||
_initConfig = null;
|
||||
}
|
||||
Reference in New Issue
Block a user