Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@ runs:
- name: Install OpenSSH client
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y openssh-client
for attempt in 1 2 3; do
sudo rm -rf /var/lib/apt/lists/*
if sudo apt-get -o Acquire::Retries=3 -o Acquire::By-Hash=force update; then
break
fi
if [[ "$attempt" == "3" ]]; then
echo "apt-get update failed after retries" >&2
exit 1
fi
echo "apt-get update attempt ${attempt} failed; retrying..." >&2
sleep $((attempt * 2))
done
sudo apt-get -o Acquire::Retries=3 install -y openssh-client
- name: Install node-gyp
shell: bash
run: npm install -g node-gyp
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
e2e-opencode:
name: E2E (OpenCode)
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
- name: Install dependencies
Expand All @@ -113,7 +113,7 @@ jobs:
e2e-clone-cache:
name: E2E (Clone cache)
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
- name: Install dependencies
Expand All @@ -126,7 +126,7 @@ jobs:
e2e-login-context:
name: E2E (Login context)
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
- name: Install dependencies
Expand All @@ -139,7 +139,7 @@ jobs:
e2e-runtime-volumes-ssh:
name: E2E (Runtime volumes + SSH)
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
- name: Install dependencies
Expand All @@ -152,7 +152,7 @@ jobs:
e2e-clone-auto-open-ssh:
name: E2E (Clone auto-open SSH)
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
- name: Install dependencies
Expand Down
19 changes: 18 additions & 1 deletion packages/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,24 @@ ENV BUN_INSTALL=/opt/bun
ENV PATH=/opt/bun/bin:$PATH
WORKDIR /workspace

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

Expand Down
10 changes: 9 additions & 1 deletion packages/api/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ import { attachProjectBrowserWebSocketServer } from "./services/project-browser.
import { attachProjectDatabaseWebSocketServer } from "./services/project-databases.js"
import { attachTerminalWebSocketServer } from "./services/terminal-sessions.js"

type ApiHttpServer = ReturnType<typeof createServer>

const resolvePort = (env: Record<string, string | undefined>): number => {
const raw = env["DOCKER_GIT_API_PORT"] ?? env["PORT"]
const parsed = raw === undefined ? Number.NaN : Number(raw)
return Number.isFinite(parsed) && parsed > 0 ? parsed : 3334
}

export const configureLongRunningRequestTimeouts = (server: ApiHttpServer): ApiHttpServer => {
server.requestTimeout = 0
server.setTimeout(0)
return server
}

const requestLogger = HttpMiddleware.make((httpApp) =>
Effect.gen(function*(_) {
const request = yield* _(HttpServerRequest.HttpServerRequest)
Expand Down Expand Up @@ -45,7 +53,7 @@ export const program = (() => {
const port = resolvePort(process.env)
const router = makeRouter()
const app = router.pipe(HttpServer.serve(requestLogger), HttpServer.withLogAddress)
const server = createServer()
const server = configureLongRunningRequestTimeouts(createServer())
attachAuthTerminalWebSocketServer(server)
attachTerminalWebSocketServer(server)
attachProjectBrowserWebSocketServer(server)
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/services/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ const sourceLabel = (request: CreateAgentRequest): string =>

const pickDefaultCommand = (provider: CreateAgentRequest["provider"]): string => {
if (provider === "codex") {
return "codex"
return "MCP_PLAYWRIGHT_ISOLATED=1 codex"
}
if (provider === "opencode") {
return "opencode"
}
if (provider === "claude") {
return "claude"
return "MCP_PLAYWRIGHT_ISOLATED=1 claude"
}
return ""
}

const buildCommand = (request: CreateAgentRequest): string => {
export const buildCommand = (request: CreateAgentRequest): string => {
const direct = request.command?.trim() ?? ""
if (direct.length > 0) {
return direct
Expand Down
23 changes: 23 additions & 0 deletions packages/api/tests/agents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest"

import { buildCommand } from "../src/services/agents.js"

describe("agent service", () => {
it("starts default Codex agents with isolated Playwright MCP", () => {
expect(buildCommand({ provider: "codex" })).toBe("MCP_PLAYWRIGHT_ISOLATED=1 codex")
expect(buildCommand({ provider: "codex", args: ["exec", "hello world"] })).toBe(
"MCP_PLAYWRIGHT_ISOLATED=1 codex 'exec' 'hello world'"
)
})

it("starts default Claude agents with isolated Playwright MCP", () => {
expect(buildCommand({ provider: "claude" })).toBe("MCP_PLAYWRIGHT_ISOLATED=1 claude")
expect(buildCommand({ provider: "claude", args: ["-p", "hello world"] })).toBe(
"MCP_PLAYWRIGHT_ISOLATED=1 claude '-p' 'hello world'"
)
})

it("does not rewrite custom agent commands", () => {
expect(buildCommand({ provider: "codex", command: "codex --help" })).toBe("codex --help")
})
})
14 changes: 14 additions & 0 deletions packages/api/tests/program.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createServer } from "node:http"

import { describe, expect, it } from "vitest"

import { configureLongRunningRequestTimeouts } from "../src/program.js"

describe("api program", () => {
it("does not abort long-running project creation requests", () => {
const server = configureLongRunningRequestTimeouts(createServer())

expect(server.requestTimeout).toBe(0)
expect(server.timeout).toBe(0)
})
})
22 changes: 13 additions & 9 deletions packages/app/src/docker-git/api-http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { HttpClientResponse } from "@effect/platform"
import { FetchHttpClient, HttpBody, HttpClient } from "@effect/platform"
import { HttpBody, HttpClient } from "@effect/platform"
import type * as HttpClientError from "@effect/platform/HttpClientError"
import { NodeHttpClient } from "@effect/platform-node"
import { Effect } from "effect"

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

return execute().pipe(
Effect.matchEffect({
onFailure: (error) =>
ensureControllerReady().pipe(
Effect.matchEffect({
onFailure: () => Effect.fail(error),
onSuccess: () => execute()
})
),
!shouldRetry
? Effect.fail(error)
: ensureControllerReady().pipe(
Effect.matchEffect({
onFailure: () => Effect.fail(error),
onSuccess: () => execute()
})
),
onSuccess: (value) => Effect.succeed(value)
})
)
Expand Down Expand Up @@ -198,7 +202,7 @@ export const request = (
}

return parsed
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))

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

return yield* _(readHttpResponseTextStream(response, onChunk))
}).pipe(Effect.provide(FetchHttpClient.layer), mapTransportError(method, path))
}).pipe(Effect.provide(NodeHttpClient.layer), mapTransportError(method, path))
2 changes: 1 addition & 1 deletion packages/app/src/docker-git/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Container runtime env (set via .orch/env/project.env):
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts (recommended for many Codex; default: 1)
MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts; default 0 shares the VNC session
MCP_PLAYWRIGHT_CDP_GUARD=1|0 Guard CDP so MCP cannot close/crash shared Chromium (default: 1)
MCP_PLAYWRIGHT_BLOCK_BROWSER_CLOSE=1|0 Block destructive Browser.close/crash CDP methods (default: 1)
MCP_PLAYWRIGHT_CDP_ENDPOINT=http://... Override CDP endpoint (default: http://dg-<repo>-browser:9223)
Expand Down
9 changes: 7 additions & 2 deletions packages/app/src/lib/core/templates-entrypoint/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@ fi`

const renderAgentPromptCommand = (mode: AgentMode): string =>
Match.value(mode).pipe(
Match.when("claude", () => String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
Match.when("codex", () => String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
Match.when(
"claude",
() =>
String
.raw`MCP_PLAYWRIGHT_ISOLATED=1 claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
),
Match.when("codex", () => String.raw`MCP_PLAYWRIGHT_ISOLATED=1 codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
Match.when("gemini", () => String.raw`gemini --approval-mode=yolo \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
Match.exhaustive
)
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/lib/core/templates-entrypoint/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ AGENT_MODE="\${AGENT_MODE:-}"
AGENT_AUTO="\${AGENT_AUTO:-}"
MCP_PLAYWRIGHT_ENABLE="\${MCP_PLAYWRIGHT_ENABLE:-${config.enableMcpPlaywright ? "1" : "0"}}"
MCP_PLAYWRIGHT_CDP_ENDPOINT="\${MCP_PLAYWRIGHT_CDP_ENDPOINT:-}"
MCP_PLAYWRIGHT_ISOLATED="\${MCP_PLAYWRIGHT_ISOLATED:-1}"
MCP_PLAYWRIGHT_ISOLATED="\${MCP_PLAYWRIGHT_ISOLATED:-0}"
MCP_PLAYWRIGHT_CDP_GUARD="\${MCP_PLAYWRIGHT_CDP_GUARD:-1}"
MCP_PLAYWRIGHT_BLOCK_BROWSER_CLOSE="\${MCP_PLAYWRIGHT_BLOCK_BROWSER_CLOSE:-1}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ CODEX_AUTO_UPDATE=1
DOCKER_GIT_ZSH_AUTOSUGGEST=1
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion
MCP_PLAYWRIGHT_ISOLATED=1
MCP_PLAYWRIGHT_ISOLATED=0
EOF
fi

Expand Down
21 changes: 19 additions & 2 deletions packages/app/src/lib/core/templates/dockerfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@ const renderDockerfilePrelude = (): string =>
ENV DEBIAN_FRONTEND=noninteractive
ENV NVM_DIR=/usr/local/nvm

RUN apt-get update && apt-get install -y --no-install-recommends \
RUN set -eu; \
sed -i \
-e 's|http://archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' \
-e 's|http://security.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' \
/etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true; \
for attempt in 1 2 3 4 5; do \
rm -rf /var/lib/apt/lists/*; \
if apt-get -o Acquire::Retries=3 -o Acquire::By-Hash=force update; then \
break; \
fi; \
if [ "$attempt" = "5" ]; then \
echo "apt-get update failed after retries" >&2; \
exit 1; \
fi; \
echo "apt-get update attempt \${attempt} failed; retrying..." >&2; \
sleep $((attempt * 2)); \
done; \
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
openssh-server git gh ca-certificates curl unzip bsdutils sudo \
make docker.io docker-compose-v2 bash-completion zsh zsh-autosuggestions xauth \
ncurses-term \
Expand Down Expand Up @@ -172,7 +189,7 @@ if [[ -z "$JSON" ]]; then
fi

EXTRA_ARGS=()
if [[ "\${MCP_PLAYWRIGHT_ISOLATED:-1}" == "1" ]]; then
if [[ "\${MCP_PLAYWRIGHT_ISOLATED:-0}" == "1" ]]; then
EXTRA_ARGS+=(--isolated)
fi

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/lib/usecases/actions/prepare-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ const defaultProjectEnvContents = [
"DOCKER_GIT_ZSH_AUTOSUGGEST=1",
"DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic",
"DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion",
"MCP_PLAYWRIGHT_ISOLATED=1",
"MCP_PLAYWRIGHT_ISOLATED=0",
"MCP_PLAYWRIGHT_CDP_GUARD=1",
"MCP_PLAYWRIGHT_BLOCK_BROWSER_CLOSE=1",
""
Expand Down
Loading