Skip to content

Commit c90d7db

Browse files
lroolleclaude
andcommitted
feat(trace): publish live UI to host, banner before TUI, poll-then-open
cctrace prints its Live UI URL inside the container where no browser can reach it, then the TUI paints over it. Fix the host side: - traced sessions publish container 9317 to a probed free host loopback port (9317, 9318, ... — concurrent traced containers get neighbors) - deva prints "Trace UI: http://localhost:<port>" before the agent takes the screen - host browser opens the moment the UI answers: background poll-then-open, never a connection-refused page; DEVA_TRACE_OPEN=0 disables - port publish is create-time only: traced attach to a container created without it warns with the recreate hint Closes #425 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fd93509 commit c90d7db

7 files changed

Lines changed: 95 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
containers pin old images; a CLI bump now creates a distinct container
3232
instead of silently attaching a stale CLI to the same config home.
3333
Label-less images keep the bare agent name (#420)
34+
- Traced sessions publish the cctrace live UI to the host loopback:
35+
free port probed from 9317, `Trace UI: http://localhost:<port>` printed
36+
before the TUI starts, and the host browser opens the moment the UI
37+
answers (poll-then-open; `DEVA_TRACE_OPEN=0` disables). Attaching to a
38+
persistent container created without the port warns instead (#425)
3439

3540
### Changed
3641
- cctrace pin bumped 0.4.0 -> 0.11.0 (client profiles, shape-first

agents/claude.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ agent_prepare() {
5151
# into the container system trust store (#414)
5252
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
5353
DEVA_TRACE_ACTIVE=true
54+
setup_trace_ui_port
5455
AGENT_COMMAND=("cctrace" "--no-open" "--")
5556
if [ "$has_dangerously" = false ]; then
5657
AGENT_COMMAND+=("--dangerously-skip-permissions")

agents/codex.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ agent_prepare() {
9494
# DEVA_TRACE=1 installs the MITM CA into the container store (#414).
9595
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
9696
DEVA_TRACE_ACTIVE=true
97+
setup_trace_ui_port
9798
AGENT_COMMAND=("cctrace" "codex" "--no-open" "--" "${AGENT_COMMAND[@]:1}")
9899
fi
99100

agents/grok.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ agent_prepare() {
4242
# DEVA_TRACE=1 installs the MITM CA into the container store (#414).
4343
DOCKER_ARGS+=("-e" "DEVA_TRACE=1")
4444
DEVA_TRACE_ACTIVE=true
45+
setup_trace_ui_port
4546
AGENT_COMMAND=("cctrace" "grok" "--no-open" "--" "${AGENT_COMMAND[@]:1}")
4647
fi
4748

agents/shared_auth.sh

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,74 @@ is_file_path() {
226226
[[ "$arg" == /* ]] || [[ "$arg" == ~* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ "$arg" == *.json ]]
227227
}
228228

229+
# Publish the cctrace live UI (container port 9317, binds 0.0.0.0) to the
230+
# host loopback so the browser can reach it. Probe host ports from 9317 so
231+
# concurrent traced containers land on predictable neighbors (#425).
232+
DEVA_TRACE_UI_URL=""
233+
setup_trace_ui_port() {
234+
local port=9317
235+
local tries=0
236+
while [ "$tries" -lt 12 ]; do
237+
if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
238+
break
239+
fi
240+
port=$((port + 1))
241+
tries=$((tries + 1))
242+
done
243+
DOCKER_ARGS+=("-p" "127.0.0.1:${port}:9317")
244+
DEVA_TRACE_UI_URL="http://localhost:${port}"
245+
}
246+
247+
# Print the trace UI URL before the TUI takes the screen, and open the host
248+
# browser the moment the UI answers. "new" trusts DEVA_TRACE_UI_URL (the
249+
# container doesn't exist yet); "existing" asks docker for the live mapping.
250+
announce_trace_ui() {
251+
[ "${DEVA_TRACE_ACTIVE:-false}" = true ] || return 0
252+
[ "${DRY_RUN:-false}" = true ] && return 0
253+
254+
local url="$DEVA_TRACE_UI_URL"
255+
if [ "${1:-new}" = "existing" ]; then
256+
local mapping
257+
mapping=$(docker port "$CONTAINER_NAME" 9317/tcp 2>/dev/null | head -1)
258+
if [ -z "$mapping" ]; then
259+
echo "warning: trace UI not reachable — container was created without the trace port; recreate it (deva rm) or use --rm" >&2
260+
return 0
261+
fi
262+
url="http://localhost:${mapping##*:}"
263+
fi
264+
[ -n "$url" ] || return 0
265+
266+
echo "Trace UI: $url (live once the agent starts)" >&2
267+
maybe_open_trace_ui "$url"
268+
}
269+
270+
# Poll-then-open in the background: the browser opens exactly when the UI
271+
# is up, never against a connection-refused page. DEVA_TRACE_OPEN=0 disables.
272+
maybe_open_trace_ui() {
273+
local url="$1"
274+
[ "${DEVA_TRACE_OPEN:-1}" = "1" ] || return 0
275+
276+
local opener=""
277+
if command -v open >/dev/null 2>&1; then
278+
opener="open"
279+
elif command -v xdg-open >/dev/null 2>&1; then
280+
opener="xdg-open"
281+
fi
282+
[ -n "$opener" ] || return 0
283+
284+
(
285+
local i=0
286+
while [ "$i" -lt 60 ]; do
287+
if curl -sf -o /dev/null --max-time 1 "$url/" 2>/dev/null; then
288+
"$opener" "$url" >/dev/null 2>&1 || true
289+
exit 0
290+
fi
291+
sleep 0.5
292+
i=$((i + 1))
293+
done
294+
) >/dev/null 2>&1 &
295+
}
296+
229297
AUTH_PROVISION_MODE=false
230298

231299
expand_auth_file_tilde() {

deva.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3732,6 +3732,10 @@ if [ "$EPHEMERAL_MODE" = false ]; then
37323732
_trace_env="DEVA_TRACE=0"
37333733
[ "${DEVA_TRACE_ACTIVE:-false}" = true ] && _trace_env="DEVA_TRACE=1"
37343734
3735+
# Trace UI reachability is fixed at container create (port publish);
3736+
# attaching to a container created without it cannot gain the mapping.
3737+
announce_trace_ui existing
3738+
37353739
if [ "$AUTH_PROVISION_MODE" = true ]; then
37363740
docker exec -e "$_trace_env" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true
37373741
finish_auth_provision
@@ -3741,6 +3745,7 @@ if [ "$EPHEMERAL_MODE" = false ]; then
37413745
else
37423746
echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via $(docker_image_ref)"
37433747
write_session_file
3748+
announce_trace_ui new
37443749
if [ "$AUTH_PROVISION_MODE" = true ]; then
37453750
docker "${DOCKER_ARGS[@]}" "${AGENT_COMMAND[@]}" || true
37463751
finish_auth_provision

docs/advanced-usage.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ tools that ignore `NODE_EXTRA_CA_CERTS`/`SSL_CERT_FILE` still verify. The
181181
container is the blast radius: trust never touches the host store and is
182182
removed at the next non-traced start.
183183

184+
The live trace UI (cctrace's web view, port 9317 in the container) is
185+
published to the host loopback. deva prints the URL before the TUI takes
186+
the screen and opens your host browser the moment the UI answers:
187+
188+
```text
189+
Trace UI: http://localhost:9317 (live once the agent starts)
190+
```
191+
192+
Set `DEVA_TRACE_OPEN=0` to keep the banner but skip the auto-open.
193+
Concurrent traced containers get neighboring host ports (9318, ...).
194+
Port publishing is fixed at container create: attaching to a persistent
195+
container created without `--trace` prints a warning instead — recreate
196+
it (`deva rm`) or use `--rm` for traced sessions.
197+
184198
Traces land in `.cctrace/` inside your workspace, so they survive the
185199
container. Each run writes a JSONL log plus a self-contained HTML snapshot you
186200
can open in a browser on the host. Credentials are redacted before anything

0 commit comments

Comments
 (0)