fix(cli): render trace details by id with validation + exit-code mapping (issue #17)#20
Merged
Merged
Conversation
…ing (issue #17) Closes #17. Bug surface: `goclaw traces get <id>` was unreadable in TTY mode (dumped raw JSON), silently swallowed unmarshal errors (empty `{}`), accepted any raw id including path-traversal sequences (`..`, `/`, control chars), and collapsed every server failure to exit code 1 regardless of HTTP status. Fixes: - `cmd/traces.go` - `tracesGetCmd.RunE`: validate id allowlist before HTTP, decode payload with error surfacing, render header card + span tree + events list for TTY, pass-through JSON for `-o json` / piped stdout. - `validateTraceID`: regex allowlist `^[A-Za-z0-9._-]+$` + reserved-token reject (`.`, `..`, empty/whitespace). Returns typed `*client.APIError{Code: INVALID_REQUEST}` so the central error handler maps it to exit code 4. - `renderTraceTable` + `buildSpanTree`: insertion-ordered span tree, orphan spans attach to virtual root. No relink walk, cycle-safe (cycles silently drop). - `internal/client/http.go` - Retry loop bug: previously closed `resp.Body` on every iteration including the final attempt, then `io.ReadAll` after the loop failed with "read on closed response body". This collapsed typed `APIError` into an opaque wrapped error and lost the exit-code mapping for 5xx/429. Fixed by skipping the per-iteration close on the final attempt; outer `defer` handles cleanup. Exit-code contract for `traces get`: - 0 success / 2 permission denied / 3 not-found / 4 malformed id (pre-HTTP) / 5 server failure / 6 rate-limit. Tests: 10 new tests in `cmd/traces_get_test.go` lock the wire contract, table+JSON rendering, exit-code mapping per HTTP status, and the "malformed id makes zero HTTP calls" invariant (parameterized table). Fixture `cmd/testdata/trace_detail_get.json` is a scrubbed stub with `_TODO_refresh` marker; refresh against live gateway before merging to default. Docs: CHANGELOG `[Unreleased]` Fixed entry, README `Reading a Trace by ID` section with exit-code table, `docs/codebase-summary.md` traces bullet updated.
Contributor
Author
Review (post-CI)Verdict: Approve — safe to merge to Findings
Checks performed
Suggestions (deferred)
Notes for merge
No actionable findings. Loop terminates (0 fix iterations needed). |
mrgoonie
added a commit
that referenced
this pull request
May 28, 2026
…#21) * feat(cli): add P6 backend-unblocked CLI surfaces (issue #16) (#19) * plan(p6): add domain coverage P6 backend-unblocked CLI plan Mirrors issue #16 scope (7 command surfaces) as 4 grouped implementation phases with TDD per the user's request. Backend evidence: digitopvn/goclaw PR #37 + PR #44. Verified beta tag containing PR #44 is v3.12.0-beta.20. Phases: 1. Scope Lock — contract re-verification, drift sweep 2. Traces Follow + Providers Reconnect (PR #37) 3. Sessions Branch + Follow (PR #44 chat) 4. Channels Writers Test (PR #44 channels) 5. Activity + Logs Runtime Aggregate (PR #44 aggregation) 6. Tests + Docs sweep with out-of-scope red-team 7. Ship Readiness — single PR to dev Out-of-scope list retained verbatim from issue #16: traces replay, generic logs aggregate, WS/SSE chat history, watch loops. * feat(cli): add P6 backend-unblocked CLI surfaces (issue #16) Seven one-shot HTTP commands wired to backend PRs #37 and #44 (gateway tag v3.12.0-beta.20+). TDD: failing tests landed before each implementation slice; all suites green under -count=1. PR #37 surfaces: - traces follow --session-key|--agent [--since --limit --include-spans --status --channel]: GET /v1/traces/follow - providers reconnect <id>: POST /v1/providers/{id}/reconnect PR #44 surfaces: - sessions branch <key> --up-to-index N [--new-session-key --label --metadata k=v]: POST /v1/chat/sessions/{key}/branch. --up-to-index=0 preserved literally on the wire (bypasses buildBody int-zero skip). - sessions follow <key> [--cursor N --limit N]: one-shot cursor-based history poll, NOT a watch loop. --cursor=0 preserved in raw query. - channels writers test <id> --group-id G --user-id U: POST writers/test with body containing exactly two keys. - activity aggregate --group-by {action|actor_type|entity_type|actor_id} [--from --to --limit + scope filters]: subcommand of existing activityCmd. - logs aggregate [--group-by {level|source} --level --source --from]: ring-buffer summary, distinct from logs tail. Shared formatLastSeen helper type-switches RFC3339-string vs epoch-millis last_seen so neither aggregate command renders scientific notation in table view. All path params url.PathEscape'd; all flag validation runs before HTTP call; central error handler honored (no direct error printing). Docs: README + docs/codebase-summary + CHANGELOG updated. Closes #16 * fix(cli): render trace details by id with validation + exit-code mapping (issue #17) (#20) Closes #17. Bug surface: `goclaw traces get <id>` was unreadable in TTY mode (dumped raw JSON), silently swallowed unmarshal errors (empty `{}`), accepted any raw id including path-traversal sequences (`..`, `/`, control chars), and collapsed every server failure to exit code 1 regardless of HTTP status. Fixes: - `cmd/traces.go` - `tracesGetCmd.RunE`: validate id allowlist before HTTP, decode payload with error surfacing, render header card + span tree + events list for TTY, pass-through JSON for `-o json` / piped stdout. - `validateTraceID`: regex allowlist `^[A-Za-z0-9._-]+$` + reserved-token reject (`.`, `..`, empty/whitespace). Returns typed `*client.APIError{Code: INVALID_REQUEST}` so the central error handler maps it to exit code 4. - `renderTraceTable` + `buildSpanTree`: insertion-ordered span tree, orphan spans attach to virtual root. No relink walk, cycle-safe (cycles silently drop). - `internal/client/http.go` - Retry loop bug: previously closed `resp.Body` on every iteration including the final attempt, then `io.ReadAll` after the loop failed with "read on closed response body". This collapsed typed `APIError` into an opaque wrapped error and lost the exit-code mapping for 5xx/429. Fixed by skipping the per-iteration close on the final attempt; outer `defer` handles cleanup. Exit-code contract for `traces get`: - 0 success / 2 permission denied / 3 not-found / 4 malformed id (pre-HTTP) / 5 server failure / 6 rate-limit. Tests: 10 new tests in `cmd/traces_get_test.go` lock the wire contract, table+JSON rendering, exit-code mapping per HTTP status, and the "malformed id makes zero HTTP calls" invariant (parameterized table). Fixture `cmd/testdata/trace_detail_get.json` is a scrubbed stub with `_TODO_refresh` marker; refresh against live gateway before merging to default. Docs: CHANGELOG `[Unreleased]` Fixed entry, README `Reading a Trace by ID` section with exit-code table, `docs/codebase-summary.md` traces bullet updated. * docs: add planning artifacts for P6 backend-unblocked CLI and skill scaffold
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix
goclaw traces get <id>— restore readable TTY output, surface decode failures, validate ids strictly, and map distinct exit codes per failure class. Closes #17.What was broken
traces get <id>was unusable as documented:printer.Print(map[string]any)falls back to JSON in table mode.unmarshalMapsilently swallowed errors —_ = json.Unmarshal(...)produced empty{}on malformed payload.args[0]concatenated raw into URL; path-traversal (..,/,\x00, whitespace) flowed straight to the gateway.Fixes
cmd/traces.gotracesGetCmd.RunE— strict id validation → HTTP → decode-with-error → render dispatch.validateTraceID— regex allowlist^[A-Za-z0-9._-]+$+ reserved-token block (.,.., empty, whitespace). Returns typed*client.APIError{Code: INVALID_REQUEST}→ exit 4 via central handler.renderTraceTable+buildSpanTree— header card + span tree + flat events list. Insertion-ordered children, cycle-safe (cycles silently drop, no infinite recursion), orphans attach to virtual root.internal/client/http.goLatent retry-body bug. The retry loop closed
resp.Bodyon every iteration including the final attempt, then post-loopio.ReadAllfailed with"http: read on closed response body". This silently collapsed typedAPIError→ opaque wrapped error → exit 1, regardless of HTTP status. Fix: skip the per-iterationClose()on the final attempt; outerdeferhandles cleanup exactly once.Exit-code contract for
traces getTests
10 new tests in
cmd/traces_get_test.go:GET /v1/traces/{id}) and JSON round-trip.├/└) + EVENTS section.".","..", path-traversal, slashes, NUL, whitespace) — asserts exit 4 and zero HTTP calls.Fixture
cmd/testdata/trace_detail_get.jsonis a scrubbed stub with_TODO_refreshmarker. Auth-blocked smoke probe documented inplans/260528-1357-fix-trace-details-by-id/reports/repro-260528-issue17.md. Refresh against live gateway before merging this PR tomain.Verification
go vet ./...— cleango build ./...— cleango test -count=1 ./...— all packages greenplans/260528-1357-fix-trace-details-by-id/reports/code-review-260528-issue17.md.Acceptance criteria mapping
renderTraceTable+TestTracesGet_TableMode_HasHeaderAndSpanMarkersTestTracesGet_JSONMode_PreservesStructure+TestTracesGet_TableMode_*TestTracesGet_NotFound_ExitCode3/_PermissionDenied_ExitCode2/_MalformedID_NoHTTPCall/_ServerError_ExitCode5cmd/traces_get_test.goOut of scope
tracesExportCmdid validation (pre-existing; same vulnerability) — filed as follow-up.Auto-close timing
This PR targets
dev.Closes #17will auto-close the issue only whendevis promoted tomain. A comment will be posted on #17 explaining this.Plan + reports
plans/260528-1357-fix-trace-details-by-id/plan.mdplans/260528-1357-fix-trace-details-by-id/reports/repro-260528-issue17.mdplans/260528-1357-fix-trace-details-by-id/reports/code-review-260528-issue17.mdCloses #17