mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
260 lines
12 KiB
Bash
Executable File
260 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
source "$(dirname "$0")/common"
|
|
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
|
bridge_prepare_sandbox
|
|
compose_config=(docker compose -f "$BRIDGE_COMPOSE" --env-file /dev/null --profile offline --profile live-devin config)
|
|
config="$("${compose_config[@]}")"
|
|
config_json="$("${compose_config[@]}" --format json)"
|
|
for forbidden in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.ssh" "/var/run/docker.sock"; do
|
|
[[ "$config" != *"$forbidden"* ]] || fail "forbidden host mount appears in compose: $forbidden"
|
|
done
|
|
grep -q 'user: 10001:10001' <<<"$config" || fail "runtime is not non-root"
|
|
grep -q 'read_only: true' <<<"$config" || fail "runtime root filesystem is not read-only"
|
|
grep -q 'internal: true' <<<"$config" || fail "internal network is missing"
|
|
grep -q 'CLAUDE_CONFIG_DIR: /home/bridge/.claude-devin-isolated' <<<"$config" || fail "isolated Claude config is missing"
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const config = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
const liveNetworks = Object.keys(config.services["omniroute-live"].networks || {}).sort();
|
|
if (JSON.stringify(liveNetworks) !== JSON.stringify(["bridge-internal", "devin-guard-internal"])) {
|
|
throw new Error(`live runtime network escape: ${liveNetworks.join(",")}`);
|
|
}
|
|
const guardNetworks = Object.keys(config.services["network-guard"].networks || {}).sort();
|
|
if (JSON.stringify(guardNetworks) !== JSON.stringify(["devin-guard-internal", "guard-egress"])) {
|
|
throw new Error(`network guard topology mismatch: ${guardNetworks.join(",")}`);
|
|
}
|
|
const claudeGuard = config.services["claude-egress-guard"];
|
|
if (JSON.stringify(Object.keys(claudeGuard.networks || {})) !== JSON.stringify(["bridge-internal"])) {
|
|
throw new Error("Claude egress guard must remain on the internal network only");
|
|
}
|
|
if (config.services["network-guard"].environment.GUARD_POLICY !== "devin") {
|
|
throw new Error("Devin network guard policy mismatch");
|
|
}
|
|
if (claudeGuard.environment.GUARD_POLICY !== "deny-all") {
|
|
throw new Error("Claude egress guard is not deny-all");
|
|
}
|
|
for (const guardName of ["network-guard", "claude-egress-guard"]) {
|
|
const guard = config.services[guardName];
|
|
const env = config.services[guardName].environment;
|
|
if (env.GUARD_ALLOW_SUFFIXES || env.GUARD_ALLOW_HOSTS) {
|
|
throw new Error(`${guardName} exposes mutable host allowlists`);
|
|
}
|
|
if (!guard.healthcheck?.test) throw new Error(`${guardName} has no healthcheck`);
|
|
const auditMount = (guard.volumes || []).find((mount) => mount.target === "/guard-audit");
|
|
if (!auditMount || auditMount.type !== "bind" || !auditMount.source.includes("/.sandbox/guard-audit/")) {
|
|
throw new Error(`${guardName} does not use its guard-only audit bind`);
|
|
}
|
|
}
|
|
const runtimeNames = ["omniroute", "claude", "contract", "omniroute-live", "claude-live"];
|
|
for (const serviceName of [...runtimeNames, "network-guard", "claude-egress-guard"]) {
|
|
const service = config.services[serviceName];
|
|
if (String(service.user) !== "10001:10001" || !service.read_only) {
|
|
throw new Error(`${serviceName} is not non-root and read-only`);
|
|
}
|
|
}
|
|
for (const serviceName of runtimeNames) {
|
|
const service = config.services[serviceName];
|
|
if ((service.volumes || []).some((mount) => mount.target === "/guard-audit")) {
|
|
throw new Error(`${serviceName} can mutate guard audit evidence`);
|
|
}
|
|
const namedVolumes = (service.volumes || []).filter((mount) => mount.type === "volume");
|
|
const hasClaudeConfig = namedVolumes.some(
|
|
(mount) => mount.target === "/home/bridge/.claude-devin-isolated",
|
|
);
|
|
const hasDevinAuth = namedVolumes.some(
|
|
(mount) => mount.target === "/home/bridge/.local/share/devin",
|
|
);
|
|
const expectsClaudeConfig = serviceName === "claude" || serviceName === "claude-live";
|
|
const expectsDevinAuth = serviceName === "omniroute-live";
|
|
if (hasClaudeConfig !== expectsClaudeConfig) {
|
|
throw new Error(`${serviceName} Claude config volume ownership mismatch`);
|
|
}
|
|
if (hasDevinAuth !== expectsDevinAuth) {
|
|
throw new Error(`${serviceName} Devin auth volume ownership mismatch`);
|
|
}
|
|
for (const key of [
|
|
"ANTHROPIC_MODEL",
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
"CLAUDE_CODE_SUBAGENT_MODEL",
|
|
]) {
|
|
if (!String(service.environment[key] || "").startsWith("devin-cli-agentic/")) {
|
|
throw new Error(`${serviceName} has a non-Devin model alias in ${key}`);
|
|
}
|
|
}
|
|
}
|
|
if (config.services["omniroute-live"].depends_on["network-guard"].condition !== "service_healthy") {
|
|
throw new Error("omniroute-live does not wait for a healthy Devin guard");
|
|
}
|
|
for (const serviceName of ["claude", "claude-live"]) {
|
|
if (config.services[serviceName].depends_on["claude-egress-guard"].condition !== "service_healthy") {
|
|
throw new Error(`${serviceName} does not wait for a healthy Claude guard`);
|
|
}
|
|
}
|
|
const liveEnv = config.services["omniroute-live"].environment;
|
|
if (liveEnv.DEVIN_BRIDGE_PROXY_URL !== "http://network-guard:8080") {
|
|
throw new Error("trusted Devin bridge proxy is missing");
|
|
}
|
|
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"]) {
|
|
if (liveEnv[key]) throw new Error(`omniroute-live must not inherit ${key}`);
|
|
}
|
|
for (const serviceName of runtimeNames.filter((name) => name !== "omniroute-live")) {
|
|
if (config.services[serviceName].environment.DEVIN_BRIDGE_PROXY_URL) {
|
|
throw new Error(`${serviceName} received the Devin bridge proxy setting`);
|
|
}
|
|
}
|
|
for (const serviceName of ["claude", "claude-live"]) {
|
|
const env = config.services[serviceName].environment;
|
|
if (
|
|
env.NODE_USE_ENV_PROXY !== "1" ||
|
|
env.HTTP_PROXY !== "http://claude-egress-guard:8080" ||
|
|
env.HTTPS_PROXY !== "http://claude-egress-guard:8080" ||
|
|
env.NO_PROXY !== "omniroute"
|
|
) {
|
|
throw new Error(`${serviceName} does not use the deny-all Claude guard`);
|
|
}
|
|
if (env.HTTP_PROXY === "http://network-guard:8080") {
|
|
throw new Error(`${serviceName} received the Devin-capable guard`);
|
|
}
|
|
}
|
|
' <<<"$config_json" || fail "structured compose isolation checks failed"
|
|
node --input-type=module -e '
|
|
import { pathToFileURL } from "node:url";
|
|
const policy = await import(pathToFileURL(process.argv[1]));
|
|
const allowed = [
|
|
"devin.ai",
|
|
"api.devin.ai",
|
|
"cognition.ai",
|
|
"api.cognition.ai",
|
|
"server.codeium.com",
|
|
"unleash.codeium.com",
|
|
];
|
|
const denied = [
|
|
"evildevin.ai",
|
|
"codeium.com",
|
|
"api.codeium.com",
|
|
"o123.ingest.sentry.io",
|
|
"api.anthropic.com",
|
|
"claude.ai",
|
|
];
|
|
for (const hostname of allowed) {
|
|
if (!policy.isAllowedGuardHostname(hostname, "devin")) throw new Error(`denied ${hostname}`);
|
|
}
|
|
for (const hostname of denied) {
|
|
if (policy.isAllowedGuardHostname(hostname, "devin")) throw new Error(`allowed ${hostname}`);
|
|
}
|
|
if (policy.isAllowedGuardHostname("api.devin.ai", "deny-all")) {
|
|
throw new Error("deny-all guard allowed Devin traffic");
|
|
}
|
|
' "$BRIDGE_ROOT/docker/devin-bridge/network-guard/policy.mjs" || fail "network guard policy checks failed"
|
|
bridge_test_env node --import tsx/esm --input-type=module -e '
|
|
import { pathToFileURL } from "node:url";
|
|
const { buildDevinChildEnv } = await import(pathToFileURL(process.argv[1]));
|
|
const home = process.env.DEVIN_AGENTIC_HOME;
|
|
const trusted = buildDevinChildEnv({}, {
|
|
DEVIN_AGENTIC_HOME: home,
|
|
DEVIN_BRIDGE_PROXY_URL: "http://network-guard:8080",
|
|
HTTP_PROXY: "http://user:password@host-proxy.example:3128",
|
|
HTTPS_PROXY: "http://user:password@host-proxy.example:3128",
|
|
ALL_PROXY: "socks5://host-proxy.example:1080",
|
|
});
|
|
if (
|
|
trusted.HTTP_PROXY !== "http://network-guard:8080" ||
|
|
trusted.HTTPS_PROXY !== "http://network-guard:8080" ||
|
|
trusted.ALL_PROXY
|
|
) {
|
|
throw new Error("trusted child proxy derivation failed");
|
|
}
|
|
const untrusted = buildDevinChildEnv({}, {
|
|
DEVIN_AGENTIC_HOME: home,
|
|
DEVIN_BRIDGE_PROXY_URL: "http://user:password@network-guard:8080",
|
|
HTTP_PROXY: "http://host-proxy.example:3128",
|
|
});
|
|
if (untrusted.HTTP_PROXY || untrusted.HTTPS_PROXY) {
|
|
throw new Error("untrusted child proxy was inherited");
|
|
}
|
|
' "$BRIDGE_ROOT/open-sse/executors/devin-cli-agentic.ts" || \
|
|
fail "Devin child proxy boundary checks failed"
|
|
bridge_assert_devin_auth_status 0 $'Logged in (via Devin)\n' || fail "clean auth fixture was rejected"
|
|
if bridge_assert_devin_auth_status 0 $'Logged in (via Devin)\nFailed to fetch from server\n' 2>/dev/null; then
|
|
fail "server-fetch auth failure was accepted"
|
|
fi
|
|
if bridge_assert_devin_auth_status 0 $'Logged out\n' 2>/dev/null; then
|
|
fail "logged-out auth fixture was accepted"
|
|
fi
|
|
if bridge_assert_devin_auth_status 0 $'Not Logged in (via Devin)\n' 2>/dev/null; then
|
|
fail "misleading auth fixture was accepted"
|
|
fi
|
|
selected_model="$(printf '%s' '{"models":[{"family_uid":"swe-1.7"},{"modelUid":"swe-1.7-lightning"}]}' | \
|
|
node --import tsx/esm "$BRIDGE_ROOT/scripts/devin-bridge/select-live-model.mjs")"
|
|
[[ "$selected_model" == swe-1-7-lightning ]] || fail "live model normalization or preference failed"
|
|
if printf '%s' '{"models":[{"family_uid":"unknown.9"}]}' | \
|
|
node --import tsx/esm "$BRIDGE_ROOT/scripts/devin-bridge/select-live-model.mjs" >/dev/null 2>&1; then
|
|
fail "unknown normalized live model was accepted"
|
|
fi
|
|
grep -q 'bridge_run_devin auth login --force-manual-token-flow' \
|
|
"$BRIDGE_ROOT/scripts/devin-bridge/login-devin" || fail "manual token login flow is missing"
|
|
if grep -Eqi 'read[[:space:]].*token|printf[[:space:]].*token|echo[[:space:]].*token' \
|
|
"$BRIDGE_ROOT/scripts/devin-bridge/login-devin"; then
|
|
fail "login script could expose a token"
|
|
fi
|
|
grep -q 'bridge_check_devin_auth' "$BRIDGE_ROOT/scripts/devin-bridge/test-live-devin" || \
|
|
fail "live test bypasses strict auth status"
|
|
grep -q 'bridge_check_devin_auth' "$BRIDGE_ROOT/scripts/devin-bridge/launch" || \
|
|
fail "normal launch bypasses strict auth status"
|
|
grep -q 'up -d --wait network-guard claude-egress-guard' "$BRIDGE_ROOT/scripts/devin-bridge/launch" || \
|
|
fail "normal launch does not start the audited Claude egress guard"
|
|
grep -qx '\.sandbox' "$BRIDGE_ROOT/.dockerignore" || fail ".sandbox is not excluded from builds"
|
|
if [[ "${1:-}" == --static ]]; then printf 'PASS: static bridge isolation checks passed\n'; exit 0; fi
|
|
trap bridge_cleanup_compose EXIT
|
|
bridge_cleanup_compose
|
|
bridge_reset_claude_egress_audit
|
|
docker compose -f "$BRIDGE_COMPOSE" --profile offline up -d --wait claude-egress-guard
|
|
docker compose -f "$BRIDGE_COMPOSE" --profile offline run --rm --no-deps claude bash -ceu '
|
|
test "$(id -u)" = 10001
|
|
test "$HOME" = /home/bridge
|
|
test "$CLAUDE_CONFIG_DIR" = /home/bridge/.claude-devin-isolated
|
|
test "$ANTHROPIC_BASE_URL" = http://omniroute:20128
|
|
test "$ANTHROPIC_AUTH_TOKEN" = sk-local-devin-gateway
|
|
test -z "${ANTHROPIC_API_KEY:-}${CLAUDE_CODE_OAUTH_TOKEN:-}${AWS_ACCESS_KEY_ID:-}${AWS_SECRET_ACCESS_KEY:-}${GOOGLE_APPLICATION_CREDENTIALS:-}${AZURE_OPENAI_API_KEY:-}"
|
|
test ! -e /var/run/docker.sock
|
|
if touch /bridge-must-remain-read-only 2>/dev/null; then
|
|
echo "container root filesystem is writable" >&2; exit 1
|
|
fi
|
|
for host in api.anthropic.com claude.ai; do
|
|
if node -e "require(\"net\").connect(443,process.argv[1]).on(\"connect\",()=>process.exit(0)).on(\"error\",()=>process.exit(1)).setTimeout(1500,()=>process.exit(1))" "$host"; then
|
|
echo "unexpected network access to $host" >&2; exit 1
|
|
fi
|
|
done
|
|
'
|
|
docker compose -f "$BRIDGE_COMPOSE" --profile offline run --rm --no-deps claude \
|
|
node --input-type=module -e '
|
|
async function expectProxyDenial(request) {
|
|
try {
|
|
const response = await request;
|
|
if (response.status !== 403) {
|
|
throw new Error(`unexpected proxy response: ${response.status}`);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.startsWith("unexpected proxy response:")) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
await expectProxyDenial(fetch("https://api.anthropic.com", {
|
|
signal: AbortSignal.timeout(3000),
|
|
}));
|
|
await expectProxyDenial(fetch("https://claude.ai", {
|
|
signal: AbortSignal.timeout(3000),
|
|
}));
|
|
'
|
|
bridge_cleanup_compose
|
|
bridge_assert_claude_guard_denials "$BRIDGE_CLAUDE_AUDIT" || \
|
|
fail "Claude proxy denial audit proof failed"
|
|
bridge_export_guard_audit "$BRIDGE_CLAUDE_AUDIT" claude-egress-verifier.jsonl
|
|
trap - EXIT
|
|
bridge_reset_claude_egress_audit
|
|
printf 'PASS: runtime bridge isolation checks passed\n'
|