Skip to content

Commit d407f6f

Browse files
authored
Merge pull request #225 from ProverCoderAI/codex/shared-browser-agent-isolation
[codex] fix browser session isolation defaults
2 parents 93d945c + cd883bb commit d407f6f

24 files changed

Lines changed: 198 additions & 82 deletions

File tree

.github/actions/setup/action.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@ runs:
2424
- name: Install OpenSSH client
2525
shell: bash
2626
run: |
27-
sudo apt-get update
28-
sudo apt-get install -y openssh-client
27+
for attempt in 1 2 3; do
28+
sudo rm -rf /var/lib/apt/lists/*
29+
if sudo apt-get -o Acquire::Retries=3 -o Acquire::By-Hash=force update; then
30+
break
31+
fi
32+
if [[ "$attempt" == "3" ]]; then
33+
echo "apt-get update failed after retries" >&2
34+
exit 1
35+
fi
36+
echo "apt-get update attempt ${attempt} failed; retrying..." >&2
37+
sleep $((attempt * 2))
38+
done
39+
sudo apt-get -o Acquire::Retries=3 install -y openssh-client
2940
- name: Install node-gyp
3041
shell: bash
3142
run: npm install -g node-gyp

.github/workflows/check.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
e2e-opencode:
101101
name: E2E (OpenCode)
102102
runs-on: ubuntu-latest
103-
timeout-minutes: 25
103+
timeout-minutes: 40
104104
steps:
105105
- uses: actions/checkout@v6
106106
- name: Install dependencies
@@ -113,7 +113,7 @@ jobs:
113113
e2e-clone-cache:
114114
name: E2E (Clone cache)
115115
runs-on: ubuntu-latest
116-
timeout-minutes: 25
116+
timeout-minutes: 40
117117
steps:
118118
- uses: actions/checkout@v6
119119
- name: Install dependencies
@@ -126,7 +126,7 @@ jobs:
126126
e2e-login-context:
127127
name: E2E (Login context)
128128
runs-on: ubuntu-latest
129-
timeout-minutes: 20
129+
timeout-minutes: 40
130130
steps:
131131
- uses: actions/checkout@v6
132132
- name: Install dependencies
@@ -139,7 +139,7 @@ jobs:
139139
e2e-runtime-volumes-ssh:
140140
name: E2E (Runtime volumes + SSH)
141141
runs-on: ubuntu-latest
142-
timeout-minutes: 25
142+
timeout-minutes: 40
143143
steps:
144144
- uses: actions/checkout@v6
145145
- name: Install dependencies
@@ -152,7 +152,7 @@ jobs:
152152
e2e-clone-auto-open-ssh:
153153
name: E2E (Clone auto-open SSH)
154154
runs-on: ubuntu-latest
155-
timeout-minutes: 25
155+
timeout-minutes: 40
156156
steps:
157157
- uses: actions/checkout@v6
158158
- name: Install dependencies

packages/api/Dockerfile

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,24 @@ ENV BUN_INSTALL=/opt/bun
99
ENV PATH=/opt/bun/bin:$PATH
1010
WORKDIR /workspace
1111

12-
RUN apt-get update && apt-get install -y --no-install-recommends \
12+
RUN set -eu; \
13+
sed -i \
14+
-e 's|http://archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' \
15+
-e 's|http://security.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' \
16+
/etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true; \
17+
for attempt in 1 2 3; do \
18+
rm -rf /var/lib/apt/lists/*; \
19+
if apt-get -o Acquire::Retries=3 -o Acquire::By-Hash=force update; then \
20+
break; \
21+
fi; \
22+
if [ "$attempt" = "3" ]; then \
23+
echo "apt-get update failed after retries" >&2; \
24+
exit 1; \
25+
fi; \
26+
echo "apt-get update attempt ${attempt} failed; retrying..." >&2; \
27+
sleep $((attempt * 2)); \
28+
done; \
29+
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
1330
ca-certificates curl git docker.io docker-compose-v2 openssh-client sshpass python3 make g++ unzip \
1431
&& rm -rf /var/lib/apt/lists/*
1532

packages/api/src/program.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@ import { attachProjectBrowserWebSocketServer } from "./services/project-browser.
1111
import { attachProjectDatabaseWebSocketServer } from "./services/project-databases.js"
1212
import { attachTerminalWebSocketServer } from "./services/terminal-sessions.js"
1313

14+
type ApiHttpServer = ReturnType<typeof createServer>
15+
1416
const resolvePort = (env: Record<string, string | undefined>): number => {
1517
const raw = env["DOCKER_GIT_API_PORT"] ?? env["PORT"]
1618
const parsed = raw === undefined ? Number.NaN : Number(raw)
1719
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3334
1820
}
1921

22+
export const configureLongRunningRequestTimeouts = (server: ApiHttpServer): ApiHttpServer => {
23+
server.requestTimeout = 0
24+
server.setTimeout(0)
25+
return server
26+
}
27+
2028
const requestLogger = HttpMiddleware.make((httpApp) =>
2129
Effect.gen(function*(_) {
2230
const request = yield* _(HttpServerRequest.HttpServerRequest)
@@ -45,7 +53,7 @@ export const program = (() => {
4553
const port = resolvePort(process.env)
4654
const router = makeRouter()
4755
const app = router.pipe(HttpServer.serve(requestLogger), HttpServer.withLogAddress)
48-
const server = createServer()
56+
const server = configureLongRunningRequestTimeouts(createServer())
4957
attachAuthTerminalWebSocketServer(server)
5058
attachTerminalWebSocketServer(server)
5159
attachProjectBrowserWebSocketServer(server)

packages/api/src/services/agents.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ const sourceLabel = (request: CreateAgentRequest): string =>
5555

5656
const pickDefaultCommand = (provider: CreateAgentRequest["provider"]): string => {
5757
if (provider === "codex") {
58-
return "codex"
58+
return "MCP_PLAYWRIGHT_ISOLATED=1 codex"
5959
}
6060
if (provider === "opencode") {
6161
return "opencode"
6262
}
6363
if (provider === "claude") {
64-
return "claude"
64+
return "MCP_PLAYWRIGHT_ISOLATED=1 claude"
6565
}
6666
return ""
6767
}
6868

69-
const buildCommand = (request: CreateAgentRequest): string => {
69+
export const buildCommand = (request: CreateAgentRequest): string => {
7070
const direct = request.command?.trim() ?? ""
7171
if (direct.length > 0) {
7272
return direct

packages/api/tests/agents.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from "vitest"
2+
3+
import { buildCommand } from "../src/services/agents.js"
4+
5+
describe("agent service", () => {
6+
it("starts default Codex agents with isolated Playwright MCP", () => {
7+
expect(buildCommand({ provider: "codex" })).toBe("MCP_PLAYWRIGHT_ISOLATED=1 codex")
8+
expect(buildCommand({ provider: "codex", args: ["exec", "hello world"] })).toBe(
9+
"MCP_PLAYWRIGHT_ISOLATED=1 codex 'exec' 'hello world'"
10+
)
11+
})
12+
13+
it("starts default Claude agents with isolated Playwright MCP", () => {
14+
expect(buildCommand({ provider: "claude" })).toBe("MCP_PLAYWRIGHT_ISOLATED=1 claude")
15+
expect(buildCommand({ provider: "claude", args: ["-p", "hello world"] })).toBe(
16+
"MCP_PLAYWRIGHT_ISOLATED=1 claude '-p' 'hello world'"
17+
)
18+
})
19+
20+
it("does not rewrite custom agent commands", () => {
21+
expect(buildCommand({ provider: "codex", command: "codex --help" })).toBe("codex --help")
22+
})
23+
})

packages/api/tests/program.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createServer } from "node:http"
2+
3+
import { describe, expect, it } from "vitest"
4+
5+
import { configureLongRunningRequestTimeouts } from "../src/program.js"
6+
7+
describe("api program", () => {
8+
it("does not abort long-running project creation requests", () => {
9+
const server = configureLongRunningRequestTimeouts(createServer())
10+
11+
expect(server.requestTimeout).toBe(0)
12+
expect(server.timeout).toBe(0)
13+
})
14+
})

packages/app/src/docker-git/api-http.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { HttpClientResponse } from "@effect/platform"
2-
import { FetchHttpClient, HttpBody, HttpClient } from "@effect/platform"
2+
import { HttpBody, HttpClient } from "@effect/platform"
33
import type * as HttpClientError from "@effect/platform/HttpClientError"
4+
import { NodeHttpClient } from "@effect/platform-node"
45
import { Effect } from "effect"
56

67
import { readHttpResponseTextStream } from "../shared/http-response-stream.js"
@@ -140,16 +141,19 @@ const executeRequestWithControllerRetry = (
140141
body: JsonRequest | undefined
141142
) => {
142143
const execute = () => executeRequest(client, resolveApiBaseUrl(), method, path, body)
144+
const shouldRetry = method === "GET"
143145

144146
return execute().pipe(
145147
Effect.matchEffect({
146148
onFailure: (error) =>
147-
ensureControllerReady().pipe(
148-
Effect.matchEffect({
149-
onFailure: () => Effect.fail(error),
150-
onSuccess: () => execute()
151-
})
152-
),
149+
!shouldRetry
150+
? Effect.fail(error)
151+
: ensureControllerReady().pipe(
152+
Effect.matchEffect({
153+
onFailure: () => Effect.fail(error),
154+
onSuccess: () => execute()
155+
})
156+
),
153157
onSuccess: (value) => Effect.succeed(value)
154158
})
155159
)
@@ -198,7 +202,7 @@ export const request = (
198202
}
199203

200204
return parsed
201-
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
205+
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))
202206

203207
export const requestVoid = (method: ApiHttpMethod, path: string, body?: JsonRequest) =>
204208
request(method, path, body).pipe(Effect.asVoid)
@@ -219,4 +223,4 @@ export const requestTextStream = (
219223
}
220224

221225
return yield* _(readHttpResponseTextStream(response, onChunk))
222-
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
226+
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))

packages/app/src/docker-git/cli/usage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Container runtime env (set via .orch/env/project.env):
9393
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
9494
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
9595
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
96-
MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts (recommended for many Codex; default: 1)
96+
MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts; default 0 shares the VNC session
9797
MCP_PLAYWRIGHT_CDP_GUARD=1|0 Guard CDP so MCP cannot close/crash shared Chromium (default: 1)
9898
MCP_PLAYWRIGHT_BLOCK_BROWSER_CLOSE=1|0 Block destructive Browser.close/crash CDP methods (default: 1)
9999
MCP_PLAYWRIGHT_CDP_ENDPOINT=http://... Override CDP endpoint (default: http://dg-<repo>-browser:9223)

packages/app/src/lib/core/templates-entrypoint/agent.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ fi`
5252

5353
const renderAgentPromptCommand = (mode: AgentMode): string =>
5454
Match.value(mode).pipe(
55-
Match.when("claude", () => String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
56-
Match.when("codex", () => String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
55+
Match.when(
56+
"claude",
57+
() =>
58+
String
59+
.raw`MCP_PLAYWRIGHT_ISOLATED=1 claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
60+
),
61+
Match.when("codex", () => String.raw`MCP_PLAYWRIGHT_ISOLATED=1 codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
5762
Match.when("gemini", () => String.raw`gemini --approval-mode=yolo \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
5863
Match.exhaustive
5964
)

0 commit comments

Comments
 (0)