feat: add internal contributorbilling capture middleware for CLI billing [IANDT-238] - #671
feat: add internal contributorbilling capture middleware for CLI billing [IANDT-238]#671brettgurman-snyk wants to merge 18 commits into
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This comment has been minimized.
This comment has been minimized.
| type Capability string | ||
|
|
||
| const ( | ||
| CapabilityOSS Capability = "oss" |
There was a problem hiding this comment.
[question]: shouldnt this be:
CapabilityOS Capability = "os"i.e os not oss
There was a problem hiding this comment.
oss matches pre-existing gaf work from 2024, as well as the pitch doc
not sure if it hugely matters as long as we're consistent
There was a problem hiding this comment.
the capability is called open source so would prefer it be called that personally what does the additional s stand for?
| return projectID.String() | ||
| } | ||
|
|
||
| func parseIaCShareResponse(body []byte) []string { |
There was a problem hiding this comment.
[suggestion]: if my understanding is correct what this is doing is "any value in this object that happens to be a UUID string is a project ID". That's a heuristic tied to today's response shape.
Is it worth adding a comment here mentioning this or even looking for the specific key instead?
There was a problem hiding this comment.
there isnt a stable key name to match, so going with adding a comment
|
overall good work have some non blocking comments. Have you got a jira ticket created for the code path? |
There was a problem hiding this comment.
Do we want to be codeowners of this?
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
This is very invisible to the product teams who own these flows/APIs. How do we plan to avoid silent breakages if their APIs change, they migrate onto new APIs, etc. ? |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Automated verification — PR #671
Ran a 4-lens verification pass (semantic analysis, adversarial review, security scan, code review) against the full branch diff (main...db4f815).
Not a dependency-only change — no go.mod/go.sum changes; this adds substantial new functionality (49 files, ~5,678 lines: internal/contributorbilling/*, pkg/networking/middleware/contributor_capture.go, plus edits to public pkg/ packages). Per current policy, this automation does not auto-approve non-dependency changes, and will never leave the PR in CHANGES_REQUESTED — findings below are informational only and do not block merge through this review.
Critical
- Request body truncated for real outgoing requests.
readRequestBody(pkg/networking/middleware/contributor_capture.go) discards any request body content beyond the 64 KiB capture cap before the real request is forwarded viam.next.RoundTrip(req). AnyPOST /orgs/{org}/tests(code test --report/iac test --report) call with a body over ~64 KiB is corrupted, not just the billing telemetry. Confirmed independently by two review passes. Unlike the response-body path (readResponseBody), which correctly stitches the unread remainder back viastitchedReadCloser, there is no equivalent preservation on the request side, and there's no test coverage for this path. - Process-wide capture-session singleton is never closed by production code.
commandSession(internal/contributorbilling/capture/session.go) is a single package-level instance;CloseCommandSession/OpenCommandSessionare only called from tests in this diff. Once thecontributor_billing_capture_enabledflag is on, the middleware's ownEnsureCaptureSessionForConfigcall opens a session that nothing closes — records accumulate indefinitely in a long-lived host process (IDE/MCP), and concurrent/unrelated billable commands in the same process can have their records merged into one bag (therepoPathof a second command is silently discarded once a session is already active). This also means the middleware is not fully inert independent of IANDT-240, contrary to the PR description's safety framing. readResponseBodysilently truncates on a read error without propagating it. On a partial/failed body read, the function swaps in only the partial bytes with no error signaled to the real caller and without closing the original body — a genuine transport error (TCP reset, TLS failure) can be misread by real product code as a truncated-but-valid response.
Should Fix
- Breaking public API change without a breaking-change marker. Adding
GetCommand() stringto the exportedanalytics.Analyticsinterface (pkg/analytics/analytics.go) breaks any downstream (CLI/IDE/MCP) type that implements this interface directly. None of the branch's commits usefeat!/BREAKING CHANGE:, so semantic-release won't bump the major version despite this being a load-bearing interface change per this repo's ownAGENTS.md. readRequestBody's error path doesn't restore/closereq.Body, so a mid-stream read failure can send the real CreateTest request with a corrupted body.pkg/utils/git/contributors.go: the commit-log scanbreaks on the first commit older than the window, assuming strictly date-ordered traversal — go-git's default order is graph/topological, not author-date, so rebases/cherry-picks can cause a valid in-window commit to be skipped, silently undercounting a contributor for billing.- Contributor email (PII) is logged at Debug level in
internal/contributorbilling/payload.go.
Suggestions
Capability/EntityTypeconstants are duplicated as typed values incaptureand untyped strings ininternal/contributorbilling/constants.go, bridged by a manual switch — a single source of truth would remove the sync risk.SkipReasonMissingCapabilityininternal/contributorbilling/types.goappears to be dead code (never produced).- No test exercises
readRequestBody's truncation path, nor concurrentEnsureCommandSessioncalls with differentrepoPaths — both would have caught the Critical findings above.
Note on two discarded findings
An earlier pass flagged an apparent regression in pkg/auth/oauth2authenticator.go / pkg/auth/authHost.go (OAuth host-validation hardening) and a CircleCI context removal in .circleci/config.yml. I verified these against the actual merge-base diff (git diff $(git merge-base main HEAD) HEAD) and confirmed this PR branch makes zero changes to those files — the apparent diff was an artifact of the branch being behind current main (which has since merged unrelated security hardening), not something introduced by this PR. Excluded from the findings above; will resolve naturally on rebase/merge.
CI status at time of review
Most checks green (unit tests, Windows tests, security scans, secrets scan, Snyk code/license/security checks); CircleCI Build and test_and_release were still pending.
Verdict: no action taken automatically (not a dependency-only change). Findings are informational — this review is posted as a comment, not a change request.
Sent by Cursor Automation: Automatic PR verification
Co-authored-by: Cursor <cursoragent@cursor.com>
The multi-item timeout test can run overlapping HTTP handlers after the client times out; use atomic.Int32 so go test -race passes in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
…[IANDT-237] Address AGENTS.md audit findings: fix WaitWithTimeout race, absolutize repo paths at emit time, filter git contributors by author date, preserve ingest URL path prefixes, optional Capability, ApplyFromConfiguration, and WaitBudget for multi-item teardown. Co-authored-by: Cursor <cursoragent@cursor.com>
Relocate contributor billing and the entitlements ingest client under internal/contributorbilling/ per packaging review. Add pkg/clibilling as a thin emit/wait facade; capture and middleware remain IANDT-238. Co-authored-by: Cursor <cursoragent@cursor.com>
Fix import ordering so make lint passes in CI Build job. Co-authored-by: Cursor <cursoragent@cursor.com>
…-237] Before each ingest POST, collapse duplicate emails in the contributor list using case-sensitive matching and keep the latest commit date. Co-authored-by: Cursor <cursoragent@cursor.com>
Send contributors_entity_type and bare UUID contributors_entity_id, and remove the temporary pkg/clibilling public facade from the emit package PR. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
HTTP capture middleware with command session, FFS gating, URL-first lazy session open, analytics command resolver, and EmitFromCapture bridge. Co-authored-by: Cursor <cursoragent@cursor.com>
db4f815 to
148d2e3
Compare
PR Reviewer Guide 🔍
|
8ff205b to
2d4a94b
Compare
There was a problem hiding this comment.
| iter, err := repo.Log(&git.LogOptions{}) | |
| if err != nil { | |
| return nil, fmt.Errorf("read log: %w", err) | |
| } | |
| defer iter.Close() | |
| authors := make(map[string]time.Time) | |
| iter.ForEach(func(c *object.Commit) error{ | |
| when, email := c.Author.When, c.Author.Email | |
| if when.Before(since) { | |
| return nil | |
| } | |
| if prev, ok := authors[email]; ok && when.Before(prev) { | |
| return nil | |
| } | |
| authors[email] = when | |
| return nil | |
| }) |
There was a problem hiding this comment.
^ iterate through all commits on all branches, using authored date as cutoff and latest commit date value, and don't limit it to "now", i.e. count commits that are logged as being in the future
643ddf2 to
7b6cebb
Compare


Summary
Adds GAF HTTP capture middleware and
EmitFromCapturefor CLI Active Contributor billing. Middleware records billing entities from successful product API responses into a command-scoped capture session. At CLI teardown (IANDT-240), captured records become IANDT-237 ingest POSTs.Depends on IANDT-237 (
internal/contributorbillingemit/ingest + sharedBillingEmitter). Does not go live end-to-end until IANDT-240 lands (#681 + CLI #7067):FinishCommandat tearDown closes the session and emits.Lazy-open capture (current design)
The capture session is not opened at CLI startup. It opens lazily on the first billable product API request:
Not in this PR:
BeginCommand,WithContributorBillingCapture(), or any cliv2 billing call at command start.What it does
Capture (
internal/contributorbilling/capture)Process-scoped command session (
EnsureCommandSession/CloseCommandSession/ActiveCapture)gate.go—IsBillableCommand,CaptureEnabledForBillableHTTP,EnsureCaptureSessionForConfig,CommandNameFromRawArgsParsers per supported CLI command / API flow:
snyk monitor(OSS)PUT /v1/monitor/...orPUT /v1/monitor-dependencies/...urifor project public IDosssnyk iac test --report(IaC legacy TS path)POST /v1/iac-cli-share-resultsiacsnyk code test --report(Code native--report)POST /orgs/{org}/testswithpublish_report: trueand SAST scan configGET /orgs/{org}/tests/{test}orGET .../components→ project IDcodeNative OS
--report(Test API, non-legacy monitor path)publish_report: truewithout SAST configossGated on
contributor_billing_capture_enabled(FFSenable-entity-contributors-publish) and billable commandsMiddleware (
ContributorCaptureMiddleware)EnsureCaptureSessionForConfigon first in-scope billable request when flag + command are eligiblenetworking.SetActiveCommandResolver+analytics.GetCommand()— active CLI command from analytics, withRAW_CMD_ARGSfallbackEmit bridge
EmitFromCapture— groups deduped capture records by capability and callsEmitContributorBillingwith git contributor collection at emit timeWhat this PR does not include
pkg/clibillinghost helpers (FinishCommand, post-invoke failure cleanup) — IANDT-240 (feat: add opt-in capture engine orchestration for CLI billing [IANDT-240] #681)FinishCommandowns close + emit)Safe to merge without billing going live: middleware only captures when the flag is on and a billable command is active; emit does not run until IANDT-240
FinishCommandat tearDown.Test plan
internal/contributorbilling/capture,contributor_capture,from_capture)-raceon capture/middleware)Merge order
IANDT-237 (#664) → IANDT-238 (#671) → IANDT-240 (GAF #681 + CLI #7067) → E2E validation
Note
Medium Risk
Adds billing HTTP emits and global network middleware that parses product API traffic; failures are fire-and-forget but mis-capture or flag rollout could post wrong or unexpected contributor data. Scope is feature-flagged and ingest at tearDown remains out of this PR.
Overview
Introduces CLI Active Contributor billing in GAF: HTTP middleware records project/target entities from successful in-scope API calls into a lazy-opened command capture session, and
EmitFromCapture/EmitContributorBillingfire-and-forget POSTs to entitlements-service ingest (with optional 90-day git log contributor collection). Gated bycontributor_billing_capture_enabled(FFSenable-entity-contributors-publish) and billable commands (monitor,iac test --report,code test --report).Capture parses Registry monitor/IaC share responses and Test API create/follow-up flows (OSS/Code/IaC capabilities). ContributorCaptureMiddleware sits on the default round tripper with URL-first gating to avoid FFS/auth recursion, 64 KiB body limits, and safe body replay for downstream consumers.
Also adds
workflow.PostInvokeHook/WithPostInvokeHooks,analytics.GetCommand(), andSetActiveCommandResolverfor command-aware capture. End-to-end billing at CLI teardown is not wired here (depends on IANDT-240FinishCommand).Reviewed by Cursor Bugbot for commit 148d2e3. Bugbot is set up for automated code reviews on this repo. Configure here.