fix: harden Devin bridge runtime boundaries

This commit is contained in:
Lucas Israel
2026-07-27 14:02:01 -03:00
parent 98c98856b8
commit ccbe6bc288
17 changed files with 945 additions and 367 deletions

View File

@@ -18,6 +18,7 @@ coverage
# Runtime data and logs
data
logs
.sandbox
# Local env files (inject at runtime via --env-file or -e)
.env

View File

@@ -73,7 +73,7 @@ services:
omniroute:
condition: service_healthy
claude-egress-guard:
condition: service_started
condition: service_healthy
working_dir: /workspace
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-e2e.sh"]
environment:
@@ -109,10 +109,21 @@ services:
environment:
GUARD_LISTEN: 0.0.0.0:8080
GUARD_POLICY: deny-all
GUARD_LOG: /logs/claude-egress.jsonl
GUARD_LOG: /guard-audit/egress.jsonl
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('net').connect(8080,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))",
]
interval: 1s
timeout: 1s
retries: 15
volumes:
- ./network-guard:/guard:ro
- ../../.sandbox/evidence:/logs
- ../../.sandbox/guard-audit/claude:/guard-audit
networks: [bridge-internal]
network-guard:
@@ -126,17 +137,30 @@ services:
environment:
GUARD_LISTEN: 0.0.0.0:8080
GUARD_POLICY: devin
GUARD_LOG: /logs/egress.jsonl
GUARD_LOG: /guard-audit/egress.jsonl
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('net').connect(8080,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))",
]
interval: 1s
timeout: 1s
retries: 15
volumes:
- ./network-guard:/guard:ro
- ../../.sandbox/evidence:/logs
- ../../.sandbox/guard-audit/devin:/guard-audit
networks: [devin-guard-internal, guard-egress]
omniroute-live:
<<: *runtime
profiles: [live-devin]
hostname: omniroute
depends_on: [network-guard]
depends_on:
network-guard:
condition: service_healthy
environment:
<<: *isolated-environment
CLI_DEVIN_AGENTIC_BIN: /usr/local/bin/devin
@@ -165,7 +189,7 @@ services:
omniroute-live:
condition: service_healthy
claude-egress-guard:
condition: service_started
condition: service_healthy
working_dir: /workspace
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-live-e2e.sh"]
environment:

View File

@@ -20,3 +20,111 @@ export function isAllowedGuardHostname(hostname, policy = "deny-all") {
(suffix) => value === suffix.slice(1) || value.endsWith(suffix)
);
}
const HOP_BY_HOP_HEADERS = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
export function sanitizeForwardHeaders(headers, target) {
const connectionTokens = String(headers.connection || "")
.split(",")
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
const blocked = new Set([...HOP_BY_HOP_HEADERS, ...connectionTokens]);
const sanitized = {};
for (const [name, value] of Object.entries(headers)) {
if (value === undefined || blocked.has(name.toLowerCase()) || name.toLowerCase() === "host") {
continue;
}
sanitized[name] = value;
}
sanitized.host = target.host;
return sanitized;
}
export function parseConnectAuthority(authority) {
const value = String(authority || "");
const match = value.match(/^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/);
if (!match) return null;
const hostname = normalizeHostname(match[1] || match[2]);
const port = Number(match[3]);
if (!hostname || port !== 443) return null;
return { hostname, port };
}
function readUint24(buffer, offset) {
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2];
}
export function parseTlsClientHelloSni(buffer) {
if (!Buffer.isBuffer(buffer)) return { status: "invalid", reason: "not_buffer" };
let offset = 0;
const handshakeParts = [];
while (offset < buffer.length) {
if (buffer.length - offset < 5) return { status: "need-more" };
if (buffer[offset] !== 22) return { status: "invalid", reason: "not_handshake_record" };
const recordLength = buffer.readUInt16BE(offset + 3);
if (recordLength <= 0 || recordLength > 18432) {
return { status: "invalid", reason: "invalid_record_length" };
}
if (buffer.length - offset - 5 < recordLength) return { status: "need-more" };
handshakeParts.push(buffer.subarray(offset + 5, offset + 5 + recordLength));
offset += 5 + recordLength;
}
const handshake = Buffer.concat(handshakeParts);
if (handshake.length < 4) return { status: "need-more" };
if (handshake[0] !== 1) return { status: "invalid", reason: "not_client_hello" };
const helloLength = readUint24(handshake, 1);
if (helloLength > 65531) return { status: "invalid", reason: "client_hello_too_large" };
if (handshake.length - 4 < helloLength) return { status: "need-more" };
const hello = handshake.subarray(4, 4 + helloLength);
let cursor = 34;
if (hello.length < cursor + 1) return { status: "invalid", reason: "truncated_hello" };
const sessionLength = hello[cursor++];
cursor += sessionLength;
if (hello.length < cursor + 2) return { status: "invalid", reason: "truncated_ciphers" };
const cipherLength = hello.readUInt16BE(cursor);
cursor += 2 + cipherLength;
if (hello.length < cursor + 1) return { status: "invalid", reason: "truncated_compression" };
const compressionLength = hello[cursor++];
cursor += compressionLength;
if (hello.length < cursor + 2) return { status: "invalid", reason: "missing_extensions" };
const extensionsLength = hello.readUInt16BE(cursor);
cursor += 2;
const extensionsEnd = cursor + extensionsLength;
if (extensionsEnd > hello.length) return { status: "invalid", reason: "truncated_extensions" };
while (cursor < extensionsEnd) {
if (extensionsEnd - cursor < 4) return { status: "invalid", reason: "truncated_extension" };
const type = hello.readUInt16BE(cursor);
const length = hello.readUInt16BE(cursor + 2);
cursor += 4;
if (cursor + length > extensionsEnd) {
return { status: "invalid", reason: "invalid_extension_length" };
}
if (type === 0) {
const data = hello.subarray(cursor, cursor + length);
if (data.length < 5 || data.readUInt16BE(0) !== data.length - 2 || data[2] !== 0) {
return { status: "invalid", reason: "invalid_server_name" };
}
const nameLength = data.readUInt16BE(3);
if (nameLength !== data.length - 5) {
return { status: "invalid", reason: "invalid_server_name_length" };
}
const serverName = normalizeHostname(data.subarray(5).toString("ascii"));
if (!/^[a-z0-9.-]+$/.test(serverName)) {
return { status: "invalid", reason: "invalid_server_name_value" };
}
return { status: "ok", serverName };
}
cursor += length;
}
return { status: "invalid", reason: "missing_sni" };
}

View File

@@ -1,58 +1,136 @@
import fs from "node:fs";
import http from "node:http";
import net from "node:net";
import fs from "node:fs";
import { isAllowedGuardHostname } from "./policy.mjs";
import { pathToFileURL } from "node:url";
const [host, portText] = (process.env.GUARD_LISTEN || "0.0.0.0:8080").split(":");
const port = Number(portText);
const policy = process.env.GUARD_POLICY || "deny-all";
if (!new Set(["deny-all", "devin"]).has(policy)) {
throw new Error(`Unknown network guard policy: ${policy}`);
}
const logPath = process.env.GUARD_LOG || "/tmp/egress.jsonl";
import {
isAllowedGuardHostname,
parseConnectAuthority,
parseTlsClientHelloSni,
sanitizeForwardHeaders,
} from "./policy.mjs";
function allowed(hostname) {
return isAllowedGuardHostname(hostname, policy);
}
const MAX_CLIENT_HELLO_BYTES = 64 * 1024;
const CLIENT_HELLO_TIMEOUT_MS = 3000;
function audit(hostname, decision) {
fs.appendFileSync(
logPath,
`${JSON.stringify({ at: new Date().toISOString(), hostname, decision })}\n`
);
}
const server = http.createServer((req, res) => {
const target = new URL(req.url);
if (!allowed(target.hostname)) {
audit(target.hostname, "deny");
res.writeHead(403).end("egress denied\n");
return;
export function createGuardProxy({
policy = "deny-all",
logPath = "/tmp/egress.jsonl",
allowHostname = (hostname) => isAllowedGuardHostname(hostname, policy),
connectSocket = (port, hostname, onConnect) => net.connect(port, hostname, onConnect),
} = {}) {
if (!new Set(["deny-all", "devin"]).has(policy)) {
throw new Error(`Unknown network guard policy: ${policy}`);
}
audit(target.hostname, "allow");
const upstream = http.request(target, { method: req.method, headers: req.headers }, (reply) => {
res.writeHead(reply.statusCode || 502, reply.headers);
reply.pipe(res);
function audit(hostname, decision, reason) {
fs.appendFileSync(
logPath,
`${JSON.stringify({ at: new Date().toISOString(), hostname, decision, reason })}\n`
);
}
const server = http.createServer((req, res) => {
let target;
try {
target = new URL(req.url);
} catch {
res.writeHead(400).end("invalid proxy target\n");
return;
}
if (target.protocol !== "http:" || target.username || target.password) {
audit(target.hostname, "deny", "invalid_http_target");
res.writeHead(403).end("egress denied\n");
return;
}
if (!allowHostname(target.hostname)) {
audit(target.hostname, "deny", "host_policy");
res.writeHead(403).end("egress denied\n");
return;
}
audit(target.hostname, "allow", "host_policy");
const upstream = http.request(
target,
{
method: req.method,
headers: sanitizeForwardHeaders(req.headers, target),
},
(reply) => {
res.writeHead(reply.statusCode || 502, reply.headers);
reply.pipe(res);
}
);
req.pipe(upstream);
upstream.on("error", () => res.writeHead(502).end("upstream error\n"));
});
req.pipe(upstream);
upstream.on("error", () => res.writeHead(502).end("upstream error\n"));
});
server.on("connect", (req, client, head) => {
const [hostname, portValue] = req.url.split(":");
if (!allowed(hostname)) {
audit(hostname, "deny");
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
return;
}
audit(hostname, "allow");
const upstream = net.connect(Number(portValue) || 443, hostname, () => {
server.on("connect", (req, client, head) => {
const authority = parseConnectAuthority(req.url);
if (!authority) {
audit(req.url, "deny", "invalid_connect_authority");
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
return;
}
const { hostname, port } = authority;
if (!allowHostname(hostname)) {
audit(hostname, "deny", "host_policy");
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
return;
}
let buffer = Buffer.from(head);
let settled = false;
const timer = setTimeout(() => fail("client_hello_timeout"), CLIENT_HELLO_TIMEOUT_MS);
timer.unref?.();
const cleanup = () => {
clearTimeout(timer);
client.removeListener("data", onData);
};
const fail = (reason) => {
if (settled) return;
settled = true;
cleanup();
audit(hostname, "deny", reason);
client.destroy();
};
const inspect = () => {
if (buffer.length > MAX_CLIENT_HELLO_BYTES) return fail("client_hello_too_large");
const parsed = parseTlsClientHelloSni(buffer);
if (parsed.status === "need-more") return;
if (parsed.status !== "ok") return fail(parsed.reason || "invalid_client_hello");
if (parsed.serverName !== hostname) return fail("sni_mismatch");
settled = true;
cleanup();
client.pause();
const upstream = connectSocket(port, hostname, () => {
audit(hostname, "allow", "sni_match");
if (buffer.length) upstream.write(buffer);
upstream.pipe(client);
client.pipe(upstream);
client.resume();
});
upstream.on("error", () => client.destroy());
};
const onData = (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
inspect();
};
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (head.length) upstream.write(head);
upstream.pipe(client);
client.pipe(upstream);
client.on("data", onData);
if (buffer.length) inspect();
client.resume();
});
upstream.on("error", () => client.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"));
});
server.listen(port, host);
return server;
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const [host, portText] = (process.env.GUARD_LISTEN || "0.0.0.0:8080").split(":");
const server = createGuardProxy({
policy: process.env.GUARD_POLICY || "deny-all",
logPath: process.env.GUARD_LOG || "/tmp/egress.jsonl",
});
server.listen(Number(portText), host);
}

View File

@@ -45,7 +45,11 @@ The `offline` network is internal, so no runtime container can reach the Interne
`live-devin` OmniRoute service is also attached only to that internal network; outbound
HTTP(S) goes through `network-guard`, whose only allowed suffixes are `.devin.ai` and
`.cognition.ai`. Anthropic, Claude, Statsig, Sentry, and every unrelated destination are
denied by default. The guard records decisions in `.sandbox/evidence/egress.jsonl`.
denied by default. The guards write their canonical audit logs to the guard-only binds
`.sandbox/guard-audit/devin/egress.jsonl` and
`.sandbox/guard-audit/claude/egress.jsonl`. Runtime services do not mount those directories.
After the guarded services stop, the scripts validate file ownership, mode, link count, and
every audit decision before copying the token-free audit record into `.sandbox/evidence`.
Run the executable proof at any time:
@@ -85,9 +89,10 @@ host session:
ENABLE_LIVE_DEVIN_TESTS=1 ./scripts/devin-bridge/login-devin
```
After login, the live test checks `devin auth status`, obtains the account's machine-readable
model list with `devin models list --format json`, selects a returned model identifier, and
runs three disposable Claude Code scenarios:
After login, the live test accepts `devin auth status` only when its output contains the exact
line `Logged in (via Devin)`. It then obtains the account's machine-readable model list with
`devin models list --format json`, selects an identifier from an explicit model-identifier
field, and runs three disposable Claude Code scenarios:
```bash
ENABLE_LIVE_DEVIN_TESTS=1 ./scripts/devin-bridge/test-live-devin
@@ -126,7 +131,11 @@ the host.
shows local routing and sanitized executor failures.
- `.sandbox/evidence/mock-acp.jsonl` records deterministic offline provider actions.
- `.sandbox/evidence/claude-stream.jsonl` records the real Claude Code offline run.
- `.sandbox/evidence/egress.jsonl` records live guard decisions without tokens.
- `.sandbox/guard-audit/devin/egress.jsonl` and
`.sandbox/guard-audit/claude/egress.jsonl` are the canonical guard-only audit files.
- `.sandbox/evidence/egress.jsonl`, `.sandbox/evidence/claude-egress.jsonl`, and
`.sandbox/evidence/claude-egress-verifier.jsonl` are validated, post-shutdown copies
without tokens.
- An ACP timeout, malformed frame, unavailable binary/model, or process exit is an explicit
`502`; it never selects a second provider.

View File

@@ -3,17 +3,34 @@ set -euo pipefail
BRIDGE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BRIDGE_COMPOSE="$BRIDGE_ROOT/docker/devin-bridge/compose.yml"
BRIDGE_SANDBOX="$BRIDGE_ROOT/.sandbox"
BRIDGE_GUARD_AUDIT_ROOT="$BRIDGE_SANDBOX/guard-audit"
BRIDGE_CLAUDE_AUDIT="$BRIDGE_GUARD_AUDIT_ROOT/claude/egress.jsonl"
BRIDGE_DEVIN_AUDIT="$BRIDGE_GUARD_AUDIT_ROOT/devin/egress.jsonl"
BRIDGE_RUNTIME_POLICY="$BRIDGE_ROOT/scripts/devin-bridge/runtime-policy.mjs"
bridge_prepare_sandbox() {
mkdir -p "$BRIDGE_SANDBOX/home" "$BRIDGE_SANDBOX/test-data" \
"$BRIDGE_SANDBOX/e2e-workspace" "$BRIDGE_SANDBOX/live-workspace" \
"$BRIDGE_SANDBOX/evidence"
"$BRIDGE_SANDBOX/evidence" "$BRIDGE_GUARD_AUDIT_ROOT/claude" \
"$BRIDGE_GUARD_AUDIT_ROOT/devin"
chmod 0777 "$BRIDGE_SANDBOX/e2e-workspace" "$BRIDGE_SANDBOX/live-workspace" \
"$BRIDGE_SANDBOX/evidence"
chmod 01777 "$BRIDGE_GUARD_AUDIT_ROOT/claude" "$BRIDGE_GUARD_AUDIT_ROOT/devin"
}
bridge_reset_guard_audit() {
local audit_path="$1"
local audit_dir
local temp_path
bridge_prepare_sandbox
audit_dir="$(dirname "$audit_path")"
temp_path="$(mktemp "$audit_dir/.egress.jsonl.XXXXXX")"
chmod 0666 "$temp_path"
mv -f "$temp_path" "$audit_path"
}
bridge_reset_claude_egress_audit() {
bridge_prepare_sandbox
: >"$BRIDGE_SANDBOX/evidence/claude-egress.jsonl"
chmod 0666 "$BRIDGE_SANDBOX/evidence/claude-egress.jsonl"
bridge_reset_guard_audit "$BRIDGE_CLAUDE_AUDIT"
}
bridge_reset_devin_egress_audit() {
bridge_reset_guard_audit "$BRIDGE_DEVIN_AUDIT"
}
bridge_reset_e2e_fixture() {
bridge_prepare_sandbox
@@ -35,6 +52,7 @@ bridge_reset_live_fixture() {
"$BRIDGE_SANDBOX/evidence/live-models.json" \
"$BRIDGE_SANDBOX/evidence/egress.jsonl"
bridge_reset_claude_egress_audit
bridge_reset_devin_egress_audit
}
bridge_test_env() {
bridge_prepare_sandbox
@@ -55,18 +73,13 @@ bridge_run_devin() {
bridge_assert_devin_auth_status() {
local exit_status="$1"
local output="$2"
[[ "$exit_status" == 0 ]] || {
printf 'FAIL: Devin auth status command failed\n' >&2
return 1
}
[[ "$output" == *"Logged in (via Devin)"* ]] || {
printf 'FAIL: Devin auth status did not confirm login\n' >&2
return 1
}
if grep -Fqi 'Failed to fetch from server' <<<"$output"; then
printf 'FAIL: Devin auth status could not confirm server access\n' >&2
return 1
fi
printf '%s' "$output" | node --input-type=module -e '
import { pathToFileURL } from "node:url";
import fs from "node:fs";
const policy = await import(pathToFileURL(process.argv[1]));
const result = policy.validateDevinAuthStatus(process.argv[2], fs.readFileSync(0, "utf8"));
if (!result.ok) throw new Error(result.error);
' "$BRIDGE_RUNTIME_POLICY" "$exit_status"
}
bridge_check_devin_auth() {
@@ -82,34 +95,37 @@ bridge_check_devin_auth() {
bridge_assert_zero_claude_egress() {
local audit_path="$1"
[[ -f "$audit_path" ]] || {
printf 'FAIL: Claude egress audit file is missing\n' >&2
return 1
}
[[ ! -s "$audit_path" ]] || {
printf 'FAIL: Claude attempted external egress during the real run\n' >&2
return 1
}
bridge_validate_guard_audit claude-zero "$audit_path"
}
bridge_assert_claude_guard_denials() {
local audit_path="$1"
[[ -s "$audit_path" ]] || {
printf 'FAIL: Claude egress denial audit is missing or empty\n' >&2
return 1
}
node -e '
const fs = require("node:fs");
const entries = fs.readFileSync(process.argv[1], "utf8")
.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
if (!entries.length) throw new Error("Claude egress audit has no records");
if (entries.some((entry) => entry.decision !== "deny")) {
throw new Error("Claude egress audit contains a non-deny decision");
}
for (const hostname of ["api.anthropic.com", "claude.ai"]) {
if (!entries.some((entry) => entry.hostname === hostname && entry.decision === "deny")) {
throw new Error(`Claude egress audit is missing deny for ${hostname}`);
}
}
' "$audit_path"
bridge_validate_guard_audit claude-denials "$audit_path"
}
bridge_assert_devin_guard_audit() {
local audit_path="$1"
bridge_validate_guard_audit devin-allowed "$audit_path"
}
bridge_validate_guard_audit() {
local kind="$1"
local audit_path="$2"
node --input-type=module -e '
import { pathToFileURL } from "node:url";
const policy = await import(pathToFileURL(process.argv[1]));
policy.validateAuditFile(process.argv[2], process.argv[3], process.argv[4]);
' "$BRIDGE_RUNTIME_POLICY" "$kind" "$audit_path" "$(id -u)"
}
bridge_export_guard_audit() {
local audit_path="$1"
local evidence_name="$2"
cp "$audit_path" "$BRIDGE_SANDBOX/evidence/$evidence_name"
chmod 0644 "$BRIDGE_SANDBOX/evidence/$evidence_name"
}
bridge_cleanup_compose() {
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin \
down --remove-orphans >/dev/null 2>&1 || true
}

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/common"
trap bridge_cleanup_compose EXIT
bridge_cleanup_compose
bridge_prepare_sandbox
"$(dirname "$0")/verify-anthropic-isolation"
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin down --remove-orphans
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d network-guard claude-egress-guard
bridge_reset_claude_egress_audit
bridge_reset_devin_egress_audit
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d --wait network-guard claude-egress-guard
bridge_check_devin_auth
bridge_run_devin models list --format json >"$BRIDGE_SANDBOX/evidence/live-models.json"
devin_model="$(node --import tsx/esm "$BRIDGE_ROOT/scripts/devin-bridge/select-live-model.mjs" \
@@ -15,5 +18,11 @@ export DEVIN_BRIDGE_OPUS_MODEL="${DEVIN_BRIDGE_OPUS_MODEL:-$DEVIN_BRIDGE_MODEL}"
export DEVIN_BRIDGE_HAIKU_MODEL="${DEVIN_BRIDGE_HAIKU_MODEL:-$DEVIN_BRIDGE_MODEL}"
export DEVIN_BRIDGE_SUBAGENT_MODEL="${DEVIN_BRIDGE_SUBAGENT_MODEL:-$DEVIN_BRIDGE_MODEL}"
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d --wait omniroute-live
exec docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps \
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps \
claude-live claude
bridge_cleanup_compose
bridge_assert_devin_guard_audit "$BRIDGE_DEVIN_AUDIT"
bridge_assert_zero_claude_egress "$BRIDGE_CLAUDE_AUDIT"
bridge_export_guard_audit "$BRIDGE_DEVIN_AUDIT" egress.jsonl
bridge_export_guard_audit "$BRIDGE_CLAUDE_AUDIT" claude-egress.jsonl
trap - EXIT

View File

@@ -2,8 +2,12 @@
set -euo pipefail
source "$(dirname "$0")/common"
[[ "${ENABLE_LIVE_DEVIN_TESTS:-}" == 1 ]] || { echo 'Set ENABLE_LIVE_DEVIN_TESTS=1' >&2; exit 1; }
trap bridge_cleanup_compose EXIT
bridge_cleanup_compose
bridge_prepare_sandbox
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin down --remove-orphans
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d network-guard
bridge_reset_devin_egress_audit
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d --wait network-guard
bridge_run_devin auth login --force-manual-token-flow
bridge_cleanup_compose
trap - EXIT
exec env ENABLE_LIVE_DEVIN_TESTS=1 "$(dirname "$0")/test-live-devin"

View File

@@ -0,0 +1,101 @@
import fs from "node:fs";
const ALLOWED_DEVIN_SUFFIXES = [".devin.ai", ".cognition.ai"];
const ALLOWED_DEVIN_EXACT = ["server.codeium.com", "unleash.codeium.com"];
function normalizedHostname(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\.$/, "");
}
export function isAllowedDevinAuditHostname(hostname) {
const value = normalizedHostname(hostname);
return (
ALLOWED_DEVIN_EXACT.includes(value) ||
ALLOWED_DEVIN_SUFFIXES.some((suffix) => value === suffix.slice(1) || value.endsWith(suffix))
);
}
export function validateDevinAuthStatus(exitStatus, output) {
if (Number(exitStatus) !== 0) return { ok: false, error: "auth status command failed" };
const lines = String(output)
.split(/\r?\n/)
.map((line) => line.trim());
if (!lines.includes("Logged in (via Devin)")) {
return { ok: false, error: "auth status did not confirm login" };
}
if (lines.some((line) => /failed to fetch from server/i.test(line))) {
return { ok: false, error: "auth status could not confirm server access" };
}
return { ok: true };
}
export function parseAuditEntries(text) {
const lines = String(text)
.split(/\r?\n/)
.filter((line) => line.trim().length > 0);
return lines.map((line) => JSON.parse(line));
}
export function validateZeroClaudeEgress(text) {
if (String(text).length !== 0) {
return { ok: false, error: "Claude attempted external egress during the real run" };
}
return { ok: true };
}
export function validateClaudeGuardDenials(text) {
const entries = parseAuditEntries(text);
if (!entries.length) return { ok: false, error: "Claude egress audit has no records" };
if (entries.some((entry) => entry.decision !== "deny")) {
return { ok: false, error: "Claude egress audit contains a non-deny decision" };
}
for (const hostname of ["api.anthropic.com", "claude.ai"]) {
if (!entries.some((entry) => entry.hostname === hostname && entry.decision === "deny")) {
return { ok: false, error: `Claude egress audit is missing deny for ${hostname}` };
}
}
return { ok: true };
}
export function validateDevinGuardAudit(text) {
const entries = parseAuditEntries(text);
if (!entries.length) return { ok: false, error: "Devin egress audit has no records" };
for (const entry of entries) {
if (entry.decision !== "allow" || !isAllowedDevinAuditHostname(entry.hostname)) {
return { ok: false, error: `unexpected Devin egress record: ${String(entry.hostname)}` };
}
}
return { ok: true };
}
export function validateAuditFileStat(stat, expectedUid) {
if (!stat || !stat.isFile() || stat.isSymbolicLink()) return "audit path is not a regular file";
if (stat.nlink !== 1) return "audit file link count is not one";
if (stat.uid !== Number(expectedUid)) return "audit file owner mismatch";
if ((stat.mode & 0o777) !== 0o666) return "audit file mode mismatch";
return null;
}
export function readValidatedAuditFile(path, expectedUid) {
const stat = fs.lstatSync(path);
const statError = validateAuditFileStat(stat, expectedUid);
if (statError) throw new Error(statError);
return fs.readFileSync(path, "utf8");
}
export function validateAuditFile(kind, path, expectedUid) {
const text = readValidatedAuditFile(path, expectedUid);
const result =
kind === "claude-zero"
? validateZeroClaudeEgress(text)
: kind === "claude-denials"
? validateClaudeGuardDenials(text)
: kind === "devin-allowed"
? validateDevinGuardAudit(text)
: { ok: false, error: `unknown audit validation kind: ${kind}` };
if (!result.ok) throw new Error(result.error);
return text;
}

View File

@@ -1,19 +1,15 @@
#!/usr/bin/env node
import fs from "node:fs";
import { pathToFileURL } from "node:url";
import { DEVIN_MODEL_CATALOG } from "../../open-sse/config/providers/registry/devin/catalog.ts";
const document = JSON.parse(fs.readFileSync(0, "utf8"));
const candidates = [];
const candidateFields = new Set([
"id",
"model",
"model_id",
"modelid",
"modelId",
"model_uid",
"modeluid",
"modelUid",
"family_uid",
"familyuid",
"slug",
"familyUid",
]);
function normalizeModelId(value) {
@@ -24,56 +20,82 @@ function normalizeModelId(value) {
.replace(/^-+|-+$/g, "");
}
function collect(value) {
function collect(value, candidates) {
if (Array.isArray(value)) {
value.forEach(collect);
value.forEach((item) => collect(item, candidates));
return;
}
if (!value || typeof value !== "object") return;
for (const [key, nested] of Object.entries(value)) {
if (
typeof nested === "string" &&
candidateFields.has(key.toLowerCase()) &&
candidateFields.has(key) &&
/^[a-z0-9][a-z0-9._/-]*$/i.test(nested)
) {
candidates.push(nested);
}
collect(nested);
collect(nested, candidates);
}
}
collect(document);
const unique = [...new Set(candidates)];
const catalogIds = new Set(DEVIN_MODEL_CATALOG.map((entry) => entry.id));
const available = [
...new Set(
unique
.map((candidate) => normalizeModelId(candidate))
.filter((candidate) => catalogIds.has(candidate))
),
];
for (const [name, configured] of [
["DEVIN_BRIDGE_SONNET_MODEL", process.env.DEVIN_BRIDGE_SONNET_MODEL],
["DEVIN_BRIDGE_OPUS_MODEL", process.env.DEVIN_BRIDGE_OPUS_MODEL],
["DEVIN_BRIDGE_HAIKU_MODEL", process.env.DEVIN_BRIDGE_HAIKU_MODEL],
["DEVIN_BRIDGE_SUBAGENT_MODEL", process.env.DEVIN_BRIDGE_SUBAGENT_MODEL],
]) {
if (!configured) continue;
const prefix = "devin-cli-agentic/";
const modelId = configured.startsWith(prefix) ? configured.slice(prefix.length) : "";
if (!modelId || !available.includes(modelId)) {
throw new Error(`${name} is not a model returned by Devin and present in OmniRoute`);
export function selectLiveModel(
document,
environment = process.env,
catalog = DEVIN_MODEL_CATALOG
) {
const candidates = [];
collect(document, candidates);
const unique = [...new Set(candidates)];
const normalizedCatalog = new Map();
for (const entry of catalog) {
const normalized = normalizeModelId(entry.id);
const existing = normalizedCatalog.get(normalized) || [];
existing.push(entry.id);
normalizedCatalog.set(normalized, existing);
}
for (const [normalized, ids] of normalizedCatalog) {
if (ids.length > 1) {
throw new Error(
`Ambiguous OmniRoute catalog normalization for ${normalized}: ${ids.join(", ")}`
);
}
}
const catalogIds = new Set(catalog.map((entry) => entry.id));
const available = [
...new Set(
unique
.map((candidate) => normalizeModelId(candidate))
.filter((candidate) => normalizedCatalog.has(candidate))
),
];
for (const [name, configured] of [
["DEVIN_BRIDGE_SONNET_MODEL", environment.DEVIN_BRIDGE_SONNET_MODEL],
["DEVIN_BRIDGE_OPUS_MODEL", environment.DEVIN_BRIDGE_OPUS_MODEL],
["DEVIN_BRIDGE_HAIKU_MODEL", environment.DEVIN_BRIDGE_HAIKU_MODEL],
["DEVIN_BRIDGE_SUBAGENT_MODEL", environment.DEVIN_BRIDGE_SUBAGENT_MODEL],
]) {
if (!configured) continue;
const prefix = "devin-cli-agentic/";
const modelId = configured.startsWith(prefix) ? configured.slice(prefix.length) : "";
if (!modelId || !catalogIds.has(modelId) || !available.includes(modelId)) {
throw new Error(`${name} is not a model returned by Devin and present in OmniRoute`);
}
}
const selected =
available.find((candidate) => candidate === "swe-1-7-lightning") ||
available.find((candidate) => candidate === "swe-1-7") ||
available.find((candidate) => /swe|claude|gpt|gemini/i.test(candidate)) ||
available[0];
if (!selected) {
throw new Error("Devin returned no model identifier present in OmniRoute's Devin catalog");
}
return selected;
}
const selected =
available.find((candidate) => candidate === "swe-1-7-lightning") ||
available.find((candidate) => candidate === "swe-1-7") ||
available.find((candidate) => /swe|claude|gpt|gemini/i.test(candidate)) ||
available[0];
if (!selected) {
throw new Error("Devin returned no model identifier present in OmniRoute's Devin catalog");
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const document = JSON.parse(fs.readFileSync(0, "utf8"));
process.stdout.write(selectLiveModel(document));
}
process.stdout.write(selected);

View File

@@ -1,12 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/common"
trap bridge_cleanup_compose EXIT
bridge_cleanup_compose
bridge_reset_e2e_fixture
"$(dirname "$0")/verify-anthropic-isolation" --static
docker compose -f "$BRIDGE_COMPOSE" --profile offline down --remove-orphans
docker compose -f "$BRIDGE_COMPOSE" --profile offline up --abort-on-container-exit \
--exit-code-from claude claude
grep -q '"action":"final"' "$BRIDGE_SANDBOX/evidence/mock-acp.jsonl"
grep -q 'BRIDGE_E2E_COMPLETE' "$BRIDGE_SANDBOX/evidence/claude-stream.jsonl"
bridge_assert_zero_claude_egress "$BRIDGE_SANDBOX/evidence/claude-egress.jsonl"
bridge_cleanup_compose
bridge_assert_zero_claude_egress "$BRIDGE_CLAUDE_AUDIT"
bridge_export_guard_audit "$BRIDGE_CLAUDE_AUDIT" claude-egress.jsonl
trap - EXIT
printf 'PASS: real Claude Code completed the offline agentic fixture\n'

View File

@@ -2,10 +2,11 @@
set -euo pipefail
source "$(dirname "$0")/common"
[[ "${ENABLE_LIVE_DEVIN_TESTS:-}" == 1 ]] || { echo 'Set ENABLE_LIVE_DEVIN_TESTS=1' >&2; exit 1; }
trap bridge_cleanup_compose EXIT
bridge_cleanup_compose
bridge_reset_live_fixture
"$(dirname "$0")/verify-anthropic-isolation" --static
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin down --remove-orphans
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d network-guard
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up -d --wait network-guard
bridge_check_devin_auth
bridge_run_devin models list --format json >"$BRIDGE_SANDBOX/evidence/live-models.json"
devin_model="$(node --import tsx/esm "$BRIDGE_ROOT/scripts/devin-bridge/select-live-model.mjs" \
@@ -16,18 +17,10 @@ export DEVIN_BRIDGE_OPUS_MODEL="$DEVIN_BRIDGE_MODEL"
export DEVIN_BRIDGE_HAIKU_MODEL="$DEVIN_BRIDGE_MODEL"
export DEVIN_BRIDGE_SUBAGENT_MODEL="$DEVIN_BRIDGE_MODEL"
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin up --abort-on-container-exit --exit-code-from claude-live claude-live
node -e '
const fs = require("node:fs");
const path = process.argv[1];
if (!fs.existsSync(path)) process.exit(0);
for (const line of fs.readFileSync(path, "utf8").trim().split("\n").filter(Boolean)) {
const entry = JSON.parse(line);
const suffixAllowed = /(^|\.)(devin\.ai|cognition\.ai)$/.test(entry.hostname);
const exactAllowed = ["server.codeium.com", "unleash.codeium.com"].includes(entry.hostname);
if (entry.decision === "allow" && !suffixAllowed && !exactAllowed) {
throw new Error(`unexpected allowed egress: ${entry.hostname}`);
}
}
' "$BRIDGE_SANDBOX/evidence/egress.jsonl"
bridge_assert_zero_claude_egress "$BRIDGE_SANDBOX/evidence/claude-egress.jsonl"
bridge_cleanup_compose
bridge_assert_devin_guard_audit "$BRIDGE_DEVIN_AUDIT"
bridge_assert_zero_claude_egress "$BRIDGE_CLAUDE_AUDIT"
bridge_export_guard_audit "$BRIDGE_DEVIN_AUDIT" egress.jsonl
bridge_export_guard_audit "$BRIDGE_CLAUDE_AUDIT" claude-egress.jsonl
trap - EXIT
printf 'PASS: live model %s was discovered and validated by three scenarios\n' "$devin_model"

View File

@@ -4,4 +4,5 @@ source "$(dirname "$0")/common"
cd "$BRIDGE_ROOT"
bridge_test_env node --import tsx/esm --test \
tests/unit/executor-devin-cli-agentic-core.test.ts \
tests/unit/executor-devin-cli-agentic-acp.test.ts
tests/unit/executor-devin-cli-agentic-acp.test.ts \
tests/unit/devin-bridge-network-guard.test.ts

View File

@@ -35,10 +35,16 @@ node -e '
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"]) {
@@ -49,6 +55,9 @@ node -e '
}
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",
@@ -76,6 +85,14 @@ node -e '
}
}
}
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");
@@ -167,6 +184,9 @@ 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"
@@ -184,15 +204,14 @@ grep -q 'bridge_check_devin_auth' "$BRIDGE_ROOT/scripts/devin-bridge/test-live-d
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 network-guard claude-egress-guard' "$BRIDGE_ROOT/scripts/devin-bridge/launch" || \
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 claude-egress-guard
cleanup_claude_guard() {
docker compose -f "$BRIDGE_COMPOSE" --profile offline stop claude-egress-guard >/dev/null 2>&1 || true
}
trap cleanup_claude_guard EXIT
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
@@ -231,9 +250,10 @@ docker compose -f "$BRIDGE_COMPOSE" --profile offline run --rm --no-deps claude
signal: AbortSignal.timeout(3000),
}));
'
bridge_assert_claude_guard_denials "$BRIDGE_SANDBOX/evidence/claude-egress.jsonl" || \
bridge_cleanup_compose
bridge_assert_claude_guard_denials "$BRIDGE_CLAUDE_AUDIT" || \
fail "Claude proxy denial audit proof failed"
cleanup_claude_guard
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'

View File

@@ -1,87 +1,54 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { CORE_SCHEMA, load, mergeTag } from "js-yaml";
import { isAllowedGuardHostname } from "../../docker/devin-bridge/network-guard/policy.mjs";
import {
validateAuditFileStat,
validateClaudeGuardDenials,
validateDevinAuthStatus,
validateDevinGuardAudit,
validateZeroClaudeEgress,
} from "../../scripts/devin-bridge/runtime-policy.mjs";
import { selectLiveModel } from "../../scripts/devin-bridge/select-live-model.mjs";
const root = process.cwd();
const composePath = path.join(root, "docker", "devin-bridge", "compose.yml");
const commonPath = path.join(root, "scripts", "devin-bridge", "common");
const mockE2ePath = path.join(root, "scripts", "devin-bridge", "test-e2e-mock");
const launchPath = path.join(root, "scripts", "devin-bridge", "launch");
const loginPath = path.join(root, "scripts", "devin-bridge", "login-devin");
const selectorPath = path.join(root, "scripts", "devin-bridge", "select-live-model.mjs");
const liveE2ePath = path.join(root, "scripts", "devin-bridge", "test-live-devin");
const verifierPath = path.join(root, "scripts", "devin-bridge", "verify-anthropic-isolation");
function composeConfig() {
const result = spawnSync(
"docker-compose",
[
"-f",
composePath,
"--env-file",
"/dev/null",
"--profile",
"offline",
"--profile",
"live-devin",
"config",
"--format",
"json",
],
{ cwd: root, encoding: "utf8" }
);
assert.equal(result.status, 0, result.stderr);
return JSON.parse(result.stdout);
interface ComposeService {
depends_on?: Record<string, { condition?: string }>;
environment?: Record<string, string>;
healthcheck?: { test?: unknown };
volumes?: Array<string | { source?: string }>;
}
function volumeSources(service: { volumes?: Array<string | { source?: string }> }): string[] {
return (service.volumes || []).map((mount) =>
interface ComposeConfig {
services: Record<string, ComposeService>;
}
function composeConfig(): ComposeConfig {
return load(fs.readFileSync(composePath, "utf8"), {
filename: composePath,
schema: CORE_SCHEMA.withTags(mergeTag),
}) as ComposeConfig;
}
function volumeSources(service: ComposeService): string[] {
return (service.volumes || []).map((mount: string | { source?: string }) =>
typeof mount === "string" ? mount.split(":", 1)[0] : String(mount.source || "")
);
}
function networkNames(service: { networks?: string[] | Record<string, unknown> }): string[] {
return Array.isArray(service.networks) ? service.networks : Object.keys(service.networks || {});
}
function assertAuthStatus(exitStatus: number, output: string) {
return spawnSync(
"bash",
[
"-c",
'source "$1"; bridge_assert_devin_auth_status "$2" "$3"',
"bridge-auth-test",
commonPath,
String(exitStatus),
output,
],
{ cwd: root, encoding: "utf8" }
);
}
function selectModel(document: unknown, env: NodeJS.ProcessEnv = {}) {
return spawnSync(process.execPath, ["--import", "tsx/esm", selectorPath], {
cwd: root,
encoding: "utf8",
input: JSON.stringify(document),
env: { PATH: process.env.PATH, ...env },
});
}
function runCommon(functionName: string, filePath: string) {
return spawnSync(
"bash",
["-c", 'source "$1"; "$2" "$3"', "bridge-audit-test", commonPath, functionName, filePath],
{ cwd: root, encoding: "utf8" }
);
}
test("network policy permits only Devin/Cognition and exact Codeium control-plane hosts", async () => {
const { isAllowedGuardHostname } =
await import("../../docker/devin-bridge/network-guard/policy.mjs");
test("network policy permits only the intended Devin destinations", () => {
for (const hostname of [
"api.devin.ai",
"devin.ai",
@@ -106,62 +73,68 @@ test("network policy permits only Devin/Cognition and exact Codeium control-plan
assert.equal(isAllowedGuardHostname("api.devin.ai", "deny-all"), false);
});
test("compose separates Claude config, Devin auth, and the two egress guards", () => {
const config = composeConfig();
const services = config.services;
test("compose isolates guard audit mounts and waits for healthy guards", () => {
const services = composeConfig().services;
for (const [name, service] of Object.entries(services)) {
const sources = volumeSources(service);
const hasGuardAudit = sources.some((source) => source.includes(".sandbox/guard-audit"));
assert.equal(hasGuardAudit, name === "network-guard" || name === "claude-egress-guard", name);
}
assert.match(volumeSources(services["network-guard"]).join(" "), /\.sandbox\/guard-audit\/devin/);
assert.match(
volumeSources(services["claude-egress-guard"]).join(" "),
/\.sandbox\/guard-audit\/claude/
);
for (const guard of ["network-guard", "claude-egress-guard"]) {
assert.ok(services[guard].healthcheck?.test, `${guard} healthcheck`);
}
assert.equal(
services["omniroute-live"].depends_on?.["network-guard"].condition,
"service_healthy"
);
assert.equal(services.claude.depends_on?.["claude-egress-guard"].condition, "service_healthy");
assert.equal(
services["claude-live"].depends_on?.["claude-egress-guard"].condition,
"service_healthy"
);
});
for (const [name, service] of Object.entries(services) as Array<
[string, { environment?: Record<string, string>; networks?: object; volumes?: object[] }]
>) {
test("compose keeps role-separated credentials and proxy settings", () => {
const services = composeConfig().services;
for (const [name, service] of Object.entries(services)) {
const sources = volumeSources(service);
assert.equal(
sources.some(
(source) =>
source === "claude-isolated-config" || source.endsWith("_claude-isolated-config")
),
sources.includes("claude-isolated-config"),
name === "claude" || name === "claude-live",
`${name} Claude config volume ownership`
);
assert.equal(
sources.some((source) => source === "devin-auth" || source.endsWith("_devin-auth")),
name === "omniroute-live",
`${name} Devin auth volume ownership`
`${name} Claude config ownership`
);
assert.equal(sources.includes("devin-auth"), name === "omniroute-live", `${name} Devin auth`);
}
assert.equal(
services["omniroute-live"].environment.DEVIN_BRIDGE_PROXY_URL,
services["omniroute-live"].environment?.DEVIN_BRIDGE_PROXY_URL,
"http://network-guard:8080"
);
assert.equal(services["omniroute-live"].environment.HTTP_PROXY, undefined);
assert.equal(services["omniroute-live"].environment.HTTPS_PROXY, undefined);
for (const name of ["claude", "claude-live"]) {
const env = services[name].environment;
assert.equal(env.NODE_USE_ENV_PROXY, "1");
assert.equal(env.HTTP_PROXY, "http://claude-egress-guard:8080");
assert.equal(env.HTTPS_PROXY, "http://claude-egress-guard:8080");
assert.equal(env.NO_PROXY, "omniroute");
assert.notEqual(env.HTTP_PROXY, "http://network-guard:8080");
assert.equal(services[name].environment?.HTTP_PROXY, "http://claude-egress-guard:8080");
assert.equal(services[name].environment?.NO_PROXY, "omniroute");
}
assert.equal(services["network-guard"].environment.GUARD_POLICY, "devin");
assert.equal(services["claude-egress-guard"].environment.GUARD_POLICY, "deny-all");
assert.deepEqual(networkNames(services["claude-egress-guard"]), ["bridge-internal"]);
});
test("auth status gate requires a clean server-confirmed login", () => {
assert.equal(assertAuthStatus(0, "Logged in (via Devin)\n").status, 0);
assert.notEqual(
assertAuthStatus(0, "Logged in (via Devin)\nFailed to fetch from server\n").status,
0
);
assert.notEqual(assertAuthStatus(0, "Logged out\n").status, 0);
assert.notEqual(assertAuthStatus(1, "Logged in (via Devin)\n").status, 0);
test("auth status requires the exact positive line and rejects misleading text", () => {
assert.equal(validateDevinAuthStatus(0, "Logged in (via Devin)\n").ok, true);
for (const output of [
"Not Logged in (via Devin)\n",
"prefix Logged in (via Devin) suffix\n",
"Logged out\n",
"Logged in (via Devin)\nFailed to fetch from server\n",
]) {
assert.equal(validateDevinAuthStatus(0, output).ok, false, output);
}
assert.equal(validateDevinAuthStatus(1, "Logged in (via Devin)\n").ok, false);
});
test("live model selection normalizes real family/model uid fields and prefers lightning", () => {
const result = selectModel({
test("model discovery accepts only explicit uid/id fields and detects catalog ambiguity", () => {
const selected = selectLiveModel({
models: [
{ family_uid: "swe-1.7" },
{ familyUid: "swe-1.7-lightning" },
@@ -169,82 +142,77 @@ test("live model selection normalizes real family/model uid fields and prefers l
{ modelUid: "swe-1.6" },
],
});
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "swe-1-7-lightning");
});
test("live model selection rejects normalized identifiers absent from the catalog", () => {
const result = selectModel({ models: [{ family_uid: "swe-9.9-unknown" }] });
assert.notEqual(result.status, 0);
assert.match(result.stderr, /no model identifier present/i);
});
test("login uses the official manual token flow without accepting tokens as arguments", () => {
const login = fs.readFileSync(loginPath, "utf8");
const common = fs.readFileSync(commonPath, "utf8");
assert.match(login, /bridge_run_devin auth login --force-manual-token-flow/);
assert.match(common, /exec devin "\$@"/);
assert.doesNotMatch(login, /read\s+.*token|printf\s+.*token|echo\s+.*token/i);
});
test("normal live launch uses the strict auth and proxied Devin helpers", () => {
const launch = fs.readFileSync(launchPath, "utf8");
assert.match(launch, /up -d network-guard claude-egress-guard/);
assert.match(launch, /bridge_check_devin_auth/);
assert.match(launch, /bridge_run_devin models list --format json/);
assert.doesNotMatch(launch, /\bdevin auth status\b/);
});
test("Claude audit helpers fail closed for missing, empty, partial, or any real-run record", () => {
const sandboxRoot = path.join(root, ".sandbox");
fs.mkdirSync(sandboxRoot, { recursive: true });
const auditRoot = fs.mkdtempSync(path.join(sandboxRoot, "audit-unit-"));
const auditPath = path.join(auditRoot, "claude-egress.jsonl");
try {
assert.notEqual(runCommon("bridge_assert_zero_claude_egress", auditPath).status, 0);
fs.writeFileSync(auditPath, "");
assert.equal(runCommon("bridge_assert_zero_claude_egress", auditPath).status, 0);
fs.writeFileSync(auditPath, '{"hostname":"api.anthropic.com","decision":"deny"}\n');
assert.notEqual(runCommon("bridge_assert_zero_claude_egress", auditPath).status, 0);
assert.notEqual(runCommon("bridge_assert_claude_guard_denials", auditPath).status, 0);
fs.appendFileSync(auditPath, '{"hostname":"claude.ai","decision":"deny"}\n');
assert.equal(runCommon("bridge_assert_claude_guard_denials", auditPath).status, 0);
fs.appendFileSync(auditPath, '{"hostname":"example.com","decision":"allow"}\n');
assert.notEqual(runCommon("bridge_assert_claude_guard_denials", auditPath).status, 0);
} finally {
fs.rmSync(auditRoot, { recursive: true, force: true });
}
});
test("offline and live resets precreate an empty world-writable Claude audit", () => {
const auditPath = path.join(root, ".sandbox", "evidence", "claude-egress.jsonl");
for (const resetFunction of ["bridge_reset_e2e_fixture", "bridge_reset_live_fixture"]) {
const reset = spawnSync(
"bash",
["-c", 'source "$1"; "$2"', "bridge-reset-test", commonPath, resetFunction],
{ cwd: root, encoding: "utf8" }
);
assert.equal(reset.status, 0, reset.stderr);
assert.equal(fs.existsSync(auditPath), true, resetFunction);
assert.equal(fs.statSync(auditPath).size, 0, resetFunction);
assert.equal(fs.statSync(auditPath).mode & 0o777, 0o666, resetFunction);
fs.writeFileSync(auditPath, "record");
}
});
test("verifier proves audited denials while real E2E gates require zero Claude attempts", () => {
const verifier = fs.readFileSync(verifierPath, "utf8");
const mockE2e = fs.readFileSync(mockE2ePath, "utf8");
const liveE2e = fs.readFileSync(liveE2ePath, "utf8");
assert.match(verifier, /fetch\("https:\/\/api\.anthropic\.com"/);
assert.match(verifier, /fetch\("https:\/\/claude\.ai"/);
assert.match(verifier, /bridge_assert_claude_guard_denials/);
assert.ok(
[...verifier.matchAll(/bridge_reset_claude_egress_audit/g)].length >= 2,
"deliberate proof must reset the audit before and after its own requests"
assert.equal(selected, "swe-1-7-lightning");
assert.throws(() => selectLiveModel({ metadata: { id: "swe-1.7" } }), /no model identifier/i);
assert.throws(
() => selectLiveModel({ models: [{ model_uid: "a.b" }] }, {}, [{ id: "a.b" }, { id: "a-b" }]),
/Ambiguous OmniRoute catalog normalization/
);
assert.match(mockE2e, /bridge_assert_zero_claude_egress/);
assert.match(liveE2e, /bridge_assert_zero_claude_egress/);
});
test("audit policies reject forged metadata, missing proof, and unexpected Devin hosts", () => {
const safeStat = {
isFile: () => true,
isSymbolicLink: () => false,
nlink: 1,
uid: 501,
mode: 0o100666,
};
assert.equal(validateAuditFileStat(safeStat as unknown as fs.Stats, 501), null);
assert.match(
validateAuditFileStat({ ...safeStat, nlink: 2 } as unknown as fs.Stats, 501) || "",
/link count/
);
assert.match(
validateAuditFileStat(
{ ...safeStat, isSymbolicLink: () => true } as unknown as fs.Stats,
501
) || "",
/regular file/
);
assert.equal(validateZeroClaudeEgress("").ok, true);
assert.equal(validateZeroClaudeEgress("{}\n").ok, false);
assert.equal(
validateClaudeGuardDenials(
'{"hostname":"api.anthropic.com","decision":"deny"}\n' +
'{"hostname":"claude.ai","decision":"deny"}\n'
).ok,
true
);
assert.equal(validateDevinGuardAudit("").ok, false);
assert.equal(
validateDevinGuardAudit('{"hostname":"api.devin.ai","decision":"allow"}\n').ok,
true
);
assert.equal(
validateDevinGuardAudit('{"hostname":"o1.ingest.sentry.io","decision":"deny"}\n').ok,
false
);
});
test("scripts use atomic audit resets, readiness waits, cleanup traps, and strict gates", () => {
const common = fs.readFileSync(commonPath, "utf8");
const login = fs.readFileSync(loginPath, "utf8");
const launch = fs.readFileSync(launchPath, "utf8");
const live = fs.readFileSync(liveE2ePath, "utf8");
const mock = fs.readFileSync(mockE2ePath, "utf8");
const verifier = fs.readFileSync(verifierPath, "utf8");
assert.match(common, /chmod 01777/);
assert.match(common, /mktemp/);
assert.match(common, /chmod 0666/);
assert.match(common, /mv -f/);
for (const script of [login, launch, live, verifier]) {
assert.match(script, /trap bridge_cleanup_compose EXIT/);
}
for (const script of [login, launch, live, verifier]) assert.match(script, /up -d --wait/);
assert.match(login, /auth login --force-manual-token-flow/);
assert.match(live, /bridge_assert_devin_guard_audit/);
assert.match(live, /bridge_assert_zero_claude_egress/);
assert.match(mock, /bridge_assert_zero_claude_egress/);
assert.ok([...verifier.matchAll(/bridge_reset_claude_egress_audit/g)].length >= 2);
});
test("Docker build context excludes sandbox runtime state", () => {
const dockerignore = fs.readFileSync(path.join(root, ".dockerignore"), "utf8");
assert.match(dockerignore, /^\.sandbox$/m);
});

View File

@@ -0,0 +1,217 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import http from "node:http";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createGuardProxy } from "../../docker/devin-bridge/network-guard/proxy.mjs";
import {
parseConnectAuthority,
parseTlsClientHelloSni,
} from "../../docker/devin-bridge/network-guard/policy.mjs";
function listen(server: net.Server | http.Server): Promise<number> {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") return reject(new Error("missing port"));
resolve(address.port);
});
});
}
function close(server: net.Server | http.Server): Promise<void> {
return new Promise((resolve) => server.close(() => resolve()));
}
function uint24(value: number) {
return Buffer.from([(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff]);
}
function clientHello(serverName?: string) {
const extensions: Buffer[] = [];
if (serverName) {
const name = Buffer.from(serverName, "ascii");
const entry = Buffer.concat([
Buffer.from([0]),
Buffer.from([name.length >> 8, name.length]),
name,
]);
const list = Buffer.concat([Buffer.from([entry.length >> 8, entry.length]), entry]);
extensions.push(Buffer.concat([Buffer.from([0, 0, 0, list.length]), list]));
}
const extensionBytes = Buffer.concat(extensions);
const body = Buffer.concat([
Buffer.from([3, 3]),
Buffer.alloc(32, 1),
Buffer.from([0]),
Buffer.from([0, 2, 0x13, 0x01]),
Buffer.from([1, 0]),
Buffer.from([extensionBytes.length >> 8, extensionBytes.length]),
extensionBytes,
]);
const handshake = Buffer.concat([Buffer.from([1]), uint24(body.length), body]);
return Buffer.concat([
Buffer.from([22, 3, 1, handshake.length >> 8, handshake.length]),
handshake,
]);
}
test("CONNECT authority is fixed to port 443 and ClientHello SNI is bounded", () => {
assert.deepEqual(parseConnectAuthority("api.devin.ai:443"), {
hostname: "api.devin.ai",
port: 443,
});
assert.equal(parseConnectAuthority("api.devin.ai:80"), null);
assert.equal(parseConnectAuthority("api.devin.ai"), null);
const hello = clientHello("api.devin.ai");
assert.equal(parseTlsClientHelloSni(hello.subarray(0, 8)).status, "need-more");
assert.deepEqual(parseTlsClientHelloSni(hello), {
status: "ok",
serverName: "api.devin.ai",
});
assert.deepEqual(parseTlsClientHelloSni(clientHello()), {
status: "invalid",
reason: "missing_sni",
});
assert.equal(parseTlsClientHelloSni(Buffer.from("not tls")).status, "invalid");
});
test("HTTP proxy overwrites Host and strips proxy and hop-by-hop credentials", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devin-guard-http-"));
const logPath = path.join(tmp, "audit.jsonl");
fs.writeFileSync(logPath, "");
let receivedHeaders: http.IncomingHttpHeaders = {};
const upstream = http.createServer((req, res) => {
receivedHeaders = req.headers;
res.end("forwarded");
});
const upstreamPort = await listen(upstream);
const proxy = createGuardProxy({ logPath, allowHostname: () => true });
const proxyPort = await listen(proxy);
try {
const body = await new Promise<string>((resolve, reject) => {
const req = http.request(
{
host: "127.0.0.1",
port: proxyPort,
path: `http://127.0.0.1:${upstreamPort}/proof`,
headers: {
host: "attacker.example",
"proxy-authorization": "Basic forged",
"proxy-connection": "keep-alive",
connection: "keep-alive, x-remove",
"x-remove": "forged",
},
},
(res) => {
let text = "";
res.on("data", (chunk) => (text += chunk));
res.on("end", () => resolve(text));
}
);
req.on("error", reject);
req.end();
});
assert.equal(body, "forwarded");
assert.equal(receivedHeaders.host, `127.0.0.1:${upstreamPort}`);
assert.equal(receivedHeaders["proxy-authorization"], undefined);
assert.equal(receivedHeaders["proxy-connection"], undefined);
assert.equal(receivedHeaders["x-remove"], undefined);
} finally {
await close(proxy);
await close(upstream);
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("CONNECT rejects mismatched SNI before opening an upstream socket", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devin-guard-sni-"));
const logPath = path.join(tmp, "audit.jsonl");
fs.writeFileSync(logPath, "");
let upstreamConnections = 0;
const proxy = createGuardProxy({
logPath,
allowHostname: () => true,
connectSocket: () => {
upstreamConnections += 1;
throw new Error("must not connect");
},
});
const proxyPort = await listen(proxy);
try {
await new Promise<void>((resolve, reject) => {
const socket = net.connect(proxyPort, "127.0.0.1", () => {
socket.write("CONNECT api.devin.ai:443 HTTP/1.1\r\nHost: api.devin.ai:443\r\n\r\n");
});
let sentHello = false;
socket.on("data", (chunk) => {
if (!sentHello && chunk.toString("latin1").includes("200 Connection Established")) {
sentHello = true;
socket.write(clientHello("claude.ai"));
}
});
socket.on("close", () => resolve());
socket.on("error", reject);
setTimeout(() => reject(new Error("CONNECT mismatch test timed out")), 2000).unref();
});
assert.equal(upstreamConnections, 0);
assert.match(fs.readFileSync(logPath, "utf8"), /"reason":"sni_mismatch"/);
} finally {
await close(proxy);
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("CONNECT forwards only after matching SNI is validated", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devin-guard-sni-ok-"));
const logPath = path.join(tmp, "audit.jsonl");
fs.writeFileSync(logPath, "");
let forwardedBytes = 0;
const upstream = net.createServer((socket) => {
socket.once("data", (chunk) => {
forwardedBytes += chunk.length;
socket.write("UPSTREAM_OK");
});
});
const upstreamPort = await listen(upstream);
const proxy = createGuardProxy({
logPath,
allowHostname: () => true,
connectSocket: (_port, _hostname, onConnect) =>
net.connect(upstreamPort, "127.0.0.1", onConnect),
});
const proxyPort = await listen(proxy);
try {
await new Promise<void>((resolve, reject) => {
const socket = net.connect(proxyPort, "127.0.0.1", () => {
socket.write("CONNECT api.devin.ai:443 HTTP/1.1\r\nHost: api.devin.ai:443\r\n\r\n");
});
let sentHello = false;
socket.on("data", (chunk) => {
const text = chunk.toString("latin1");
if (!sentHello && text.includes("200 Connection Established")) {
sentHello = true;
socket.write(clientHello("api.devin.ai"));
return;
}
if (text.includes("UPSTREAM_OK")) {
socket.destroy();
resolve();
}
});
socket.on("error", reject);
setTimeout(() => reject(new Error("CONNECT forwarding test timed out")), 2000).unref();
});
assert.ok(forwardedBytes > 0);
assert.match(fs.readFileSync(logPath, "utf8"), /"reason":"sni_match"/);
} finally {
await close(proxy);
await close(upstream);
fs.rmSync(tmp, { recursive: true, force: true });
}
});

View File

@@ -4,16 +4,19 @@ import fs from "node:fs";
import path from "node:path";
import { writeFileSync } from "node:fs";
import {
assertLocalAcpUrl,
buildDevinChildEnv,
DevinCliAgenticExecutor,
} from "../../open-sse/executors/devin-cli-agentic.ts";
import { devin_cli_agenticProvider } from "../../open-sse/config/providers/registry/devin-cli-agentic/index.ts";
import { getProviderCredentials } from "../../src/sse/services/auth.ts";
const isolatedRoot = path.join(process.cwd(), ".sandbox", "direct-acp-test");
process.env.HOME = path.join(isolatedRoot, "home");
process.env.DATA_DIR = path.join(isolatedRoot, "data");
process.env.SQLITE_FILE = path.join(isolatedRoot, "data", "storage.sqlite");
process.env.DEVIN_AGENTIC_HOME = process.env.HOME;
fs.mkdirSync(process.env.HOME, { recursive: true });
fs.mkdirSync(process.env.DATA_DIR, { recursive: true });
process.env.DEVIN_AGENTIC_HOME = path.join(process.cwd(), ".sandbox", "unit-home");
fs.mkdirSync(process.env.DEVIN_AGENTIC_HOME, { recursive: true });
const { assertLocalAcpUrl, buildDevinChildEnv, DevinCliAgenticExecutor } =
await import("../../open-sse/executors/devin-cli-agentic.ts");
const { devin_cli_agenticProvider } =
await import("../../open-sse/config/providers/registry/devin-cli-agentic/index.ts");
const { getProviderCredentials } = await import("../../src/sse/services/auth.ts");
async function readResponseText(response: Response) {
return await response.text();