mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
feat(executors): add isolated Claude Code bridge over Devin ACP (#8914)
Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
This commit is contained in:
5
scripts/devin-bridge/build
Executable file
5
scripts/devin-bridge/build
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/common"
|
||||
bridge_prepare_sandbox
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile offline build
|
||||
12
scripts/devin-bridge/clean
Executable file
12
scripts/devin-bridge/clean
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/common"
|
||||
if [[ "${1:-}" == "--all" ]]; then
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin down \
|
||||
--remove-orphans --volumes
|
||||
printf 'Containers, networks, and bridge-owned named volumes were removed.\n'
|
||||
else
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin down \
|
||||
--remove-orphans
|
||||
printf 'Containers and networks stopped. Named auth/config volumes were preserved; use --all to remove them.\n'
|
||||
fi
|
||||
131
scripts/devin-bridge/common
Executable file
131
scripts/devin-bridge/common
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
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_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_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
|
||||
cp -R "$BRIDGE_ROOT/tests/fixtures/devin-bridge/e2e-workspace/." \
|
||||
"$BRIDGE_SANDBOX/e2e-workspace/"
|
||||
rm -f "$BRIDGE_SANDBOX/e2e-workspace/.e2e-hook.log" \
|
||||
"$BRIDGE_SANDBOX/evidence/claude-stream.jsonl" \
|
||||
"$BRIDGE_SANDBOX/evidence/mock-acp.jsonl"
|
||||
bridge_reset_claude_egress_audit
|
||||
}
|
||||
bridge_reset_live_fixture() {
|
||||
bridge_prepare_sandbox
|
||||
cp -R "$BRIDGE_ROOT/tests/fixtures/devin-bridge/e2e-workspace/." \
|
||||
"$BRIDGE_SANDBOX/live-workspace/"
|
||||
rm -f "$BRIDGE_SANDBOX/live-workspace/.e2e-hook.log" \
|
||||
"$BRIDGE_SANDBOX/evidence/live-analysis.jsonl" \
|
||||
"$BRIDGE_SANDBOX/evidence/live-fix.jsonl" \
|
||||
"$BRIDGE_SANDBOX/evidence/live-command.jsonl" \
|
||||
"$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
|
||||
env HOME="$BRIDGE_SANDBOX/home" DATA_DIR="$BRIDGE_SANDBOX/test-data" SQLITE_FILE="$BRIDGE_SANDBOX/test-data/storage.sqlite" DEVIN_AGENTIC_HOME="$BRIDGE_SANDBOX/home" "$@"
|
||||
}
|
||||
|
||||
bridge_run_devin() {
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps \
|
||||
omniroute-live sh -ceu '
|
||||
trusted_proxy=http://network-guard:8080
|
||||
test "${DEVIN_BRIDGE_PROXY_URL:-}" = "$trusted_proxy"
|
||||
export HTTP_PROXY="$trusted_proxy" HTTPS_PROXY="$trusted_proxy"
|
||||
unset ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy
|
||||
exec devin "$@"
|
||||
' bridge-devin "$@"
|
||||
}
|
||||
|
||||
bridge_assert_devin_auth_status() {
|
||||
local exit_status="$1"
|
||||
local output="$2"
|
||||
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() {
|
||||
local output
|
||||
local exit_status
|
||||
set +e
|
||||
output="$(bridge_run_devin auth status 2>&1)"
|
||||
exit_status=$?
|
||||
set -e
|
||||
bridge_assert_devin_auth_status "$exit_status" "$output"
|
||||
printf 'PASS: Devin authentication confirmed\n'
|
||||
}
|
||||
|
||||
bridge_assert_zero_claude_egress() {
|
||||
local audit_path="$1"
|
||||
bridge_validate_guard_audit claude-zero "$audit_path"
|
||||
}
|
||||
|
||||
bridge_assert_claude_guard_denials() {
|
||||
local audit_path="$1"
|
||||
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
|
||||
}
|
||||
28
scripts/devin-bridge/launch
Executable file
28
scripts/devin-bridge/launch
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/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"
|
||||
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" \
|
||||
<"$BRIDGE_SANDBOX/evidence/live-models.json")"
|
||||
export DEVIN_BRIDGE_MODEL="devin-cli-agentic/$devin_model"
|
||||
export DEVIN_BRIDGE_SONNET_MODEL="${DEVIN_BRIDGE_SONNET_MODEL:-$DEVIN_BRIDGE_MODEL}"
|
||||
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
|
||||
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
|
||||
13
scripts/devin-bridge/login-devin
Executable file
13
scripts/devin-bridge/login-devin
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
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
|
||||
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"
|
||||
111
scripts/devin-bridge/runtime-policy.mjs
Normal file
111
scripts/devin-bridge/runtime-policy.mjs
Normal file
@@ -0,0 +1,111 @@
|
||||
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.some((line) => /^Logged in \(via Devin\)\.?$/.test(line))) {
|
||||
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" };
|
||||
let sawAllowedDevinRequest = false;
|
||||
for (const entry of entries) {
|
||||
if (entry.decision === "deny") {
|
||||
if (/anthropic|claude\.ai/i.test(normalizedHostname(entry.hostname))) {
|
||||
return { ok: false, error: `forbidden Devin egress attempt: ${String(entry.hostname)}` };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry.decision !== "allow" || !isAllowedDevinAuditHostname(entry.hostname)) {
|
||||
return { ok: false, error: `unexpected Devin egress record: ${String(entry.hostname)}` };
|
||||
}
|
||||
sawAllowedDevinRequest = true;
|
||||
}
|
||||
return sawAllowedDevinRequest
|
||||
? { ok: true }
|
||||
: { ok: false, error: "Devin egress audit has no approved request" };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
101
scripts/devin-bridge/select-live-model.mjs
Normal file
101
scripts/devin-bridge/select-live-model.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/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 candidateFields = new Set([
|
||||
"model_id",
|
||||
"modelId",
|
||||
"model_uid",
|
||||
"modelUid",
|
||||
"family_uid",
|
||||
"familyUid",
|
||||
]);
|
||||
|
||||
function normalizeModelId(value) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function collect(value, candidates) {
|
||||
if (Array.isArray(value)) {
|
||||
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) &&
|
||||
/^[a-z0-9][a-z0-9._/-]*$/i.test(nested)
|
||||
) {
|
||||
candidates.push(nested);
|
||||
}
|
||||
collect(nested, candidates);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
24
scripts/devin-bridge/test-contract
Executable file
24
scripts/devin-bridge/test-contract
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/common"
|
||||
bridge_prepare_sandbox
|
||||
rm -f "$BRIDGE_SANDBOX/evidence/mock-acp.jsonl"
|
||||
"$(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 contract contract
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const rows = fs.readFileSync(process.argv[1], "utf8").trim().split("\n").map(JSON.parse);
|
||||
const repairRows = rows.filter((row) => row.scenario === "narrative-repair");
|
||||
if (
|
||||
rows.length !== 7 ||
|
||||
rows.some((row) => row.provider !== "devin-cli-agentic") ||
|
||||
repairRows.length !== 2 ||
|
||||
repairRows[0].stage !== "initial" ||
|
||||
repairRows[1].stage !== "repair"
|
||||
) {
|
||||
throw new Error("wire contract observed a missing or non-Devin provider");
|
||||
}
|
||||
' "$BRIDGE_SANDBOX/evidence/mock-acp.jsonl"
|
||||
printf 'PASS: bridge wire contract suite completed without provider fallback\n'
|
||||
16
scripts/devin-bridge/test-e2e-mock
Executable file
16
scripts/devin-bridge/test-e2e-mock
Executable file
@@ -0,0 +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 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_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'
|
||||
37
scripts/devin-bridge/test-live-devin
Executable file
37
scripts/devin-bridge/test-live-devin
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
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 live-devin up -d --wait network-guard
|
||||
bridge_check_devin_auth
|
||||
models_file="$BRIDGE_SANDBOX/evidence/live-models.json"
|
||||
if [[ -n "${DEVIN_BRIDGE_DISCOVERED_MODEL:-}" ]]; then
|
||||
devin_model="$DEVIN_BRIDGE_DISCOVERED_MODEL"
|
||||
else
|
||||
for attempt in 1 2 3; do
|
||||
if bridge_run_devin models list --format json >"$models_file"; then
|
||||
break
|
||||
fi
|
||||
[[ "$attempt" == 3 ]] && exit 1
|
||||
sleep 1
|
||||
done
|
||||
devin_model="$(node --import tsx/esm "$BRIDGE_ROOT/scripts/devin-bridge/select-live-model.mjs" \
|
||||
<"$models_file")"
|
||||
fi
|
||||
export DEVIN_BRIDGE_MODEL="devin-cli-agentic/$devin_model"
|
||||
export DEVIN_BRIDGE_SONNET_MODEL="$DEVIN_BRIDGE_MODEL"
|
||||
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
|
||||
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"
|
||||
9
scripts/devin-bridge/test-unit
Executable file
9
scripts/devin-bridge/test-unit
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
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/devin-bridge-network-guard.test.ts \
|
||||
tests/unit/devin-bridge-live-runtime.test.ts
|
||||
125
scripts/devin-bridge/validate-claude-evidence.mjs
Normal file
125
scripts/devin-bridge/validate-claude-evidence.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function contentBlocks(message) {
|
||||
return Array.isArray(message?.message?.content) ? message.message.content : [];
|
||||
}
|
||||
|
||||
export function validateClaudeEvidenceText(text, options) {
|
||||
const marker = String(options?.marker || "").trim();
|
||||
const requiredTools = Array.isArray(options?.requiredTools) ? options.requiredTools : [];
|
||||
if (!marker) throw new Error("A final marker is required");
|
||||
|
||||
const toolUses = new Map();
|
||||
const successfulResults = new Set();
|
||||
const slashCommands = new Set();
|
||||
const skills = new Set();
|
||||
let finalResult = null;
|
||||
|
||||
for (const [index, rawLine] of String(text).split(/\r?\n/).entries()) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch {
|
||||
throw new Error(`Invalid Claude evidence JSON at line ${index + 1}`);
|
||||
}
|
||||
|
||||
if (event?.type === "system" && event?.subtype === "init") {
|
||||
for (const command of Array.isArray(event.slash_commands) ? event.slash_commands : []) {
|
||||
slashCommands.add(String(command));
|
||||
}
|
||||
for (const skill of Array.isArray(event.skills) ? event.skills : []) {
|
||||
skills.add(String(skill));
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of contentBlocks(event)) {
|
||||
if (block?.type === "tool_use" && typeof block.id === "string") {
|
||||
toolUses.set(block.id, { name: String(block.name || ""), input: block.input || {} });
|
||||
}
|
||||
if (
|
||||
block?.type === "tool_result" &&
|
||||
typeof block.tool_use_id === "string" &&
|
||||
block.is_error !== true
|
||||
) {
|
||||
successfulResults.add(block.tool_use_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (event?.type === "result") finalResult = event;
|
||||
}
|
||||
|
||||
if (!finalResult || finalResult.subtype !== "success" || finalResult.is_error === true) {
|
||||
throw new Error("Claude evidence has no successful terminal result");
|
||||
}
|
||||
const resultText = String(finalResult.result || "");
|
||||
const incompleteResult = [
|
||||
/(?:^|\n)\s*(?:\*\*)?blocker(?:\*\*)?\s*:/im,
|
||||
/\btask (?:is|remains) (?:not complete|incomplete)\b/i,
|
||||
/(?:^|\n)\s*(?:[-*]\s*)?(?:\*\*)?next steps? needed(?:\*\*)?\s*:/im,
|
||||
].some((pattern) => pattern.test(resultText));
|
||||
if (incompleteResult) {
|
||||
throw new Error("Claude terminal result explicitly reports incomplete work");
|
||||
}
|
||||
if (options?.requiredSlashCommand && !slashCommands.has(options.requiredSlashCommand)) {
|
||||
throw new Error(`Claude did not load required slash command: ${options.requiredSlashCommand}`);
|
||||
}
|
||||
if (options?.requiredSkill && !skills.has(options.requiredSkill)) {
|
||||
throw new Error(`Claude did not load required skill: ${options.requiredSkill}`);
|
||||
}
|
||||
const markerIsStandalone = resultText.split(/\r?\n/).some((line) => line.trim() === marker);
|
||||
|
||||
for (const requiredTool of requiredTools) {
|
||||
if (![...toolUses.values()].some((tool) => tool.name === requiredTool)) {
|
||||
throw new Error(`Claude did not request required client-owned tool: ${requiredTool}`);
|
||||
}
|
||||
}
|
||||
|
||||
const npmTestSucceeded = [...toolUses.entries()].some(
|
||||
([id, tool]) =>
|
||||
tool.name === "Bash" &&
|
||||
/\bnpm\s+test\b/.test(String(tool.input?.command || "")) &&
|
||||
successfulResults.has(id)
|
||||
);
|
||||
if (options?.requireSuccessfulNpmTest) {
|
||||
if (!npmTestSucceeded) throw new Error("Claude evidence has no successful npm test tool turn");
|
||||
}
|
||||
const markerIsCorroborated =
|
||||
options?.requireSuccessfulNpmTest && npmTestSucceeded && resultText.includes(marker);
|
||||
const explicitCompletionIsCorroborated =
|
||||
options?.acceptExplicitCompletion === true &&
|
||||
npmTestSucceeded &&
|
||||
/\btask is complete\b/i.test(resultText);
|
||||
if (!markerIsStandalone && !markerIsCorroborated && !explicitCompletionIsCorroborated) {
|
||||
throw new Error(`Claude result has no standalone marker: ${marker}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
const [
|
||||
evidencePath,
|
||||
marker,
|
||||
requiredTools = "",
|
||||
requireNpmTest = "false",
|
||||
requiredSlashCommand = "",
|
||||
requiredSkill = "",
|
||||
acceptExplicitCompletion = "false",
|
||||
] = process.argv.slice(2);
|
||||
if (!evidencePath)
|
||||
throw new Error("Usage: validate-claude-evidence.mjs FILE MARKER [TOOLS] [NPM_TEST]");
|
||||
validateClaudeEvidenceText(fs.readFileSync(evidencePath, "utf8"), {
|
||||
marker,
|
||||
requiredTools: requiredTools
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
requireSuccessfulNpmTest: requireNpmTest === "true",
|
||||
requiredSlashCommand: requiredSlashCommand || undefined,
|
||||
requiredSkill: requiredSkill || undefined,
|
||||
acceptExplicitCompletion: acceptExplicitCompletion === "true",
|
||||
});
|
||||
process.stdout.write(`PASS: validated Claude evidence for ${marker}\n`);
|
||||
}
|
||||
259
scripts/devin-bridge/verify-anthropic-isolation
Executable file
259
scripts/devin-bridge/verify-anthropic-isolation
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user