mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
test: add isolated Claude Devin bridge harness
This commit is contained in:
6
.env.devin-bridge.example
Normal file
6
.env.devin-bridge.example
Normal file
@@ -0,0 +1,6 @@
|
||||
ENABLE_LIVE_DEVIN_TESTS=0
|
||||
DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -72,6 +72,7 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.devin-bridge.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
@@ -207,6 +208,8 @@ scripts/i18n/_pending-keys.json
|
||||
.agents/
|
||||
.antigravitycli/
|
||||
.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
@@ -243,7 +246,7 @@ _artifacts/ # release-green artifacts
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
.artifacts/
|
||||
.sandbox/
|
||||
.sandbox/ # isolated Devin bridge workspaces, evidence, and test databases
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
|
||||
57
docker/devin-bridge/Dockerfile
Normal file
57
docker/devin-bridge/Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
||||
FROM node:26.0.0-bookworm-slim
|
||||
|
||||
ARG CLAUDE_CODE_VERSION=2.1.220
|
||||
ARG DEVIN_CLI_VERSION=3000.2.17
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl git bash python3 make g++ tini \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& npm install --global "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
|
||||
|
||||
RUN set -eu; \
|
||||
case "${TARGETARCH}" in \
|
||||
amd64) devin_arch=x86_64-unknown-linux; devin_sha=f0e1e9363afc6ee68c4ef87bab4aeb7ff5cc08a5fa838350ef3ceefdbb2a2be2 ;; \
|
||||
arm64) devin_arch=aarch64-unknown-linux; devin_sha=116dc71ef085a922bc3ff0ea0377d4b26c529a431d58246e36572913e2d25624 ;; \
|
||||
*) echo "Unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL "https://static.devin.ai/cli/${DEVIN_CLI_VERSION}/devin-${DEVIN_CLI_VERSION}-${devin_arch}.tar.gz" -o /tmp/devin.tar.gz; \
|
||||
echo "${devin_sha} /tmp/devin.tar.gz" | sha256sum -c -; \
|
||||
tar -xzf /tmp/devin.tar.gz -C /tmp; \
|
||||
install -m 0755 "$(find /tmp -type f -name devin | head -1)" /usr/local/bin/devin; \
|
||||
rm -rf /tmp/devin.tar.gz /tmp/devin-*
|
||||
|
||||
RUN groupadd --gid 10001 bridge \
|
||||
&& useradd --uid 10001 --gid bridge --create-home --home-dir /home/bridge --shell /bin/bash bridge \
|
||||
&& mkdir -p /opt/omniroute /workspace \
|
||||
&& chown -R bridge:bridge /opt/omniroute /workspace
|
||||
|
||||
WORKDIR /opt/omniroute
|
||||
USER bridge
|
||||
COPY --chown=bridge:bridge package.json package-lock.json .npmrc ./
|
||||
RUN npm ci --ignore-scripts --no-audit --fund=false
|
||||
COPY --chown=bridge:bridge . .
|
||||
RUN npm rebuild better-sqlite3 || true
|
||||
|
||||
ENV HOME=/home/bridge \
|
||||
CLAUDE_CONFIG_DIR=/home/bridge/.claude-devin-isolated \
|
||||
DEVIN_AGENTIC_HOME=/home/bridge \
|
||||
DATA_DIR=/home/bridge/.omniroute-isolated \
|
||||
SQLITE_FILE=/home/bridge/.omniroute-isolated/storage.sqlite \
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||||
DISABLE_TELEMETRY=1 \
|
||||
DISABLE_ERROR_REPORTING=1 \
|
||||
DISABLE_AUTOUPDATER=1 \
|
||||
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN mkdir -p /home/bridge/.claude-devin-isolated /home/bridge/.local/share/devin \
|
||||
/home/bridge/.omniroute-isolated
|
||||
|
||||
RUN DATA_DIR=/tmp/omniroute-build-data \
|
||||
SQLITE_FILE=/tmp/omniroute-build-data/storage.sqlite \
|
||||
npm run build \
|
||||
&& rm -rf /tmp/omniroute-build-data
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["bash"]
|
||||
164
docker/devin-bridge/compose.yml
Normal file
164
docker/devin-bridge/compose.yml
Normal file
@@ -0,0 +1,164 @@
|
||||
name: omniroute-devin-bridge
|
||||
|
||||
x-isolated-environment: &isolated-environment
|
||||
HOME: /home/bridge
|
||||
CLAUDE_CONFIG_DIR: /home/bridge/.claude-devin-isolated
|
||||
DEVIN_AGENTIC_HOME: /home/bridge
|
||||
DATA_DIR: /home/bridge/.omniroute-isolated
|
||||
SQLITE_FILE: /home/bridge/.omniroute-isolated/storage.sqlite
|
||||
ANTHROPIC_BASE_URL: http://omniroute:20128
|
||||
ANTHROPIC_AUTH_TOKEN: sk-local-devin-gateway
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
||||
DISABLE_TELEMETRY: "1"
|
||||
DISABLE_ERROR_REPORTING: "1"
|
||||
DISABLE_AUTOUPDATER: "1"
|
||||
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
|
||||
DEVIN_BRIDGE_MODEL: ${DEVIN_BRIDGE_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
ANTHROPIC_MODEL: ${DEVIN_BRIDGE_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: ${DEVIN_BRIDGE_SONNET_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: ${DEVIN_BRIDGE_OPUS_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${DEVIN_BRIDGE_HAIKU_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: ${DEVIN_BRIDGE_SUBAGENT_MODEL:-devin-cli-agentic/swe-1-7}
|
||||
REQUIRE_API_KEY: "true"
|
||||
OMNIROUTE_API_KEY: sk-local-devin-gateway
|
||||
|
||||
x-runtime: &runtime
|
||||
image: omniroute-devin-bridge:local
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/devin-bridge/Dockerfile
|
||||
args:
|
||||
CLAUDE_CODE_VERSION: 2.1.220
|
||||
DEVIN_CLI_VERSION: 3000.2.17
|
||||
user: "10001:10001"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,nodev,size=256m
|
||||
- /opt/omniroute/.source:rw,nosuid,nodev,size=16m,uid=10001,gid=10001
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
environment: *isolated-environment
|
||||
networks: [bridge-internal]
|
||||
|
||||
services:
|
||||
omniroute:
|
||||
<<: *runtime
|
||||
profiles: [offline]
|
||||
hostname: omniroute
|
||||
environment:
|
||||
<<: *isolated-environment
|
||||
CLI_DEVIN_AGENTIC_BIN: /opt/omniroute/docker/devin-bridge/mock-devin.mjs
|
||||
DEVIN_BRIDGE_MOCK_LOG: /evidence/mock-acp.jsonl
|
||||
command: ["npm", "run", "start"]
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:20128/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 60
|
||||
volumes:
|
||||
- claude-isolated-config:/home/bridge/.claude-devin-isolated
|
||||
- omniroute-offline-data:/home/bridge/.omniroute-isolated
|
||||
- ../../.sandbox/evidence:/evidence
|
||||
- ./mock-devin.mjs:/opt/omniroute/docker/devin-bridge/mock-devin.mjs:ro
|
||||
|
||||
claude:
|
||||
<<: *runtime
|
||||
profiles: [offline]
|
||||
depends_on:
|
||||
omniroute:
|
||||
condition: service_healthy
|
||||
working_dir: /workspace
|
||||
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-e2e.sh"]
|
||||
volumes:
|
||||
- claude-isolated-config:/home/bridge/.claude-devin-isolated
|
||||
- ../../.sandbox/e2e-workspace:/workspace
|
||||
- ../../.sandbox/evidence:/evidence
|
||||
- ./run-claude-e2e.sh:/opt/omniroute/docker/devin-bridge/run-claude-e2e.sh:ro
|
||||
|
||||
contract:
|
||||
<<: *runtime
|
||||
profiles: [offline]
|
||||
depends_on:
|
||||
omniroute:
|
||||
condition: service_healthy
|
||||
command: ["node", "/opt/omniroute/docker/devin-bridge/run-contract.mjs"]
|
||||
volumes:
|
||||
- claude-isolated-config:/home/bridge/.claude-devin-isolated
|
||||
- ./run-contract.mjs:/opt/omniroute/docker/devin-bridge/run-contract.mjs:ro
|
||||
|
||||
network-guard:
|
||||
image: node:26.0.0-bookworm-slim
|
||||
profiles: [live-devin]
|
||||
user: "10001:10001"
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
command: ["node", "/guard/proxy.mjs"]
|
||||
environment:
|
||||
GUARD_LISTEN: 0.0.0.0:8080
|
||||
GUARD_ALLOW_SUFFIXES: .devin.ai,.cognition.ai
|
||||
GUARD_LOG: /logs/egress.jsonl
|
||||
volumes:
|
||||
- ./network-guard:/guard:ro
|
||||
- ../../.sandbox/evidence:/logs
|
||||
networks: [bridge-internal, guard-egress]
|
||||
|
||||
omniroute-live:
|
||||
<<: *runtime
|
||||
profiles: [live-devin]
|
||||
hostname: omniroute
|
||||
depends_on: [network-guard]
|
||||
environment:
|
||||
<<: *isolated-environment
|
||||
CLI_DEVIN_AGENTIC_BIN: /usr/local/bin/devin
|
||||
HTTPS_PROXY: http://network-guard:8080
|
||||
HTTP_PROXY: http://network-guard:8080
|
||||
NO_PROXY: localhost,127.0.0.1,omniroute
|
||||
command: ["npm", "run", "start"]
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:20128/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 60
|
||||
volumes:
|
||||
- claude-isolated-config:/home/bridge/.claude-devin-isolated
|
||||
- devin-auth:/home/bridge/.local/share/devin
|
||||
- omniroute-live-data:/home/bridge/.omniroute-isolated
|
||||
|
||||
claude-live:
|
||||
<<: *runtime
|
||||
profiles: [live-devin]
|
||||
depends_on:
|
||||
omniroute-live:
|
||||
condition: service_healthy
|
||||
working_dir: /workspace
|
||||
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-live-e2e.sh"]
|
||||
volumes:
|
||||
- claude-isolated-config:/home/bridge/.claude-devin-isolated
|
||||
- devin-auth:/home/bridge/.local/share/devin
|
||||
- ../../.sandbox/live-workspace:/workspace
|
||||
- ../../.sandbox/evidence:/evidence
|
||||
- ./run-claude-live-e2e.sh:/opt/omniroute/docker/devin-bridge/run-claude-live-e2e.sh:ro
|
||||
|
||||
networks:
|
||||
bridge-internal:
|
||||
internal: true
|
||||
guard-egress: {}
|
||||
|
||||
volumes:
|
||||
claude-isolated-config: {}
|
||||
devin-auth: {}
|
||||
omniroute-offline-data: {}
|
||||
omniroute-live-data: {}
|
||||
184
docker/devin-bridge/mock-devin.mjs
Executable file
184
docker/devin-bridge/mock-devin.mjs
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import readline from "node:readline";
|
||||
|
||||
if (
|
||||
process.argv[2] !== "acp" ||
|
||||
process.argv[3] !== "--agent-type" ||
|
||||
process.argv[4] !== "summarizer"
|
||||
) {
|
||||
process.exit(64);
|
||||
}
|
||||
|
||||
const logFile = process.env.DEVIN_BRIDGE_MOCK_LOG || "/evidence/mock-acp.jsonl";
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
|
||||
const log = (value) => fs.appendFileSync(logFile, `${JSON.stringify(value)}\n`);
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Skill",
|
||||
arguments: { skill: "bridge-proof" },
|
||||
},
|
||||
{
|
||||
name: "Bash",
|
||||
arguments: {
|
||||
command: "find . -maxdepth 2 -type f -print",
|
||||
description: "Locate the fixture files",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Read",
|
||||
arguments: { file_path: "/workspace/math.js" },
|
||||
},
|
||||
{
|
||||
name: "Edit",
|
||||
arguments: {
|
||||
file_path: "/workspace/math.js",
|
||||
old_string: "return a - b;",
|
||||
new_string: "return a * b;",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bash",
|
||||
arguments: { command: "npm test", description: "Run the fixture tests" },
|
||||
},
|
||||
{
|
||||
name: "Edit",
|
||||
arguments: {
|
||||
file_path: "/workspace/math.js",
|
||||
old_string: "return a * b;",
|
||||
new_string: "return a + b;",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bash",
|
||||
arguments: { command: "npm test", description: "Confirm the corrected fixture" },
|
||||
},
|
||||
];
|
||||
|
||||
rl.on("line", (line) => {
|
||||
const message = JSON.parse(line);
|
||||
if (message.method === "initialize") {
|
||||
if (message.params?.protocolVersion !== 1) {
|
||||
send({ jsonrpc: "2.0", id: message.id, error: { code: -32602, message: "ACP v1 required" } });
|
||||
return;
|
||||
}
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: 1 } });
|
||||
} else if (message.method === "session/new") {
|
||||
if (message.params?.cwd !== "/home/bridge" || !Array.isArray(message.params?.mcpServers)) {
|
||||
send({ jsonrpc: "2.0", id: message.id, error: { code: -32602, message: "unsafe session" } });
|
||||
return;
|
||||
}
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { sessionId: "offline" } });
|
||||
} else if (message.method === "session/prompt") {
|
||||
const prompt = String(message.params?.prompt?.[0]?.text || "");
|
||||
if (prompt.includes("CONTRACT_AFTER_TOOL")) {
|
||||
log({ provider: "devin-cli-agentic", scenario: "after-tool" });
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "session/update",
|
||||
params: {
|
||||
sessionId: "offline",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "contract continued" },
|
||||
},
|
||||
},
|
||||
});
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
|
||||
return;
|
||||
}
|
||||
if (prompt.includes("CONTRACT_EXIT")) {
|
||||
log({ provider: "devin-cli-agentic", scenario: "exit" });
|
||||
process.exit(7);
|
||||
}
|
||||
if (prompt.includes("CONTRACT_ERROR")) {
|
||||
log({ provider: "devin-cli-agentic", scenario: "error" });
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
id: message.id,
|
||||
error: { code: -32000, message: "deterministic upstream failure" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (prompt.includes("CONTRACT_TEXT")) {
|
||||
log({ provider: "devin-cli-agentic", scenario: "text" });
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "session/update",
|
||||
params: {
|
||||
sessionId: "offline",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: "contract text" },
|
||||
},
|
||||
},
|
||||
});
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
|
||||
return;
|
||||
}
|
||||
if (prompt.includes("CONTRACT_TOOL")) {
|
||||
log({ provider: "devin-cli-agentic", scenario: "tool" });
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "session/update",
|
||||
params: {
|
||||
sessionId: "offline",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: '<tool>{"name":"Read","arguments":{"file_path":"/workspace/math.js"}}</tool>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
|
||||
return;
|
||||
}
|
||||
const resultCount = (prompt.match(/\[Tool Result\]/g) || []).length;
|
||||
if (!prompt.includes("CLAUDE_MD_BRIDGE_ACTIVE") || !prompt.includes("COMMAND_BRIDGE_ACTIVE")) {
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
id: message.id,
|
||||
error: { code: -32602, message: "Claude project context missing" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const action = actions[resultCount];
|
||||
const text = action
|
||||
? `<tool>${JSON.stringify(action)}</tool>`
|
||||
: "BRIDGE_E2E_COMPLETE CLAUDE_MD_BRIDGE_ACTIVE SKILL_BRIDGE_ACTIVE COMMAND_BRIDGE_ACTIVE";
|
||||
if (!action && !prompt.includes("SKILL_BRIDGE_ACTIVE")) {
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
id: message.id,
|
||||
error: { code: -32602, message: "Skill result missing" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
log({
|
||||
provider: "devin-cli-agentic",
|
||||
model: message.params?.model || "swe-1-7",
|
||||
resultCount,
|
||||
action: action?.name || "final",
|
||||
});
|
||||
const midpoint = Math.max(1, Math.floor(text.length / 2));
|
||||
for (const chunk of [text.slice(0, midpoint), text.slice(midpoint)]) {
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "session/update",
|
||||
params: {
|
||||
sessionId: "offline",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: { type: "text", text: chunk },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
|
||||
}
|
||||
});
|
||||
58
docker/devin-bridge/network-guard/proxy.mjs
Normal file
58
docker/devin-bridge/network-guard/proxy.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import http from "node:http";
|
||||
import net from "node:net";
|
||||
import fs from "node:fs";
|
||||
|
||||
const [host, portText] = (process.env.GUARD_LISTEN || "0.0.0.0:8080").split(":");
|
||||
const port = Number(portText);
|
||||
const suffixes = (process.env.GUARD_ALLOW_SUFFIXES || "")
|
||||
.split(",")
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const logPath = process.env.GUARD_LOG || "/tmp/egress.jsonl";
|
||||
|
||||
function allowed(hostname) {
|
||||
const value = hostname.toLowerCase().replace(/\.$/, "");
|
||||
return suffixes.some((suffix) => value === suffix.slice(1) || value.endsWith(suffix));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
});
|
||||
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, () => {
|
||||
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
||||
if (head.length) upstream.write(head);
|
||||
upstream.pipe(client);
|
||||
client.pipe(upstream);
|
||||
});
|
||||
upstream.on("error", () => client.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"));
|
||||
});
|
||||
|
||||
server.listen(port, host);
|
||||
27
docker/devin-bridge/run-claude-e2e.sh
Executable file
27
docker/devin-bridge/run-claude-e2e.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BEDROCK_BASE_URL ANTHROPIC_VERTEX_BASE_URL
|
||||
unset CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY
|
||||
|
||||
set -o pipefail
|
||||
check() {
|
||||
"$@"
|
||||
printf 'E2E check passed: %s\n' "$*"
|
||||
}
|
||||
|
||||
claude -p --output-format stream-json --verbose --max-turns 12 \
|
||||
--permission-mode bypassPermissions \
|
||||
"/bridge-check" | tee /evidence/claude-stream.jsonl
|
||||
|
||||
if grep -Eqi 'log[ -]?in|authenticate.*anthropic|claude\.ai' /evidence/claude-stream.jsonl; then
|
||||
echo "Claude Code requested forbidden authentication" >&2
|
||||
exit 1
|
||||
fi
|
||||
check grep -q 'return a + b;' /workspace/math.js
|
||||
npm test
|
||||
check grep -q 'Skill' /workspace/.e2e-hook.log
|
||||
check grep -q 'Read' /workspace/.e2e-hook.log
|
||||
check grep -q 'Edit' /workspace/.e2e-hook.log
|
||||
check grep -q 'Bash' /workspace/.e2e-hook.log
|
||||
check grep -q 'BRIDGE_E2E_COMPLETE' /evidence/claude-stream.jsonl
|
||||
31
docker/devin-bridge/run-claude-live-e2e.sh
Normal file
31
docker/devin-bridge/run-claude-live-e2e.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BEDROCK_BASE_URL ANTHROPIC_VERTEX_BASE_URL
|
||||
unset CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY
|
||||
|
||||
run_scenario() {
|
||||
local evidence_file="$1"
|
||||
local prompt="$2"
|
||||
claude -p --output-format stream-json --verbose --max-turns 12 \
|
||||
--permission-mode bypassPermissions "$prompt" | tee "$evidence_file"
|
||||
if grep -Eqi 'log[ -]?in|authenticate.*anthropic|claude\.ai' "$evidence_file"; then
|
||||
echo "Claude Code requested forbidden authentication" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_scenario /evidence/live-analysis.jsonl \
|
||||
"Read CLAUDE.md, inspect math.js and its test without editing, explain the defect, then end with LIVE_ANALYSIS_COMPLETE."
|
||||
grep -q LIVE_ANALYSIS_COMPLETE /evidence/live-analysis.jsonl
|
||||
|
||||
run_scenario /evidence/live-fix.jsonl \
|
||||
"Fix the defect in math.js, run npm test, and end with LIVE_FIX_COMPLETE only after the test passes."
|
||||
grep -q 'return a + b;' /workspace/math.js
|
||||
npm test
|
||||
grep -q LIVE_FIX_COMPLETE /evidence/live-fix.jsonl
|
||||
|
||||
run_scenario /evidence/live-command.jsonl "/bridge-check"
|
||||
grep -q BRIDGE_E2E_COMPLETE /evidence/live-command.jsonl
|
||||
|
||||
printf 'PASS: three live Devin-backed Claude Code scenarios completed\n'
|
||||
115
docker/devin-bridge/run-contract.mjs
Normal file
115
docker/devin-bridge/run-contract.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const endpoint = "http://omniroute:20128/v1/messages";
|
||||
const headers = {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
"x-api-key": "sk-local-devin-gateway",
|
||||
};
|
||||
const model = process.env.DEVIN_BRIDGE_MODEL || "devin-cli-agentic/swe-1-7";
|
||||
|
||||
async function request(prompt, extra = {}) {
|
||||
return fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
max_tokens: 256,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
...extra,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const textReply = await request("CONTRACT_TEXT");
|
||||
assert.equal(textReply.status, 200);
|
||||
assert.match(textReply.headers.get("content-type") || "", /application\/json/);
|
||||
const textBody = await textReply.json();
|
||||
assert.equal(textBody.type, "message");
|
||||
assert.equal(textBody.role, "assistant");
|
||||
assert.equal(textBody.stop_reason, "end_turn");
|
||||
assert.deepEqual(textBody.content, [{ type: "text", text: "contract text" }]);
|
||||
|
||||
const toolReply = await request("CONTRACT_TOOL", {
|
||||
stream: true,
|
||||
tools: [
|
||||
{
|
||||
name: "Read",
|
||||
description: "Read a file",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: { file_path: { type: "string" } },
|
||||
required: ["file_path"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(toolReply.status, 200);
|
||||
assert.match(toolReply.headers.get("content-type") || "", /text\/event-stream/);
|
||||
const toolStream = await toolReply.text();
|
||||
const eventNames = toolStream
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("event: "))
|
||||
.map((line) => line.slice(7));
|
||||
assert.deepEqual(eventNames, [
|
||||
"message_start",
|
||||
"content_block_start",
|
||||
"content_block_delta",
|
||||
"content_block_stop",
|
||||
"message_delta",
|
||||
"message_stop",
|
||||
]);
|
||||
const toolEvents = toolStream
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data: "))
|
||||
.map((line) => JSON.parse(line.slice(6)));
|
||||
const toolUse = toolEvents.find((event) => event.type === "content_block_start")?.content_block;
|
||||
assert.equal(toolUse?.type, "tool_use");
|
||||
assert.equal(toolUse?.name, "Read");
|
||||
assert.match(toolUse?.id || "", /^tool_devin_/);
|
||||
|
||||
const continuationReply = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
max_tokens: 256,
|
||||
tools: [
|
||||
{
|
||||
name: "Read",
|
||||
description: "Read a file",
|
||||
input_schema: { type: "object", properties: {}, additionalProperties: true },
|
||||
},
|
||||
],
|
||||
messages: [
|
||||
{ role: "user", content: "CONTRACT_TOOL" },
|
||||
{ role: "assistant", content: [toolUse] },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: "CONTRACT_AFTER_TOOL",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
assert.equal(continuationReply.status, 200);
|
||||
const continuationBody = await continuationReply.json();
|
||||
assert.equal(continuationBody.stop_reason, "end_turn");
|
||||
assert.deepEqual(continuationBody.content, [{ type: "text", text: "contract continued" }]);
|
||||
|
||||
for (const marker of ["CONTRACT_ERROR", "CONTRACT_EXIT"]) {
|
||||
const failedReply = await request(marker);
|
||||
assert.equal(failedReply.status, 502);
|
||||
const failedBody = await failedReply.json();
|
||||
assert.equal(failedBody.error?.type, "server_error");
|
||||
assert.doesNotMatch(JSON.stringify(failedBody), /stack|anthropic|openai/i);
|
||||
}
|
||||
|
||||
console.log("PASS: Anthropic Messages wire contracts and fail-closed errors passed");
|
||||
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
|
||||
35
scripts/devin-bridge/common
Executable file
35
scripts/devin-bridge/common
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/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_prepare_sandbox() {
|
||||
mkdir -p "$BRIDGE_SANDBOX/home" "$BRIDGE_SANDBOX/test-data" \
|
||||
"$BRIDGE_SANDBOX/e2e-workspace" "$BRIDGE_SANDBOX/live-workspace" \
|
||||
"$BRIDGE_SANDBOX/evidence"
|
||||
chmod 0777 "$BRIDGE_SANDBOX/e2e-workspace" "$BRIDGE_SANDBOX/live-workspace" \
|
||||
"$BRIDGE_SANDBOX/evidence"
|
||||
}
|
||||
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_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_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" "$@"
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${ENABLE_LIVE_DEVIN_TESTS:-}" != "1" && "${DEVIN_BRIDGE_OFFLINE:-}" != "1" ]]; then
|
||||
printf 'Set DEVIN_BRIDGE_OFFLINE=1 for mock/offline mode or ENABLE_LIVE_DEVIN_TESTS=1 for live Devin.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
"$SCRIPT_DIR/verify-anthropic-isolation"
|
||||
|
||||
printf 'Bridge launch preflight passed. Start Claude Code only inside the isolated container/profile.\n'
|
||||
|
||||
source "$(dirname "$0")/common"
|
||||
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
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps omniroute-live \
|
||||
devin auth status
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps omniroute-live \
|
||||
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
|
||||
exec docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps \
|
||||
claude-live claude
|
||||
|
||||
10
scripts/devin-bridge/login-devin
Executable file
10
scripts/devin-bridge/login-devin
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/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; }
|
||||
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
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps omniroute-live \
|
||||
devin auth login
|
||||
exec env ENABLE_LIVE_DEVIN_TESTS=1 "$(dirname "$0")/test-live-devin"
|
||||
53
scripts/devin-bridge/select-live-model.mjs
Normal file
53
scripts/devin-bridge/select-live-model.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import { DEVIN_MODEL_CATALOG } from "../../open-sse/config/providers/registry/devin/catalog.ts";
|
||||
|
||||
const document = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const candidates = [];
|
||||
|
||||
function collect(value) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(collect);
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
for (const [key, nested] of Object.entries(value)) {
|
||||
if (
|
||||
typeof nested === "string" &&
|
||||
["id", "model", "model_id", "slug"].includes(key.toLowerCase()) &&
|
||||
/^[a-z0-9][a-z0-9._/-]*$/i.test(nested)
|
||||
) {
|
||||
candidates.push(nested);
|
||||
}
|
||||
collect(nested);
|
||||
}
|
||||
}
|
||||
|
||||
collect(document);
|
||||
const unique = [...new Set(candidates)];
|
||||
const catalogIds = new Set(DEVIN_MODEL_CATALOG.map((entry) => entry.id));
|
||||
const available = unique.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`);
|
||||
}
|
||||
}
|
||||
|
||||
const selected =
|
||||
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");
|
||||
}
|
||||
process.stdout.write(selected);
|
||||
17
scripts/devin-bridge/test-contract
Executable file
17
scripts/devin-bridge/test-contract
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/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);
|
||||
if (rows.length !== 5 || rows.some((row) => row.provider !== "devin-cli-agentic")) {
|
||||
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'
|
||||
11
scripts/devin-bridge/test-e2e-mock
Executable file
11
scripts/devin-bridge/test-e2e-mock
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/common"
|
||||
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"
|
||||
printf 'PASS: real Claude Code completed the offline agentic fixture\n'
|
||||
32
scripts/devin-bridge/test-live-devin
Executable file
32
scripts/devin-bridge/test-live-devin
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/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; }
|
||||
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 run --rm --no-deps omniroute-live \
|
||||
devin auth status
|
||||
docker compose -f "$BRIDGE_COMPOSE" --profile live-devin run --rm --no-deps omniroute-live \
|
||||
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_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
|
||||
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);
|
||||
if (entry.decision === "allow" && !/(^|\.)(devin\.ai|cognition\.ai)$/.test(entry.hostname)) {
|
||||
throw new Error(`unexpected allowed egress: ${entry.hostname}`);
|
||||
}
|
||||
}
|
||||
' "$BRIDGE_SANDBOX/evidence/egress.jsonl"
|
||||
printf 'PASS: live model %s was discovered and validated by three scenarios\n' "$devin_model"
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
node --import tsx/esm --test \
|
||||
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
|
||||
|
||||
|
||||
@@ -1,43 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${CLAUDE_CONFIG_DIR:-}" in
|
||||
/home/bridge/.claude-devin-isolated|*/.sandbox/*|*/claude-devin-isolated*)
|
||||
;;
|
||||
"")
|
||||
fail "CLAUDE_CONFIG_DIR is not set"
|
||||
;;
|
||||
*)
|
||||
fail "CLAUDE_CONFIG_DIR must point at an isolated bridge directory"
|
||||
;;
|
||||
esac
|
||||
|
||||
for key in \
|
||||
ANTHROPIC_API_KEY \
|
||||
CLAUDE_CODE_OAUTH_TOKEN \
|
||||
ANTHROPIC_BEDROCK_BASE_URL \
|
||||
ANTHROPIC_VERTEX_BASE_URL \
|
||||
CLAUDE_CODE_USE_BEDROCK \
|
||||
CLAUDE_CODE_USE_VERTEX \
|
||||
CLAUDE_CODE_USE_FOUNDRY
|
||||
do
|
||||
if [[ -n "${!key:-}" ]]; then
|
||||
fail "$key must be unset"
|
||||
fi
|
||||
source "$(dirname "$0")/common"
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
bridge_prepare_sandbox
|
||||
config="$(docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin config)"
|
||||
config_json="$(docker compose -f "$BRIDGE_COMPOSE" --profile offline --profile live-devin 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
|
||||
|
||||
if [[ "${ANTHROPIC_BASE_URL:-}" != "http://omniroute:20128" && "${ANTHROPIC_BASE_URL:-}" != http://127.0.0.1:* && "${ANTHROPIC_BASE_URL:-}" != http://localhost:* ]]; then
|
||||
fail "ANTHROPIC_BASE_URL must target local OmniRoute"
|
||||
fi
|
||||
|
||||
if [[ "${ANTHROPIC_AUTH_TOKEN:-}" != sk-local-* ]]; then
|
||||
fail "ANTHROPIC_AUTH_TOKEN must be a local bridge token"
|
||||
fi
|
||||
|
||||
printf 'PASS: Anthropic isolation environment looks local and isolated\n'
|
||||
|
||||
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"])) {
|
||||
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(["bridge-internal", "guard-egress"])) {
|
||||
throw new Error(`network guard topology mismatch: ${guardNetworks.join(",")}`);
|
||||
}
|
||||
if (config.services["network-guard"].environment.GUARD_ALLOW_SUFFIXES !== ".devin.ai,.cognition.ai") {
|
||||
throw new Error("network guard allowlist mismatch");
|
||||
}
|
||||
for (const serviceName of ["omniroute", "claude", "contract", "omniroute-live", "claude-live"]) {
|
||||
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`);
|
||||
}
|
||||
const configMount = (service.volumes || []).find(
|
||||
(mount) => mount.target === "/home/bridge/.claude-devin-isolated",
|
||||
);
|
||||
if (!configMount || configMount.type !== "volume") {
|
||||
throw new Error(`${serviceName} does not use the isolated named Claude volume`);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
' <<<"$config_json" || fail "structured compose isolation checks failed"
|
||||
if [[ "${1:-}" == --static ]]; then printf 'PASS: static bridge isolation checks passed\n'; exit 0; fi
|
||||
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
|
||||
'
|
||||
printf 'PASS: runtime bridge isolation checks passed\n'
|
||||
|
||||
9
tests/fixtures/devin-bridge/e2e-workspace/.claude/commands/bridge-check.md
vendored
Normal file
9
tests/fixtures/devin-bridge/e2e-workspace/.claude/commands/bridge-check.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: Exercise the isolated Claude-to-Devin agentic bridge
|
||||
allowed-tools: Skill, Read, Edit, Bash
|
||||
---
|
||||
|
||||
`COMMAND_BRIDGE_ACTIVE`
|
||||
|
||||
Use the bridge-proof skill. Locate and read the implementation, correct it, run its test, diagnose
|
||||
the intentionally failed first correction, fix it, rerun the test, and report completion.
|
||||
6
tests/fixtures/devin-bridge/e2e-workspace/.claude/hooks/log-tool.mjs
vendored
Normal file
6
tests/fixtures/devin-bridge/e2e-workspace/.claude/hooks/log-tool.mjs
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
const event = JSON.parse(input || "{}");
|
||||
fs.appendFileSync("/workspace/.e2e-hook.log", `${String(event.tool_name || "unknown")}\n`);
|
||||
16
tests/fixtures/devin-bridge/e2e-workspace/.claude/settings.json
vendored
Normal file
16
tests/fixtures/devin-bridge/e2e-workspace/.claude/settings.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Skill|Read|Edit|Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/hooks/log-tool.mjs",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
9
tests/fixtures/devin-bridge/e2e-workspace/.claude/skills/bridge-proof/SKILL.md
vendored
Normal file
9
tests/fixtures/devin-bridge/e2e-workspace/.claude/skills/bridge-proof/SKILL.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: bridge-proof
|
||||
description: This skill should be used when the user invokes the bridge-check command or asks to verify the isolated Devin bridge.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
`SKILL_BRIDGE_ACTIVE`
|
||||
|
||||
Use project-local tools to inspect, edit, and test the fixture. Do not merely narrate actions.
|
||||
7
tests/fixtures/devin-bridge/e2e-workspace/CLAUDE.md
vendored
Normal file
7
tests/fixtures/devin-bridge/e2e-workspace/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Offline bridge fixture
|
||||
|
||||
`CLAUDE_MD_BRIDGE_ACTIVE`
|
||||
|
||||
Use the `bridge-proof` project skill for the `/bridge-check` task. Inspect the project, repair
|
||||
`math.js`, and run the tests. A failed first attempt must be diagnosed and corrected. Finish only
|
||||
after the tests pass.
|
||||
3
tests/fixtures/devin-bridge/e2e-workspace/math.js
vendored
Normal file
3
tests/fixtures/devin-bridge/e2e-workspace/math.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export function add(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
8
tests/fixtures/devin-bridge/e2e-workspace/math.test.js
vendored
Normal file
8
tests/fixtures/devin-bridge/e2e-workspace/math.test.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { add } from "./math.js";
|
||||
|
||||
test("add returns the sum", () => {
|
||||
assert.equal(add(2, 3), 5);
|
||||
});
|
||||
8
tests/fixtures/devin-bridge/e2e-workspace/package.json
vendored
Normal file
8
tests/fixtures/devin-bridge/e2e-workspace/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "devin-bridge-e2e-fixture",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test math.test.js"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user