Skip to content

feat: CLI usage telemetry with coding agent detection - #318

Open
tkkhq wants to merge 57 commits into
mainfrom
feat/agent-detection
Open

tkkhq wants to merge 57 commits into
mainfrom
feat/agent-detection

Conversation

@tkkhq

@tkkhq tkkhq commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Tracking

Why

  • we can't see which commands people run, where they fail, or which coding agent ran them, so we can't tell how agents use Volcano or where they get stuck

Summary

  • add internal/agent: detect the coding agent from env vars in this order: named AI_AGENT/AGENT, harness markers (CLAUDECODE, CODEX_THREAD_ID, GEMINI_CLI, CURSOR_AGENT, OPENCODE_CLIENT, ...), VOLCANO_AGENT, bare truthy AI_AGENT/AGENT (unknown), then host-terminal markers (CURSOR_TRACE_ID, REPL_ID); exact name matches only, canonical names, values sanitized to a header-safe charset capped at 64 chars
  • send X-Volcano-Agent, -Source, -Raw, -Markers on every API request from versionProtocolDoer, and nothing when no agent is detected, telemetry is off, or the first-run notice has not been shown yet
  • add internal/telemetry: one cli_command_invoked event per run with command path, names of flags set, selected project ID, resource names and IDs, CLI version, OS, arch, CPU count, CI vendor, stdin TTY, agent name/source/markers, Volcano plugin installed + version for that agent (version sent only when it is semver-shaped), exit code, duration; every failed run adds error kind, HTTP status, API error code, and the error message
  • send the project ID only for volcano cloud ... commands, since local commands run against the local sandbox
  • take resource names only from positional args under an allowlisted usage-line placeholder (<database>, <name-or-id>, <bucket>, ...) and from the name, function, database, project, topic, bucket flags, keyed by placeholder or flag name; paths, queries, payloads, and sandbox commands are never sent, and values that don't look like a name, contain / (paths), or look like a secret are dropped; a flag can opt out with sensitive.FlagAnnotation, and durable start --name (an idempotency key) does
  • before sending an error message, replace the home dir with ~, strip ANSI escapes and control characters, collapse whitespace, then redact bearer tokens, JWTs, Volcano keys and tokens (pk-/pt-/sk-/ak-/vbt_/vpg_/sbp1.), URL user info, email addresses, and URL query strings, and cut to 500 runes; the API applies the same steps again, and its docs/internal/guides/cli-telemetry.md is the wire contract
  • redact by placement: the rules cover the token formats Volcano issues and the places a documented command puts a credential or email (auth headers, URLs, API error text), and internal/sensitive covers errors that quote files or application data; a secret typed where no command takes one, such as volcano DB_PASSWORD=hunter2, is not looked for and is sent as typed; docs/telemetry.md tells users to pass secrets only through the inputs meant for them
  • add internal/sensitive to mark errors whose text can carry file contents or application data; telemetry replaces that part with [redacted] while the terminal still shows it; used for volcano-config.yaml YAML/interpolation/decode/validation errors, env file parse errors (godotenv quotes the file), PostgreSQL errors from migrations, a function's passthrough error body in volcano functions invoke, and an execution name passed where volcano durable expects an execution ID; an API error code inside a sensitive error is dropped too
  • send the event from a detached volcano telemetry flush process to POST {apiURL}/cli/telemetry with the saved login token when present, so commands never wait on the network
  • the notice points to volcano telemetry --help, which ships in the binary, since docs caches synced before this release lack cli/telemetry.md
  • add setup.PluginStatus to answer "is the Volcano plugin installed for this harness" from local files, without the harness presence probe
  • add opt-out: volcano telemetry disable|enable|status and VOLCANO_TELEMETRY_DISABLED=1; VOLCANO_TELEMETRY_DEBUG=1 prints the event instead of sending (off unless set); volcano telemetry commands are never recorded, so an opt-out isn't itself reported
  • print a one-time notice on the first recorded run (it says events are linked to the account when logged in) and send nothing on that run, so users can opt out before any event leaves the machine; save the state before printing, so a read-only home (sandboxed agents) shows no notice instead of repeating it
  • store device id, opt-out, and notice state in a telemetry block of ~/.volcano/config.json, updated through config.Update so a first run can't overwrite a concurrent volcano telemetry disable
  • a process that started before the first-run notice never sends, even when a concurrent process showed the notice first
  • read the setting once per process: the agent headers and the event follow the read at start, and the flush doesn't recheck, so a change applies from the next command
  • change volcano logout to clear the token, user ID, current project, and function aliases instead of deleting config.json, so the telemetry setting and docs source survive a logout; the file is still removed when nothing else is in it, or when it can't be read or parsed (so logout still removes the token from a damaged or unreadable file)
  • never record shell completion (__complete, which runs on every Tab with stderr discarded, and volcano completion <shell>, which rc files run on every new shell) or --offline runs, checking raw args too so a flag parse error can't bypass it
  • run the detached flush from the temp dir so it never holds the caller's working directory open
  • record from run() after the exit code is known, using ExecuteC for the executed command; telemetry never changes the exit code or output beyond the one-time notice
  • turn telemetry off in the API and local-mode e2e env helpers and the CI smoke step
  • treat COPILOT_MODEL/COPILOT_ALLOW_ALL as weak signals, since people export them in their own profile; map pi to its volcano setup harness
  • add docs/telemetry.md and docs/agent-detection.md, linked from docs/README.md and docs/setup.md
  • the agent name list lives in three places and must change together: internal/agent aliases and markers here, allowedAgents in volcano-hosting internal/productanalytics/allowlist.go, and the harness list in volcano-skills AGENTS.md
  • merge order: the CLI posts to /cli/telemetry, which needs Kong/volcano-hosting#1919 deployed first; before that the flush gets a 404 in the background and nothing breaks
  • defer a first-run consent prompt: telemetry is opt-out like the Vercel and Next.js CLIs; whether EU ePrivacy rules require opt-in consent for reading local env vars, and how GDPR applies to resource names and error messages that may carry personal data, and whether a secret a user mistypes into a command argument reaching PostHog is acceptable, are open questions for legal, to be answered before a release containing this ships (not a merge blocker)

Verification

  • make lint
  • go test ./...
  • go test -race ./internal/agent/ ./internal/api/
  • ran the built binary with a fresh HOME against a local sink: first run printed the notice once and sent nothing, each later run posted one event to /cli/telemetry with the expected command, flags, agent, exit code, and error kind; a failing cloud functions get hello carried the project ID, {"name-or-id": "hello"}, and the error message; volcano telemetry status/disable sent nothing; DO_NOT_TRACK=1 no longer opts out; after volcano telemetry disable nothing was sent; volcano logout kept the telemetry block in config.json
  • ran the built binary with a read-only HOME (no notice on repeated runs) and __complete (no state written)
  • ran volcano projects list twice with a fresh HOME against a local server: the first run sent no X-Volcano-Agent header and no event; the second sent both
  • ran the built binary against a local header-echo server: Claude Code shell, CODEX_THREAD_ID + CURSOR_TRACE_ID, VOLCANO_AGENT only, a CRLF injection attempt in AI_AGENT, and a clean env (no headers)

@tkkhq
tkkhq requested a review from a team as a code owner October 15, 2026 02:35
Copilot AI balanced review requested due to automatic review settings October 15, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

AI review: no P0-P2 findings

Reviewed fed48e1. Run log

This PR adds opt-out CLI usage telemetry: one event per command, sent to /cli/telemetry by a detached volcano telemetry flush process. It also adds coding-agent detection, reported as X-Volcano-Agent* request headers, and a sensitive marker that keeps file and application data out of error messages. volcano logout now clears only login fields instead of deleting config.json. The code handles the first-run notice, concurrent opt-outs, completion and --offline skips, and redaction carefully. The only defect found is a privacy statement in the published docs that overstates what is withheld. The main risk is the policy itself: the description says the legal questions are still open.

⚪ P3

  • docs/telemetry.md:71 Docs say no environment variable values are sent, but network errors send the VOLCANO_API_URL host

Not checked: The AI review read the diff, the code, and the PR's text; it ran no tests or builds and read nothing outside this repository.

  • Whether Kong/volcano-hosting#1919/#1937 is deployed and accepts this payload: field names, the resource-key allowlist, the 16 KiB body limit, and bearer tokens on /cli/telemetry.
  • Whether the detached flush actually outlives the parent and never blocks it on Windows and macOS (process detachment and the stdin pipe-buffer assumption).
  • Whether current releases of Claude Code, Codex, Gemini CLI, Cursor, OpenCode and the others still set the listed marker variables.
  • Verify that the deployed volcano-hosting telemetry endpoint accepts this payload and applies the matching allowlist and redaction rules.

Human review: needed

  • Should opt-out telemetry (resource names, error messages, account-linked machine ID) merge onto the release path while the ePrivacy/GDPR questions are still open?
  • Is a one-time stderr notice enough consent when an agent runs the CLI first and the human may never see it?
  • Is it acceptable that volcano logout keeps config.json, so the machine ID links later logged-out events to the prior account?
  • Should resource names and redacted error messages be collected by default and remain linkable across logout through a persistent machine ID (internal/telemetry/telemetry.go, internal/config/config.go…

The AI review found no P0-P2 problems at fed48e1, so the code needs no second review for bugs. Decide the points above: approve to accept them, or request changes.

Suggested reviewer: subnetmarco (Edited docs/README.md and internal/api/errors.go; suited to the product and privacy call). Any collaborator with write access other than the author can approve.

Comment thread docs/agent-detection.md Outdated
| Claude Code | `CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT` |
| Codex | `CODEX_THREAD_ID`, `CODEX_SANDBOX`, `CODEX_CI` |
| Gemini CLI | `GEMINI_CLI` |
| Cursor | `CURSOR_EXTENSION_HOST_ROLE=agent-exec` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Docs list Cursor before Cursor CLI, but the code checks Cursor CLI first

The doc says "If several are set, the first row in this table wins" and lists Cursor (CURSOR_EXTENSION_HOST_ROLE=agent-exec) above Cursor CLI (CURSOR_AGENT). In internal/agent/agent.go:92-93, markers puts CURSOR_AGENT → cursor-cli first. Running cursor-agent from a Cursor agent-exec terminal sets both variables, so the CLI reports cursor-cli while the published doc says cursor. Fix: swap the two table rows so they match the code.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: docs/agent-detection.md now lists Cursor CLI (CURSOR_AGENT) above Cursor, matching the order of markers in internal/agent/agent.go.

Comment thread internal/agent/agent.go Outdated
{env: "ANTIGRAVITY_AGENT", agent: "antigravity"},
// COPILOT_GITHUB_TOKEN is left out on purpose: people export it in their
// own shell profile, so it does not show an agent ran the command.
{env: "COPILOT_MODEL", agent: "github-copilot"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: COPILOT_MODEL and COPILOT_ALLOW_ALL may be shell-profile settings, not proof an agent ran

The code skips COPILOT_GITHUB_TOKEN because people export it in their shell profile. As far as I know, Copilot CLI also documents COPILOT_MODEL (choose the model) and COPILOT_ALLOW_ALL (auto-approve tools) as settings users export themselves. If so, a user with export COPILOT_MODEL=... in .zshrc gets every command they type tagged github-copilot with source marker. Because step-2 markers outrank VOLCANO_AGENT, an agent that identifies itself only through VOLCANO_AGENT would also be reported as Copilot. Consider dropping these two markers, or treating them as a host-level signal, unless Copilot sets them in its own command shells.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: COPILOT_MODEL and COPILOT_ALLOW_ALL are now weak host markers (host: true), so they only count when nothing stronger is set and never outrank VOLCANO_AGENT. The docs describe them as settings people can export in their own shell profile.

Comment thread docs/setup.md Outdated

## Agent detection

Once set up, the CLI reports which agent ran each command to the Volcano API.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: "Once set up" suggests reporting depends on volcano setup

The new section says "Once set up, the CLI reports which agent ran each command." Detection actually runs on every API request whatever the setup state: detectAgent in versionProtocolDoer reads only environment variables and never checks whether volcano setup ran. A user who skips setup could wrongly think nothing is reported. Fix: say the CLI reports the agent on every API request whether or not volcano setup has run.

Found by claude-opus-5-5

@tkkhq tkkhq Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the "Once set up" wording is gone. The doc now says the CLI reports the agent with each request when a coding agent runs a command, with no tie to volcano setup. It also says the headers wait for the first-run telemetry notice and stop when telemetry is off.

Comment thread docs/agent-detection.md Outdated
| Claude Code | `CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT` |
| Codex | `CODEX_THREAD_ID`, `CODEX_SANDBOX`, `CODEX_CI` |
| Gemini CLI | `GEMINI_CLI` |
| Cursor | `CURSOR_EXTENSION_HOST_ROLE=agent-exec` |

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: Cursor marker precedence is documented in the wrong order

When both CURSOR_EXTENSION_HOST_ROLE=agent-exec and CURSOR_AGENT=1 are set without an earlier match, this table promises cursor because the first row wins. However, internal/agent/agent.go checks CURSOR_AGENT first and reports cursor-cli. Swap these rows to match the implemented precedence.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the doc table now puts Cursor CLI above Cursor, matching the code's precedence.

Comment thread docs/agent-detection.md Outdated
| `X-Volcano-Agent-Raw` | `claude-code_2-1-299_agent` | The original value of `AI_AGENT`, `AGENT`, or `VOLCANO_AGENT`, when it differs from the name |
| `X-Volcano-Agent-Markers` | `claude-code,cursor` | Every agent whose step 2 or step 5 variables are set. More than one means one agent ran inside another, for example Claude Code run from a Cursor terminal |

Values are limited to letters, digits, and `._-/@+:`, and to 64 characters.

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: The stated header limits do not apply to the markers header

setAgentHeaders in internal/api/version_protocol.go joins marker names with commas without sanitizing or truncating the combined value. Consequently, even the documented claude-code,cursor example contains an excluded character, and sufficiently many markers can exceed 64 characters. Scope this statement to the environment-derived name and raw fields, and describe the marker list separately.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the docs scope the 64-character and character-set limits to the name and raw values, and describe the markers header separately as a comma-separated list of names from the table.

Comment thread docs/agent-detection.md
| Augment | `AUGMENT_AGENT` |
| Antigravity | `ANTIGRAVITY_AGENT` |
| GitHub Copilot | `COPILOT_MODEL`, `COPILOT_ALLOW_ALL` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Docs table lists Cursor before Cursor CLI, but the code checks them the other way round

The doc says "If several are set, the first row in this table wins" and puts CURSOR_EXTENSION_HOST_ROLE=agent-exec (Cursor) above CURSOR_AGENT (Cursor CLI). In internal/agent/agent.go the markers slice puts CURSOR_AGENT first. So when both are set, the CLI reports cursor-cli, while the published doc says cursor. Fix: swap the two table rows, or swap the two marker entries, so the doc and code agree.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the table now lists Cursor CLI first, matching markers in internal/agent/agent.go.

@tkkhq tkkhq changed the title feat: report which coding agent ran each CLI command feat: CLI usage telemetry with coding agent detection Oct 15, 2026
@github-actions
github-actions Bot requested a review from subnetmarco October 15, 2026 03:54
Comment thread internal/telemetry/telemetry.go Outdated

func printNotice(w io.Writer) {
fmt.Fprintln(w, "Note: the Volcano CLI collects usage data: the command and flag names you use (never their values), CLI version, platform, and which coding agent ran it. This helps us improve the CLI.")
fmt.Fprintf(w, "Turn it off with `volcano telemetry disable` or %s=1. Details: `volcano docs get telemetry`.\n", EnvDisabled)

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: First-run notice points users to volcano docs get telemetry, which cannot resolve

docs.Service.Get reads the cached snapshot path exactly as given (snap.readFile(docPath) after safeRelPath). It adds no .md extension and does no fuzzy lookup, and doc IDs look like cli/deploy.md. AGENTS.md says this repo's docs/ syncs under /cli, so the new page will be at cli/telemetry.md. Running the exact command the notice prints fails with a doc-not-found error, at the one moment a user is told where to read about data collection. Change the hint to volcano docs get cli/telemetry.md, or point to the published URL.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: the notice now points to volcano telemetry --help, which needs no docs cache.

Comment thread internal/telemetry/telemetry.go Outdated
// swallowed, since telemetry must not change what the CLI does.
func (r Recorder) Record(inv Invocation) {
r = r.withDefaults()
if inv.Command != nil && inv.Command.Annotations[SkipAnnotation] != "" {

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: Every shell-completion request is recorded, spawns a flush process, and can silently use up the first-run notice

Only commands annotated with SkipAnnotation are skipped. Cobra's hidden __complete command (added by ExecuteC because the root does not disable completion) is returned as cmd, so every Tab press records an event. Each one also starts a detached volcano telemetry flush process that makes an HTTP POST. Cobra's bash, zsh, and fish completion scripts run __complete with 2>/dev/null. If a user's first interaction is a Tab completion, printNotice writes to a discarded stderr, NoticeShown is saved as true, and the user never sees the notice. Skip cobra.ShellCompRequestCmd / ShellCompNoDescRequestCmd (and possibly the completion command) in Record.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: shell completion (__complete and volcano completion <shell>) is skipped and never recorded, so it neither spawns a flush nor uses up the notice.

Comment thread internal/telemetry/telemetry.go Outdated
if state.DeviceID == "" {
state.DeviceID = uuid.NewString()
}
if !state.NoticeShown && inv.Stderr != nil {

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: Notice is printed on every command when ~/.volcano/telemetry.json cannot be written

Record prints the notice and sets NoticeShown, then returns if State.Save fails, so the flag is never saved. When HOME is readable but not writable, every invocation prints the two-line notice to stderr again and sends no event. That covers sandboxed agents (Codex's default workspace-write sandbox, Claude Code's sandbox), read-only containers, and Nix-style builds. These are the agent runs this feature targets, and every command's output picks up the noise. Save the state before printing, and print only when the save succeeds (or skip the notice when state cannot be saved).

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: when the config cannot be saved (read-only home, sandbox), initialize returns an error and the CLI neither prints the notice nor sends anything. docs/telemetry.md says so.

Comment thread docs/telemetry.md Outdated
failure (`api`, `network`, `local_not_running`, `usage`, or `other`) and, for an
API error, its HTTP status and error code.

The CLI never sends flag values, command arguments, file contents, project or

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Docs say environment variable values are never sent, but free-text agent values are

For an unrecognized AI_AGENT, AGENT, or VOLCANO_AGENT value, canonical returns the sanitized, lowercased value itself. That value goes out as agent in the telemetry event and as X-Volcano-Agent/X-Volcano-Agent-Raw on every API request. AGENT in particular is a generic name a user may have set for something else. The privacy statement "never sends ... environment variable values" is therefore not accurate. Qualify it (e.g. "other than the agent name read from AI_AGENT/AGENT/VOLCANO_AGENT").

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: docs/telemetry.md now names the two exceptions: the project ID from VOLCANO_PROJECT_ID, and the agent name and raw value from AI_AGENT, AGENT, or VOLCANO_AGENT, with the limits applied to them.

}
return
}
_ = r.Send(payload)

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: Honor the docs command's offline guarantee

With telemetry enabled, volcano docs list --offline still starts a background POST to /cli/telemetry, even when the docs command fails because its cache is missing. This breaks the explicit “Never touch the network” contract in internal/cmd/docs/docs.go and docs/docs-search.md. Suppress network telemetry for invocations using this offline mode.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: --offline runs are never recorded, including when flag parsing fails before cobra reads the flag (offlineArg checks the raw arguments).

Comment thread internal/telemetry/telemetry.go Outdated
printNotice(inv.Stderr)
state.NoticeShown = true
}
if err := r.State.Save(state); err != nil {

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: Prevent initialization from overwriting a concurrent opt-out

A first-run recorder can load enabled state, overlap with volcano telemetry disable saving disabled:true, then overwrite that preference with its stale state here. Subsequent invocations resume telemetry despite the successful opt-out. Atomic rename prevents partial JSON but does not protect this read-modify-write sequence; serialize initialization and preference updates with a cross-process lock.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: initialize writes the device ID and notice flag under the config lock and re-reads the saved state, so a concurrent volcano telemetry disable is never overwritten.

case "opencode":
return "opencode"
}
return ""

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: Map the supported pi harness before checking plugin status

After volcano setup --agent pi installs skills into ~/.pi/agent/skills, an invocation with VOLCANO_AGENT=pi reaches this fallback and checks ~/.volcano/skills instead. It therefore reports plugin_installed:false for a valid Pi installation, or reports an unrelated manual installation. Add the pi mapping supported by internal/setup/harness.go.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: harnessFor maps pi and checks ~/.pi/agent/skills, matching internal/setup/harness.go.

Comment thread internal/telemetry/telemetry.go Outdated
// wait or the login poll, is not one; an HTTP request that times out still
// counts, since the client wraps it in *url.Error.
func isNetworkError(err error) bool {
if _, ok := errors.AsType[*url.Error](err); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: isNetworkError labels every *url.Error as a network failure, including URL parse errors and Ctrl-C

*url.Error is returned by url.Parse as well as by http.Client. It also wraps context.Canceled when the user interrupts a request. So a malformed URL (for example a bad VOLCANO_API_URL) or a cancelled command is reported as error_kind: network, which inflates the network-failure count this field exists to measure. Fix: check Op (skip parse), and exclude context.Canceled, before counting a *url.Error as network.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in 4546ada: a context.Canceled error and a *url.Error with Op == "parse" are no longer counted as network. TestErrorKind covers both.

Comment thread docs/telemetry.md Outdated
known agent names to analytics and reports any other as `other`. See
[agent-detection.md](agent-detection.md).

If you are logged in, the event is linked to your Volcano account. Otherwise it

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Docs say logged-out events link only to a random machine ID, but that ID is also sent while logged in and survives logout

Event.DeviceID is always sent, including on events that carry the bearer token. config.ClearLogin deliberately keeps the telemetry block, so the same device_id survives volcano logout. The server therefore receives (device_id, account) pairs and can tie a machine's logged-out events back to the account that last used it. The sentence "Otherwise it is linked only to the random machine ID" reads as if logged-out events are anonymous. Either say that the machine ID is sent with logged-in events and kept after logout, or have logout generate a new device_id.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in aa93c80: docs/telemetry.md now says every event carries the machine ID, and that it is kept after volcano logout, so logged-out events can be linked to the account last used on that machine. Logout keeps the ID on purpose; regenerating it would be a separate decision.

@tkkhq

tkkhq commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

The AI review at c3f0ade had a P3 it posted only in its summary: the release checks in publish-cli.yml ("Verify uploaded Linux binary") and verify_install in scripts/ci/publish-downloads.py ran the published binary with telemetry on, so each release would send events. Valid; fixed in 1aec98d. Both now set VOLCANO_TELEMETRY_DISABLED=1, and verify_install passes that env to its --version run, which before inherited the runner environment. test_install_check_pins_only_when_asked asserts it.

Comment thread internal/cmd/telemetry/telemetry.go Outdated
terminal, the coding agent that ran it, whether the Volcano plugin is installed
for that agent, the exit code, and the duration. When a command fails, the
event also carries the failure type, the API error status and code, and the
error message with tokens and keys removed. Events are linked to your account

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: volcano telemetry --help and the first-run notice understate how events are linked and what is sent

The help text says events are "linked to your account when you are logged in, and otherwise to a random ID for this machine". The first-run notice (internal/telemetry/telemetry.go printNotice) says only that events are linked to the account when logged in. In the code, build puts DeviceID on every event, logged in or not. ClearLogin keeps that ID across volcano logout, so events sent after logout can be tied to the last account. docs/telemetry.md was updated in aa93c80 to say exactly this, but the in-binary text was not. That text is what the notice points to as "Details", and the PR says it is the disclosure users can reach because older docs caches lack cli/telemetry.md. The help also lists "the selected project ID" with no qualifier, but only volcano cloud … commands send it. Fix: use the docs wording in both places — a machine ID is sent with every event and kept after logout, and the project ID is sent for cloud commands only.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in 76a6cc8. volcano telemetry --help now says every event carries a random machine ID that volcano logout keeps, so events sent after logout can be linked to the account last used on that machine, and that the project ID is sent for volcano cloud commands only. The first-run notice says events are linked to a random machine ID and, when logged in, to the account.

Comment thread docs/telemetry.md Outdated
nothing, so you can turn telemetry off before any event is sent.

The CLI saves that the notice was shown in `~/.volcano/config.json`. If it
can't write there, as in a sandbox with a read-only home directory, it shows no

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: Limit the read-only-home guarantee to uninitialized telemetry state

A sandbox mounting an already initialized home read-only still sends telemetry: Recorder.Reporting enables agent headers, and Recorder.Record skips initialization when notice_shown and device_id already exist. Users relying on this guarantee can therefore transmit command, resource, and error data. Qualify the statement to homes where first-run state has not been saved, and direct users to the explicit opt-out.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in d5cadf9. docs/telemetry.md now limits the guarantee to a read-only home that has never run the CLI, says a home where the notice was already saved keeps sending when mounted read-only, and points to VOLCANO_TELEMETRY_DISABLED=1 for that case.

@tkkhq

tkkhq commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

The AI review at 76a6cc8 had a P3 it posted only in its summary: the release build job's "Smoke-test Linux binary" step in publish-cli.yml still ran --help and --version with telemetry on. Valid; fixed in bbf415c with the same VOLCANO_TELEMETRY_DISABLED: "1" env. Every workflow step that runs the binary now sets it. Separately, the #1937 review flagged that logo@2x.png matched the email rule; the CLI side is fixed in efd87c5.

Comment thread internal/telemetry/fields.go Outdated
jwtPattern = regexp.MustCompile(`eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*`)
keyPattern = regexp.MustCompile(`\b(?:(?:(?:pk|pt|sk|ak)-|vbt_|vpg_)[A-Za-z0-9_-]{16,}|sbp1\.[A-Za-z0-9+/=:]{16,})`)
userPattern = regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9+.-]*://)[^\s/?#]*@`)
emailPattern = regexp.MustCompile(`[A-Za-z0-9._%+-]+@[A-Za-z][A-Za-z0-9-]*(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}`)

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: Redact email addresses with digit-led domains

Requiring a letter immediately after @​ leaves valid addresses such as owner@​163.com unchanged. When volcano cloud durable approvals deny <id> encounters an already-approved approval, ApprovalConflict and DecisionSummary in internal/durable/durable.go include the decider’s email, which telemetry then sends unredacted despite the documented guarantee. Support digit-led domains and handle asset-path false positives separately, or mark email-bearing errors sensitive.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, and a regression from my previous push. Fixed in Kong/volcano-cli 5f726dc / Kong/volcano-hosting 6178202ee: digit-led domains match again (alice@163.com, ops@1password.com, u@123corp.example), and only a first domain label of digits then x (logo@2x.png, icon@3x.webp) is skipped. Both sides have tests for each case and the patterns stay identical.

@tkkhq

tkkhq commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

The AI review at efd87c5 also had the Claude reviewer report the same digit-led-domain email finding in its summary (alice@163.com). Covered by the fix in 5f726dc.

Comment thread docs/telemetry.md
resource names, though they can appear in an error message. A name that looks
like a path or a Volcano token or key is dropped.

The CLI does not send file contents or environment variable values, except in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Docs say no env var values are sent except two, but error messages carry the VOLCANO_API_URL host and path

The doc says the CLI "does not send file contents or environment variable values, except in two cases" (VOLCANO_PROJECT_ID and the agent variables). But errorMessage sends the text of a failed request as is. A Go *url.Error reads like Get "https://api.corp.internal/projects/<id>/functions": dial tcp: lookup api.corp.internal: no such host. Redaction strips only user info and query strings, so a self-hosted or staging VOLCANO_API_URL host and the request path reach analytics on every network failure. Either name this case in the doc, or reduce URLs in error messages to scheme and host (or drop them) before sending.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changing. VOLCANO_API_URL is a developer override for pointing the CLI at a local or staging API; users run against the default host, whose request paths carry only the project ID and resource names that the event already sends.

Comment thread internal/api/version_protocol.go Outdated
// agentReportingOff gates the agent headers (zero value: on). The entry point
// sets it when the user opted out of telemetry, since the API forwards these
// headers to product analytics too.
var agentReportingOff atomic.Bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Agent headers default to on; only main turns them off before the first-run notice

agentReportingOff is an atomic.Bool whose zero value means the X-Volcano-Agent* headers are sent. The promise that nothing is sent before the first-run notice, or after an opt-out, holds only because main() calls api.SetAgentReporting(recorder.Reporting()) first. Today that is the only entry point, so nothing leaks. But any new entry point or in-process caller of internal/api that skips this call would send agent headers without consent. Defaulting to off (an agentReportingOn flag that main sets) would fail closed. The api tests that expect the headers would then need to enable it explicitly.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in 57381c8: the gate is now agentReportingOn, whose zero value sends no agent headers; main turns it on only when telemetry is reporting. New test TestVersionProtocolDoer_SendsNoAgentHeadersUntilReportingIsOn; the header tests turn reporting on explicitly.

Comment thread internal/telemetry/fields.go Outdated
jwtPattern = regexp.MustCompile(`eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*`)
keyPattern = regexp.MustCompile(`\b(?:(?:(?:pk|pt|sk|ak)-|vbt_|vpg_)[A-Za-z0-9_-]{16,}|sbp1\.[A-Za-z0-9+/=:]{16,})`)
userPattern = regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9+.-]*://)[^\s/?#]*@`)
emailPattern = regexp.MustCompile(`[A-Za-z0-9._%+-]+@(?:[A-Za-z][A-Za-z0-9-]*|[0-9]+(?:[A-Za-wyz-][A-Za-z0-9-]*|x[A-Za-z0-9-]+)?)(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}`)

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: The asset-name exception leaves valid email addresses unredacted

The pattern excludes any domain whose first label is digits followed by x, including valid addresses such as alice@​2x.example.com. Attempting to approve an already denied durable approval can include its decider's address through internal/durable.ApprovalConflict and DecisionSummary, so that address reaches telemetry unchanged despite the documented redaction guarantee. Restrict the asset exception to identifiable image filenames, or mark the email sensitive at its source.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in 957fe7b (and Kong/volcano-hosting b2d14e61b): the asset-name carve-out is gone and the email rule is back to its original form, so ops@10x.dev and a@2x.example.com are redacted. An asset name such as logo@2x.png is now redacted too; that costs a file name, while any narrower carve-out leaves some real address unredacted.

}
if inv.Err != nil {
event.ErrorKind = errorKind(inv.Err)
event.ErrorStatus = api.Status(inv.Err)

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: Config validation failures lose their API classification and status

When volcano cloud config deploy receives HTTP 422, internal/api.ApplyProjectConfig returns *api.ProjectConfigValidationError. That type does not wrap *api.Error, so api.Status returns zero and errorKind reports other. These server rejections therefore disappear from API-failure and HTTP-status telemetry. Preserve the 422 status on this error type and recognize it when building the event.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in 81e23ab: api.Status now returns 422 for *api.ProjectConfigValidationError, so the event carries error_kind: api and error_status: 422. No existing caller branches on 422. TestErrorKind covers it.

@tkkhq

tkkhq commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

The AI review at 57381c8 had one P3 it posted only in its summary: the api.Status doc comment did not mention the new 422 case. Fixed in e90ff00.

// plain errors with fixed message prefixes.
func isUsageError(err error) bool {
msg := err.Error()
for _, prefix := range []string{

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: Classify Cobra flag-group validation failures as usage errors

volcano cloud databases restore app without --backup or --to produces at least one of the flags in the group ... is required; supplying both produces an error beginning if any flags in the group. Neither prefix matches here, so errorKind reports these CLI usage failures as other. The same affects storage object visibility commands. Include Cobra's flag-group error prefixes and cover these existing command validations in the classification tests.

Found by gpt-6.1-sol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in eb4ee22: isUsageError now matches cobra's three flag-group errors (if any flags in the group, at least one of the flags in the group). TestErrorKind_CobraFlagGroups runs real cobra commands with MarkFlagsOneRequired, MarkFlagsRequiredTogether, and MarkFlagsMutuallyExclusive, so the test fails if cobra changes the wording.

Comment thread docs/authentication.md Outdated
```

This deletes local credentials but does not revoke the token. Revoke it in the
This removes local credentials and the selected project but does not revoke the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Logout doc omits that function aliases are also removed

volcano logout --help now says logout removes "local credentials, the current project, and function aliases", but the published doc says only "local credentials and the selected project". A reader could take that to mean their saved functions alias mappings survive logout, which config.ClearLogin contradicts. The sentence is also broken across lines oddly ("Revoke it in the" / "Volcano dashboard"). Mention aliases and reflow the paragraph.

Found by claude-opus-5-5

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, fixed in fed48e1: the doc now says logout removes local credentials, the selected project, and function aliases (matching config.ClearLogin), and the paragraph is reflowed.

@tkkhq

tkkhq commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

The AI review at eb4ee22 also had a P3 in its summary only: an older CLI rewriting config.json drops the telemetry block. Not changing: two CLI versions sharing one config is not a supported setup, and VOLCANO_TELEMETRY_DISABLED=1 is unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants