mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
feat(sandbox): Phase 7 PR-A — @anthropic-ai/sandbox-runtime dep + doctor + ADR 0014
Lays the foundation for multi-tenant provider spawning isolation per ADR 0014 § Decision. NO production wiring — PR-B (anthropic.mjs spawn wrap) lands separately and requires bubblewrap + socat + ripgrep installed on PI231 first. Files: - package.json: add @anthropic-ai/sandbox-runtime ^0.0.52 - lib/sandbox/doctor.mjs (new): preflight checkSandboxAvailability + describeSandboxStatus; pure module, no state, no initialize() call - server.mjs: /health response gains 'sandbox' field with availability + missing deps + install hint; result memoized via _sandboxStatusCache; __resetSandboxStatusCache() test seam exported - docs/adr/0014-sandbox-runtime-integration.md (new): 4-PR layered rollout decision per Iron Rule 11; PR-B/C/D acceptance criteria previewed; 6 spike pitfalls recorded; 282 lines - CHANGELOG.md: Unreleased entry under Phase 7 PR-A - test-features.mjs Suite 42 (+8 tests, 797 → 805, all pass) Authority: - @anthropic-ai/sandbox-runtime v0.0.52 — anthropic-experimental org https://github.com/anthropic-experimental/sandbox-runtime - 2026-05-28 PoC spike (verdict YELLOW; report at /tmp/sandbox-spike/ on PI231): clean arm64 install, dep check fail-closed behaviour confirmed, three PoC scripts parked - cc-mem incident memory § 4 (prior-art search — ecosystem hasn't solved multi-tenant fs/tool isolation) - ADR 0009 Amendment 1 § Caveats #3 — sandbox is cloud prerequisite - docs/plans/cloud-deployment-family.md § 5 Spike note (macOS): on dev Mac mini with rg via Homebrew, SandboxManager.isSupportedPlatform()=true and checkDependencies() returns no errors — macOS uses built-in sandbox-exec, not bwrap. /health.sandbox.available=true on macOS dev, false on PI231 until apt install. Operational follow-ups (NOT this PR): 1. sudo apt-get install -y bubblewrap socat ripgrep on PI231 (5-min window) 2. PR-B: lib/providers/anthropic.mjs spawn wrap + negative test (in-sandbox cat of OAuth token MUST fail) 3. PR-C: lib/providers/codex.mjs wrap with enableWeakerNestedSandbox:true 4. PR-D: cloud deployment plan update + unblock Tests: 797 → 805 (all pass, 0 fail). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+57
@@ -70,6 +70,11 @@ import {
|
||||
ENV_OWNER_KEY_ID,
|
||||
} from './lib/keys.mjs';
|
||||
import { appendAuditEvent } from './lib/audit.mjs';
|
||||
// Phase 7 / PR-A — sandbox availability preflight module (ADR 0014).
|
||||
// checkSandboxAvailability is called lazily at first /health hit and memoized
|
||||
// process-wide (bwrap/socat install state does not change at runtime; we don't
|
||||
// want a child_process.execFileSync per /health call).
|
||||
import { checkSandboxAvailability } from './lib/sandbox/doctor.mjs';
|
||||
// Phase 3 / D50 — management endpoints consume the audit aggregate query layer.
|
||||
// D81 (Phase 5) — adds aggregateProviderQuota for quota_v2 shape.
|
||||
import {
|
||||
@@ -221,6 +226,20 @@ export function __resetRequestCounters() {
|
||||
_activeRequests = 0;
|
||||
}
|
||||
|
||||
// ── Phase 7 PR-A: sandbox availability cache ──────────────────────────────
|
||||
// checkSandboxAvailability() forks `which bwrap` / `which socat` / `which rg`
|
||||
// and imports @anthropic-ai/sandbox-runtime. Neither can change at runtime —
|
||||
// bwrap is either installed or it isn't. Memoize the first result to avoid
|
||||
// repeated child_process.execFileSync calls on every /health hit.
|
||||
//
|
||||
// _sandboxStatusCache: null → not yet fetched
|
||||
// object → memoized result from checkSandboxAvailability()
|
||||
let _sandboxStatusCache = null;
|
||||
/** @internal — test seam: reset sandbox cache between tests. */
|
||||
export function __resetSandboxStatusCache() {
|
||||
_sandboxStatusCache = null;
|
||||
}
|
||||
|
||||
// ── Startup config ────────────────────────────────────────────────────────
|
||||
// Read ~/.olp/config.json once at startup. Provides:
|
||||
// - providers.enabled → which providers are loaded (ADR 0002 § Disable model)
|
||||
@@ -859,10 +878,48 @@ async function handleHealth(req, res) {
|
||||
providerStatuses[name] = { ok: false, error: e.message, activeSpawns };
|
||||
}
|
||||
}
|
||||
// Phase 7 PR-A (ADR 0014): sandbox availability field.
|
||||
// Result is memoized process-wide in _sandboxStatusCache — bwrap/socat
|
||||
// install state does not change at runtime. If the library call throws for
|
||||
// any reason, the field is still included with available: false + error
|
||||
// (don't crash /health).
|
||||
if (_sandboxStatusCache === null) {
|
||||
try {
|
||||
_sandboxStatusCache = await checkSandboxAvailability();
|
||||
} catch (e) {
|
||||
_sandboxStatusCache = {
|
||||
available: false,
|
||||
missing: [],
|
||||
details: { platform: process.platform, error: String(e?.message ?? e) },
|
||||
};
|
||||
}
|
||||
}
|
||||
const sandboxField = {
|
||||
available: _sandboxStatusCache.available,
|
||||
missing: _sandboxStatusCache.missing ?? [],
|
||||
platform: _sandboxStatusCache.details?.platform ?? process.platform,
|
||||
};
|
||||
if (!_sandboxStatusCache.available) {
|
||||
// Include human-readable install hint for owner-tier callers.
|
||||
const missingDeps = (_sandboxStatusCache.missing ?? []).filter(
|
||||
m => m === 'bubblewrap' || m === 'socat' || m === 'ripgrep',
|
||||
);
|
||||
if (missingDeps.length > 0) {
|
||||
sandboxField.message =
|
||||
`Sandbox dependencies not available: ${missingDeps.map(m => `${m} not installed`).join(', ')}.` +
|
||||
` Install: sudo apt-get install -y ${missingDeps.join(' ')}`;
|
||||
} else if (_sandboxStatusCache.details?.error) {
|
||||
sandboxField.message = `Sandbox check error: ${_sandboxStatusCache.details.error}`;
|
||||
} else if (_sandboxStatusCache.details?.libError) {
|
||||
sandboxField.message = `Sandbox library error: ${_sandboxStatusCache.details.libError}`;
|
||||
}
|
||||
}
|
||||
|
||||
const fullPayload = {
|
||||
ok: true,
|
||||
version: VERSION,
|
||||
providers: { enabled, available, status: providerStatuses },
|
||||
sandbox: sandboxField,
|
||||
};
|
||||
if (anonymousKey !== null) fullPayload.anonymousKey = anonymousKey;
|
||||
sendJSON(res, 200, fullPayload);
|
||||
|
||||
Reference in New Issue
Block a user