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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Stable trace URL via portless (vercel-labs): when the `portless` CLI
is on the host, `--trace` registers/refreshes the `cctrace` alias
for the published UI port and announces the routed URL — the live
UI and `/dashboard` are always at `https://cctrace.localhost`
regardless of which port the run landed on. Best-effort no-op
without portless; `DEVA_TRACE_PORTLESS=0` disables. A host `PORT`
env is also honored as the publish port (portless-wrapped runs),
and `DEVA_TRACE_URL` still overrides the announced URL outright.
`DEVA_TRACE_UI_URL` (the host-reachable UI URL) is now exported
into every traced container on create and reattach — contract for
the claude-code-statusline trace chip, which can't derive the host
port from container-side env (#547)
- KIMI_WEBBRIDGE_VERSION as a managed pin: `make versions-up` and
`make versions-pin` resolve Kimi WebBridge from
cdn.kimi.com/webbridge/latest/version.json (no npm package; same
Expand Down Expand Up @@ -36,6 +48,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
identity as a launch flag, official CLIs stock, zero workflow tax
(#536)

### Fixed
- `--trace` UI dead on arrival with cctrace >= 0.36: its default port
moved 9317 -> 8722, so the container bound 8722 while deva published
9317 and the poll-then-open never connected. All four traced agent
commands now pin `cctrace --port 9317`, which also keeps existing
containers' 9317 mappings valid (#547)
- False "created without the trace port" warning on every traced
reattach under `HOST_NET=true`: host networking makes `-p` a docker
no-op and `docker port` permanently empty. Trace plumbing now
detects host networking (skips the publish, announces
`http://127.0.0.1:9317` directly, checks `NetworkMode` on reattach);
the real-mismatch warning now names the container and the exact
`deva rm` command (#547)

## [0.18.2] - 2026-08-04

### Changed
Expand Down
5 changes: 5 additions & 0 deletions DEV-LOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
- Minimal markdown markers, no unnecessary formatting, minimal emojis.
- Reference issue numbers in the format `#<issue-number>` for easy linking.

# [2026-08-09] Dev Log: --trace port pin, HOST_NET awareness, portless URL #547
- Why: `--trace` was broken two ways — cctrace >= 0.36 binds 8722 while deva publishes 9317 (browser poll never connects), and HOST_NET=true makes `docker port` permanently empty (false "created without the trace port" warning on every reattach).
- What: `cctrace --port 9317` pinned in all four traced agents; setup_trace_ui_port/announce_trace_ui detect host networking (skip -p, loopback URL, NetworkMode check on reattach); host PORT env honored (portless-style); portless `cctrace` alias registered per traced launch so the UI/dashboard live at a stable https://cctrace.localhost route (DEVA_TRACE_PORTLESS=0 opts out, DEVA_TRACE_URL overrides); DEVA_TRACE_UI_URL exported into the container on create AND reattach for the statusline trace chip.
- Result: `deva claude --trace` opens the UI again under both port-publish and HOST_NET; dashboard has one bookmarkable URL. Statusline chip contract: prefer DEVA_TRACE_UI_URL, fallback http://localhost:$CCTRACE_SERVER_PORT (patch proposed against statusline PR #9). Open: multi-session cctrace in one published container walks to 9318+ unpublished.

# [2026-08-07] Dev Log: KIMI_WEBBRIDGE_VERSION joins pin management #544
- Why: #543 (WebBridge browser tools for all agents) needs a pinned daemon+extension version, and pins added outside the flow rot — the round-trip guard exists because the heredoc is a second copy of the layout. Land the pin machinery first so #543 only adds consumers.
- What: WebBridge has no npm package; the CDN's version-first layout publishes latest/version.json ({"version":"vX.Y.Z", binaries with per-arch sha256}) — shared _webbridge_cdn_latest in release-utils.sh parses that. versions.env + VERSION_PIN_VARS + heredoc get KIMI_WEBBRIDGE_VERSION (v1.11.5). version-upgrade.sh resolves it outside the tool registry exactly like the cloakbrowser wrapper: CLI override → CDN → pin fallback, staleness gates the nothing-to-upgrade exit, --only kimi-webbridge, manifest display, pin-what-was-resolved. update-version-pins.sh gets a webbridge fetch type; versions-pr.sh a label; Makefile forwards the override. Tests: version.json fixture in both working curl fakes (tripwire hard-errors unknown URLs), pin-write assertion v1.12.0; outage + round-trip unchanged.
Expand Down
2 changes: 1 addition & 1 deletion agents/claude.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ agent_prepare() {
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
DEVA_TRACE_ACTIVE=true
setup_trace_ui_port
AGENT_COMMAND=("cctrace" "--no-open" "--")
AGENT_COMMAND=("cctrace" "--no-open" "--port" "9317" "--")
if [ "$has_dangerously" = false ]; then
AGENT_COMMAND+=("--dangerously-skip-permissions")
fi
Expand Down
2 changes: 1 addition & 1 deletion agents/codex.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ agent_prepare() {
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
DEVA_TRACE_ACTIVE=true
setup_trace_ui_port
AGENT_COMMAND=("cctrace" "codex" "--no-open" "--" "${AGENT_COMMAND[@]:1}")
AGENT_COMMAND=("cctrace" "codex" "--no-open" "--port" "9317" "--" "${AGENT_COMMAND[@]:1}")
fi

DOCKER_ARGS+=("-p" "127.0.0.1:1455:1455")
Expand Down
2 changes: 1 addition & 1 deletion agents/grok.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ agent_prepare() {
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
DEVA_TRACE_ACTIVE=true
setup_trace_ui_port
AGENT_COMMAND=("cctrace" "grok" "--no-open" "--" "${AGENT_COMMAND[@]:1}")
AGENT_COMMAND=("cctrace" "grok" "--no-open" "--port" "9317" "--" "${AGENT_COMMAND[@]:1}")
fi

setup_grok_auth "$AUTH_METHOD"
Expand Down
2 changes: 1 addition & 1 deletion agents/kimi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ agent_prepare() {
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
DEVA_TRACE_ACTIVE=true
setup_trace_ui_port
AGENT_COMMAND=("cctrace" "kimi" "--no-open" "--" "${AGENT_COMMAND[@]:1}")
AGENT_COMMAND=("cctrace" "kimi" "--no-open" "--port" "9317" "--" "${AGENT_COMMAND[@]:1}")
fi

setup_kimi_auth "$AUTH_METHOD"
Expand Down
114 changes: 92 additions & 22 deletions agents/shared_auth.sh
Original file line number Diff line number Diff line change
Expand Up @@ -249,30 +249,86 @@ filter_trace_flag() {
done
}

# Publish the cctrace live UI (container port 9317, binds 0.0.0.0) to the
# host loopback so the browser can reach it. Probe host ports from 9317 so
# Publish the cctrace live UI to the host loopback so the browser can reach
# it. cctrace is pinned to container port 9317 via --port (its 0.36+ default
# moved to 8722; 9317 keeps existing containers' mappings valid). Host side:
# honor $PORT when a portless-style router set it (portless assigns the port
# and routes https://<name>.localhost to it), else probe from 9317 so
# concurrent traced containers land on predictable neighbors (#425).
# DEVA_TRACE_URL overrides the announced URL (e.g. the portless route name).
DEVA_TRACE_UI_URL=""

# Host networking makes -p a docker no-op and `docker port` permanently
# empty — the container binds the host loopback directly, so the UI lives
# at cctrace's pinned port with no publish at all.
_trace_host_network_args() {
local joined=" ${DOCKER_ARGS[*]+"${DOCKER_ARGS[*]}"} ${EXTRA_DOCKER_ARGS[*]+"${EXTRA_DOCKER_ARGS[*]}"} "
case "$joined" in
*" --net host "* | *" --network host "* | *" --net=host "* | *" --network=host "*) return 0 ;;
esac
return 1
}

# Stable trace URL via portless (vercel-labs): register/refresh the
# `cctrace` alias for the host-reachable UI port so the dashboard is
# always at the same named URL (e.g. https://cctrace.localhost) no
# matter which port this run landed on. Best-effort: silent no-op when
# the portless CLI is absent or the proxy is down. Prints the routed
# URL on success. DEVA_TRACE_PORTLESS=0 disables.
_trace_portless_url() {
local port="$1"
[ "${DEVA_TRACE_PORTLESS:-1}" = "1" ] || return 1
command -v portless >/dev/null 2>&1 || return 1
portless alias cctrace "$port" >/dev/null 2>&1 || return 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip portless registration during dry runs

When portless is installed, deva <agent> --trace --dry-run still reaches this call before the dry-run exit and mutates the global cctrace alias. If an existing traced session currently owns that alias, the dry run can redirect https://cctrace.localhost to the newly probed port even though no container is started there, breaking the live session's stable URL. Guard this registration when DRY_RUN=true while still computing a display-only URL.

Useful? React with 👍 / 👎.

local url
url=$(portless get cctrace 2>/dev/null | head -1)
[ -n "$url" ] || return 1
printf '%s' "$url"
}

# Resolve the announced/exported UI URL for a host-reachable port.
# Precedence: explicit DEVA_TRACE_URL > portless route > raw loopback.
_trace_resolve_ui_url() {
local port="$1"
local routed=""
routed=$(_trace_portless_url "$port") || routed=""
printf '%s' "${DEVA_TRACE_URL:-${routed:-http://127.0.0.1:${port}}}"
}
Comment on lines +291 to +296

setup_trace_ui_port() {
local port=9317
local tries=0
if _trace_host_network_args; then
DEVA_TRACE_UI_URL="$(_trace_resolve_ui_url 9317)"
DOCKER_ARGS+=("-e" "DEVA_TRACE_UI_URL=${DEVA_TRACE_UI_URL}")
return 0
fi

local free_port=""
while [ "$tries" -lt 12 ]; do
if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
free_port="$port"
break
fi
port=$((port + 1))
tries=$((tries + 1))
done
if [ -n "${PORT:-}" ] && [[ "${PORT}" =~ ^[0-9]{2,5}$ ]]; then
free_port="$PORT"
else
local port=9317
Comment on lines 305 to +309
local tries=0
while [ "$tries" -lt 12 ]; do
if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
free_port="$port"
break
fi
port=$((port + 1))
tries=$((tries + 1))
done
fi

if [ -z "$free_port" ]; then
echo "warning: no free host port in 9317-9328; trace UI will not be reachable from the host" >&2
return 0
fi

DOCKER_ARGS+=("-p" "127.0.0.1:${free_port}:9317")
DEVA_TRACE_UI_URL="http://127.0.0.1:${free_port}"
DEVA_TRACE_UI_URL="$(_trace_resolve_ui_url "$free_port")"
# Host-reachable URL for in-container tooling (statusline trace chip):
# the container-side CCTRACE_SERVER_PORT is not the port the host
# browser can reach, this is.
DOCKER_ARGS+=("-e" "DEVA_TRACE_UI_URL=${DEVA_TRACE_UI_URL}")
}

# Print the trace UI URL before the TUI takes the screen, and open the host
Expand All @@ -284,15 +340,27 @@ announce_trace_ui() {

local url="$DEVA_TRACE_UI_URL"
if [ "${1:-new}" = "existing" ]; then
# docker port exits non-zero for unpublished mappings; under
# set -euo pipefail that must not kill the launch.
local mapping
mapping=$(docker port "$CONTAINER_NAME" 9317/tcp 2>/dev/null | head -1 || true)
if [ -z "$mapping" ]; then
echo "warning: trace UI not reachable — container was created without the trace port; recreate it (deva rm) or use --rm" >&2
return 0
# Host-network containers never have port mappings — the UI binds
# the host loopback directly at the pinned cctrace port.
local netmode
netmode=$(docker inspect -f '{{.HostConfig.NetworkMode}}' "$CONTAINER_NAME" 2>/dev/null || true)
if [ "$netmode" = "host" ]; then
url="$(_trace_resolve_ui_url 9317)"
else
# docker port exits non-zero for unpublished mappings; under
# set -euo pipefail that must not kill the launch.
local mapping
mapping=$(docker port "$CONTAINER_NAME" 9317/tcp 2>/dev/null | head -1 || true)
if [ -z "$mapping" ]; then
echo "warning: trace UI not reachable — $CONTAINER_NAME was created without the trace port; recreate it (deva rm $CONTAINER_NAME) or use --rm" >&2
DEVA_TRACE_UI_URL="" # don't hand a dead URL to in-container tooling
return 0
fi
url="$(_trace_resolve_ui_url "${mapping##*:}")"
fi
url="http://127.0.0.1:${mapping##*:}"
# Live value wins over the create-time guess; the exec env carries
# it to in-container tooling (statusline trace chip).
DEVA_TRACE_UI_URL="$url"
fi
[ -n "$url" ] || return 0

Expand All @@ -317,7 +385,9 @@ maybe_open_trace_ui() {
(
local i=0
while [ "$i" -lt 60 ]; do
if curl -sf -o /dev/null --max-time 1 "$url/" 2>/dev/null; then
# -k: portless https routes use a local CA curl may not trust;
# this is a loopback readiness probe, not a trust decision.
if curl -skf -o /dev/null --max-time 1 "$url/" 2>/dev/null; then
Comment on lines +388 to +390
"$opener" "$url" >/dev/null 2>&1 || true
exit 0
fi
Expand Down
10 changes: 8 additions & 2 deletions deva.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4370,15 +4370,21 @@ if [ "$EPHEMERAL_MODE" = false ]; then

# Trace UI reachability is fixed at container create (port publish);
# attaching to a container created without it cannot gain the mapping.
# announce_trace_ui refreshes DEVA_TRACE_UI_URL from the live mapping,
# so it must run before the exec env is assembled.
announce_trace_ui existing
announce_cloak_browser existing
announce_cloak_vnc existing

_trace_exec_env=(-e "$_trace_env")
[ "${DEVA_TRACE_ACTIVE:-false}" = true ] && [ -n "${DEVA_TRACE_UI_URL:-}" ] && \
_trace_exec_env+=(-e "DEVA_TRACE_UI_URL=${DEVA_TRACE_UI_URL}")

if [ "$AUTH_PROVISION_MODE" = true ]; then
docker exec -e "$_trace_env" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true
docker exec "${_trace_exec_env[@]}" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true
finish_auth_provision
else
exec docker exec -e "$_trace_env" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}"
exec docker exec "${_trace_exec_env[@]}" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}"
fi
else
echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via $(docker_image_ref)"
Expand Down
Loading