feat(cloak): -p cloak stealth-browser profile, auth sessions, non-gating release - #492
Conversation
…pport Second of two commits on this branch, on top of the kimi batch. Adds the CloakBrowser profile (#456) and the image/tag precedence fix it needs. - cloak profile: `-p cloak` runs agents in a CloakBrowser image (Dockerfile.cloak, extends :rust) -- source-patched stealth Chromium, headed on Xvfb :99 + openbox, binary baked at build (auto-update off). Ships the deva-cloak skill, make build-cloak/test-cloak, a paths-filtered cloak-image.yml, and release :cloak / :vX.Y.Z-cloak tags. - auth sessions: a persistent host profile mount (~/.config/deva/ cloak-profile -> userDataDir) so a login survives container removal. Interactive login over VNC is on-demand and direct-reach on OrbStack/Linux -- the agent starts x11vnc and you connect at the container IP; --cloak-vnc is the Docker-Desktop fallback that publishes a loopback port. --cloak-browser is an optional always-on cloak-browserd daemon the agent attaches to over CDP. - fix(precedence): env > config > -p > default, so an explicit -p beats a .deva DEVA_DOCKER_TAG (guarded by scripts/test-image-precedence.sh). Not built/smoke-tested locally (container network blocks apt); release CI builds the image on GitHub runners. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- release job no longer needs build-and-push-cloak; the cloak job is
continue-on-error. A ~200MB Chromium bake per arch (arm64 under QEMU)
must not sit on the critical path of a GitHub Release, and a flake in
an optional layer must not fail a release whose artifact is deva.sh
- cloak holds no agent CLIs and only changes when Dockerfile.cloak,
cloak-entrypoint.sh or CLOAKBROWSER_WRAPPER_VERSION change -- never on
a version bump. Nightly already skipped it for the same reason
- release notes: :cloak is the rolling tag, :vX.Y.Z-cloak may lag
- skill: goto(data:,{domcontentloaded}) not setContent() -- setContent
hangs on a fresh daemon page waiting for a load event that never fires
- dev log records the live proofs that close the 07-21 unverified gap:
RFB 003.008 on :5900, CDP on :9222, connectOverCDP drives the live
browser and the daemon survives client disconnect
Refs #456
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe6782bbda
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| ./scripts/test-image-precedence.sh |
There was a problem hiding this comment.
Invoke the non-executable smoke script through Bash
The new script is committed with mode 100644, so this direct ./scripts/test-image-precedence.sh invocation fails with Permission denied before running any assertions, causing every CI run through this job to fail. Either mark the script executable or invoke it as bash ./scripts/test-image-precedence.sh.
Useful? React with 👍 / 👎.
| DOCKER_ARGS+=("-e" "DEVA_CLOAK_BROWSER=1") | ||
| DOCKER_ARGS+=("-e" "DEVA_CLOAK_CDP_PORT=${cdp_port}") | ||
| DOCKER_ARGS+=("-e" "CLOAK_CDP_ENDPOINT=http://127.0.0.1:${cdp_port}") |
There was a problem hiding this comment.
Recreate warm containers when enabling the browser daemon
When a persistent cloak container was originally created without --cloak-browser, rerunning with this flag resolves to the same container because the option is not included in its shape. These -e entries only affect docker run; the existing-container path executes the entrypoint with only _trace_env, so DEVA_CLOAK_BROWSER and CLOAK_CDP_ENDPOINT remain absent and no daemon starts. Treat this option as a distinct container shape or explicitly recreate the container when it is first enabled.
Useful? React with 👍 / 👎.
| # dead URL. | ||
| announce_cloak_vnc() { | ||
| local phase="$1" | ||
| if [ "$phase" = "existing" ] && [ "${DEVA_CLOAK_VNC:-}" = "1" ] && [ -z "$DEVA_CLOAK_VNC_URL" ]; then |
There was a problem hiding this comment.
Do not advertise VNC for a warm container without its port
When --cloak-vnc is added after a persistent cloak container already exists, setup_cloak_vnc always assigns DEVA_CLOAK_VNC_URL from a newly probed port, so this -z check is false and the script prints that URL and a newly generated password. The corresponding -p and VNC environment arguments cannot be applied to the existing container, so the advertised endpoint is dead. Check the container's actual published ports or use a separate container shape for VNC-enabled runs.
Useful? React with 👍 / 👎.
| setsid nohup gosu "$DEVA_USER" env \ | ||
| "HOME=$DEVA_HOME" "PATH=$PATH" "DISPLAY=${DISPLAY:-:99}" \ | ||
| "CLOAK_APP_DIR=${CLOAK_APP_DIR:-/opt/cloak}" \ | ||
| "DEVA_CLOAK_PROFILE_DIR=$profile" "DEVA_CLOAK_CDP_PORT=$port" \ | ||
| node "$daemon" >"$log" 2>&1 & |
There was a problem hiding this comment.
Wait for the CDP endpoint before launching the agent
On a fresh --cloak-browser launch, this backgrounds Node and immediately returns without waiting for Chromium's CDP port to listen. The documented client calls connectOverCDP directly, so normal browser startup latency can make the first agent connection fail even though the daemon becomes ready moments later. Poll the CDP endpoint with a bounded timeout before returning from maybe_start_cloak_browser.
Useful? React with 👍 / 👎.
| paths: | ||
| - "Dockerfile.cloak" | ||
| - "scripts/cloak-entrypoint.sh" | ||
| - "versions.env" | ||
| - ".github/workflows/cloak-image.yml" |
There was a problem hiding this comment.
Trigger the cloak workflow for daemon changes
A PR or push that changes only scripts/cloak-browserd.mjs skips this workflow even though Dockerfile.cloak copies that file into the image and test-cloak is intended to validate it. This allows a broken daemon change to merge without building or testing the cloak image; add the daemon path to both path-filter lists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds a new cloak image profile to deva, providing a headed “stealth Chromium” environment (CloakBrowser) plus composable authenticated-session workflows (persistent profile mount, optional VNC, optional always-on browser daemon). Also hardens image/tag resolution so config files can’t silently override environment intent or make -p a no-op, and updates CI/release policy so cloak publishing is non-gating.
Changes:
- Introduces the
cloakimage (Dockerfile + entrypoint + daemon) anddeva.sh -p cloakruntime flags (--cloak-vnc,--cloak-browser) with persistent profile support. - Adds version pinning for
CLOAKBROWSER_WRAPPER_VERSIONand wires it through update/refresh tooling and build workflows. - Adjusts release/CI workflows: cloak build is paths-filtered in CI and non-gating in release; adds a smoke test for image/tag precedence.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
versions.env |
Adds CLOAKBROWSER_WRAPPER_VERSION pin with explanatory comments. |
skills/deva-cloak/SKILL.md |
New skill doc for operating CloakBrowser, VNC login, and CDP daemon usage. |
scripts/version-pins.sh |
Includes CLOAKBROWSER_WRAPPER_VERSION in pin registry. |
scripts/update-version-pins.sh |
Emits/refreshes CloakBrowser wrapper version from npm. |
scripts/test-image-precedence.sh |
Adds regression test for image/tag precedence and -p behavior. |
scripts/cloak-entrypoint.sh |
New cloak entrypoint starting Xvfb/openbox and optional x11vnc. |
scripts/cloak-browserd.mjs |
New always-on headed browser daemon exposing loopback CDP. |
README.md |
Documents the new stable :cloak tag. |
Makefile |
Adds build/test targets and args for cloak image. |
llms.txt |
Updates image/tag overview and adds -p cloak usage example. |
docs/custom-images.md |
Documents local Dockerfile.cloak build path and args. |
docs/advanced-usage.md |
Documents cloak profile purpose and how to build/use it. |
Dockerfile.cloak |
Defines the cloak image layer (X stack + wrapper install + baked Chromium). |
docker-entrypoint.sh |
Adds optional startup of the cloak browser daemon post UID/GID remap. |
deva.sh |
Adds cloak profile support, new runtime flags, and fixes image/tag precedence. |
DEV-LOGS.md |
Adds dev-log entries describing cloak policy and verification notes. |
CHANGELOG.md |
Changelog entries for cloak support and image-precedence fix. |
AGENTS.md |
Updates container environment notes (ephemeral behavior). |
.github/workflows/release.yml |
Adds non-gating cloak publish job and release-note section for cloak tags. |
.github/workflows/cloak-image.yml |
New paths-filtered workflow to build/test cloak image. |
.github/workflows/ci.yml |
Adds smoke test execution for image/tag precedence. |
.dockerignore |
Ignores reference/ directory in addition to references/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for _ in $(seq 1 50); do | ||
| DISPLAY=:99 xdotool getdisplaygeometry >/dev/null 2>&1 && break | ||
| sleep 0.2 | ||
| done | ||
|
|
| if [ -z "$DEVA_CLOAK_VNC_PASSWORD_EFFECTIVE" ]; then | ||
| # Read a FINITE chunk of urandom, then filter+cut. Piping an unbounded | ||
| # `</dev/urandom` into `head -c 8` makes head close the pipe early, tr | ||
| # takes SIGPIPE, and pipefail+set -e would kill the whole launch. | ||
| DEVA_CLOAK_VNC_PASSWORD_EFFECTIVE="$(head -c 128 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | cut -c1-8)" | ||
| DEVA_CLOAK_VNC_PASSWORD_GENERATED=true |
| # 0 if this run targets the cloak image (via -p cloak or a cloak tag). | ||
| is_cloak_image() { | ||
| [ "${PROFILE:-}" = "cloak" ] || [[ "${DEVA_DOCKER_TAG:-}" == *cloak* ]] | ||
| } |
| - What: | ||
| - The #414 flock rework ended the CA-install subshell on `[ "$VERBOSE" = "true" ] && echo ...`; with VERBOSE unset the failed test became the subshell's exit status, and under `set -e` the entrypoint died right after the agent banner — silently, only in freshly built images (the entrypoint is baked). Fix: explicit `exit 0` before the subshell closes. Verified A/B: baked image dies, overlay-fixed image runs `deva.sh claude --trace --rm -Q -- --version` end-to-end (cctrace MITM up, trace file written). Rebuild images to pick this up. | ||
| - Result: traced launch verified live against the host daemon. | ||
| - Live verification against the built `:cloak` image (wrapper 0.5.2, Chrome 146.0.7680.177): x11vnc answers the RFB handshake on :5900 with `RFB 003.008`; cloak-browserd serves CDP on 127.0.0.1:9222; a separate playwright-core client `connectOverCDP`s, drives a page, and the daemon survives that client's `close()`. Stealth confirmed live: UA spoofs `Windows NT 10.0`, `navigator.webdriver === false`. First probe raced the daemons — both are up ~4s after boot, so poll the ports, don't poll only the display. |
CI invokes it as ./scripts/test-image-precedence.sh and got exit 126, Permission denied. Local runs used `bash <script>`, which ignores the exec bit -- so the mode never showed up until CI ran it directly. Refs #456
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
| if [ "$DEVA_DOCKER_TAG_SOURCE" != "env" ]; then | ||
| DEVA_DOCKER_TAG="$value" | ||
| DEVA_DOCKER_TAG_ENV_SET=true | ||
| DEVA_DOCKER_TAG_SOURCE="config" | ||
| normalize_docker_image_parts | ||
| export DEVA_DOCKER_TAG | ||
| USER_ENVS+=("$name=$value") | ||
| fi |
| announce_cloak_vnc() { | ||
| local phase="$1" | ||
| if [ "$phase" = "existing" ] && [ "${DEVA_CLOAK_VNC:-}" = "1" ] && [ -z "$DEVA_CLOAK_VNC_URL" ]; then | ||
| echo "note: DEVA_CLOAK_VNC=1 but this container was created without a VNC port; recreate it (--rm, or stop/remove) to enable VNC" >&2 | ||
| return 0 | ||
| fi | ||
| [ -n "$DEVA_CLOAK_VNC_URL" ] || return 0 | ||
| echo "Cloak VNC: $DEVA_CLOAK_VNC_URL (open in a VNC client to watch/drive the browser the agent opens)" >&2 |
| # CloakBrowser npm wrapper version. This also pins the Chromium binary: | ||
| # the wrapper hardcodes per-arch free-binary versions (linux-x64 146.x.x.5, | ||
| # linux-arm64 146.x.x.3), so bumping the wrapper is what moves Chromium. | ||
| CLOAKBROWSER_WRAPPER_VERSION=0.4.12 |
check_image ran unconditionally, so a dry run pulled a multi-GB image and, when the requested tag was missing, its availability fallback rewrote DEVA_DOCKER_TAG -- making --dry-run report a tag the user never asked for. CI caught it as a precedence-test failure that could not reproduce locally: :rust is pullable, so an earlier case pulled it, and the following `-p cloak` case then failed to pull :cloak (not published yet), found the freshly-pulled rust locally, and "resolved" to rust. The assertion was right; the dry run was lying. Resolution is what --dry-run reports; availability is a launch-time concern. Skipping check_image on dry runs also makes the precedence contract testable without any registry state -- the suite now passes with docker entirely absent. Refs #456
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
DEV-LOGS.md:19
- This dev-log entry says the built cloak image used wrapper 0.5.2, but the repo pin is CLOAKBROWSER_WRAPPER_VERSION=0.4.12 (versions.env) and Dockerfile.cloak defaults to 0.4.12. Please reconcile so readers don't assume a different wrapper/Chromium than what will be built/released.
- Live verification against the built `:cloak` image (wrapper 0.5.2, Chrome 146.0.7680.177): x11vnc answers the RFB handshake on :5900 with `RFB 003.008`; cloak-browserd serves CDP on 127.0.0.1:9222; a separate playwright-core client `connectOverCDP`s, drives a page, and the daemon survives that client's `close()`. Stealth confirmed live: UA spoofs `Windows NT 10.0`, `navigator.webdriver === false`. First probe raced the daemons — both are up ~4s after boot, so poll the ports, don't poll only the display.
| npm install --no-audit --no-fund \ | ||
| "cloakbrowser@${CLOAKBROWSER_WRAPPER_VERSION}" playwright-core && \ | ||
| ./node_modules/.bin/cloakbrowser install && \ |
| ```bash | ||
| x11vnc -display :99 -rfbport 5900 -forever -shared -bg # add -passwd <pw> to require a password | ||
| ``` |
Supersedes #487, which GitHub auto-closed when its base branch (feat/kimi-455)
was deleted on merge of #486. Same content, rebased onto main.
deva.sh -p cloak <agent>runs agents in a new image (Dockerfile.cloak,extends :rust) with CloakBrowser stealth Chromium, headed on Xvfb :99 +
openbox. Chromium is baked at build time (hermetic, auto-update off);
CLOAKBROWSER_WRAPPER_VERSION pins the wrapper and thereby Chromium.
Authenticated sessions, three composable pieces:
so one login survives container removal
container directly (x11vnc on demand, no flag).
--cloak-vncis theDocker-Desktop fallback that publishes VNC on a host-loopback port
--cloak-browser: an always-on daemon holds one headed persistent browserwith a loopback CDP endpoint; the agent drives it via connectOverCDP --
the same live browser, not a throwaway
Image policy, the part worth arguing about
cloak is an auxiliary image on its own cadence, NOT a general releasable
profile. It holds no agent CLIs and only changes when Dockerfile.cloak,
cloak-entrypoint.sh or CLOAKBROWSER_WRAPPER_VERSION change -- never on a
deva.sh version bump. So:
continue-on-errorand is OUT of thereleasejob's
needs. A ~200MB Chromium bake per arch (arm64 under QEMU) must notsit on the critical path of every release, and a flake in an optional layer
must not fail a release whose artifact is deva.sh
Follow-up #485 makes this policy data instead of a comment: profile identity
is currently duplicated across deva.sh, Makefile and three workflows.
Also fixed here: a .deva config setting DEVA_DOCKER_TAG silently overrode the
real environment and made CLI -p a no-op. Precedence is now
environment > config files > -p profile > default, guarded by
scripts/test-image-precedence.sh in CI.
Test plan
x11vnc answers
RFB 003.008on :5900; cloak-browserd serves CDP on127.0.0.1:9222; a separate playwright-core client connectOverCDPs,
drives a page, and the daemon survives that client's close()
=== false
unverified
hangs on a fresh daemon page; use goto(data:, domcontentloaded)
Closes #456