Skip to content

fix(usage-limits): scope the Antigravity process scan to the current user - #138

Merged
pitimon merged 2 commits into
mainfrom
fix/132-scope-antigravity-ps-scan
Aug 2, 2026
Merged

fix(usage-limits): scope the Antigravity process scan to the current user#138
pitimon merged 2 commits into
mainfrom
fix/132-scope-antigravity-ps-scan

Conversation

@pitimon

@pitimon pitimon commented Jul 30, 2026

Copy link
Copy Markdown
Owner

PR Goal (one sentence)

Scope the Antigravity ps scan to the current user, and settle the severity question #132 left open.

Why

detectAntigravityProcess ran /bin/ps -ax — every account on the box — and attached to whichever Antigravity language server matched first. #129 had already narrowed the other ps call site and deliberately left this one alone, because nothing in that PR exercised this path.

Step 1 of the issue: what can actually escape

The CSRF token and the pid never reach an HTTP response. processInfo is read field by field and is never spread into a returned object: pidlistAntigravityPorts (usage-limits.js:1815), csrfToken → the X-Codeium-Csrf-Token request header (:1616), extensionPort → fallback port choice (:1840). All four return shapes of fetchAntigravityLimits (:1795-1863) carry neither, and every error string on that path is a literal — the token travels as a header, so no URL an error could echo contains it.

What the token fetches does reach the response. normalizeAntigravityResponse (:1763-1775) returns account_email and account_plan; finalize (:1804-1812) spreads them into the result; getUsageLimits (:1976) collects it; GET /functions/tokentracker-usage-limits serves it. And writeAntigravityLimitsCache (:1470-1483) writes account_email to ~/.tokentracker/tracker/usage-limits-cache.json, where the not-configured branch (:1798) keeps serving it after the other user's process exits.

Severity: not credential exposure — cross-account PII display and disk persistence, on multi-user hosts only. Single-user machines were never affected, which is exactly why observed output could not have caught this.

Scope

  • CLI (src/)
  • Dashboard (dashboard/)
  • macOS app (TokenTrackerBar/)
  • Windows app (TokenTrackerWin/)
  • Docs / CI / config

Checklist

  • npm run ci:local passes (exit 0, 1007 CLI tests, 41 dashboard test files, all validators, OpenWiki 0 findings)
  • New dashboard/user-facing strings go through copy.csv — N/A, no user-facing strings
  • Commits follow conventional style
  • PR description explains why, not just what
  • Version bump files in lockstep — not release-bound. Main is still 0.39.44 and release: re-add the 0.40.0 version bump when it should actually ship (dropped from #129 on purpose) #135 tracks re-adding the bump when something should actually ship; adding it here would collide with that.
  • npm publish state — not published

What changed

detectAntigravityProcess now uses the PS_BINARY / PS_ARGS already exported by src/lib/process-list.js (-x, own user) instead of its own inline ["-ax", ...]. Sharing the constant is the point rather than a convenience: #129 fixed one of these two scans, and two scans that must both stay own-user should not be able to drift apart independently.

The comment above the function records the trace above, so the next reader does not have to redo it to know whether the token escapes.

Codex Context

  • Delta since last Codex review: none — this is a follow-up to the QA gate on feat(doctor): notice when Claude usage cannot be recorded at all #129 that produced the issue.
  • Intended behavior / invariants: the scan sees only processes owned by the user running TokenTracker; no -a in the argv, ever; both ps call sites read the same constant.
  • Edge cases covered: literal argv asserted from an injected commandRunner; the existing detectAntigravityProcess parse test still passes unchanged, so the behaviour on matching output is untouched.
  • Tests run (command + result): node --test test/usage-limits.test.js → 46/46. npm run ci:local → exit 0, 1007 CLI tests.
  • Known gaps / out of scope: an Antigravity deliberately run under a different account (root, a service user) is now invisible to the quota fetch and reports as not-configured. That is the intended trade and matches the reasoning in feat(doctor): notice when Claude usage cannot be recorded at all #129. No audit of other providers' detection paths for the same pattern — only these two ps call sites exist today, per rg 'PS_BINARY|/bin/ps' src/. (Note the narrower rg '/bin/ps' src/ is no longer sufficient: after this change usage-limits.js imports the constant instead of spelling the path, so that grep misses the very call site this PR touches, and would miss any future caller too.) The cache artifact an affected host may already hold is usage-limits: a cache written before #138 may still hold another account's email #139, deliberately not folded in here.

Risk Layer Trigger (if any)

  • Public exposure / share links / unauthenticated access
  • Auth/session/token handling
  • Cross-endpoint invariants or shared logic
  • External gateway / environment constraints

Risk Layer Addendum

Rules / Invariants

  • Neither ps call site may be invoked with an all-users flag. Both now read PS_ARGS from one module, so there is a single place to get this wrong.
  • /functions/tokentracker-usage-limits answers unauthenticated loopback GETs, so anything the fetch obtains is effectively served; the scope of the scan is therefore the control, not the shape of the response.

Boundary Matrix (must list at least 3)

# Boundary Input Allowed out Enforced by
1 other users' processes → this scan every process on the host own-user processes only PS_ARGS = ["-x", "-o", "pid=,command="]; test/usage-limits.test.js "scans only the current user's processes" asserts the literal argv and the absence of -a
2 matched process's command line → HTTP response argv containing --csrf_token and --extension_server_port nothing — token and pid are consumed internally traced above; processInfo is never spread into a return value at any of the four fetchAntigravityLimits exits
3 another account's quota API → served payload + on-disk cache account_email, account_plan, quota windows only the local user's, because only the local user's process can now be matched the scan scope in row 1 — this is the boundary the fix actually closes
4 the two ps call sites → each other independent edits they cannot diverge both import PS_ARGS from src/lib/process-list.js; test/process-list.test.js pins the same value

Evidence (tests or repro)

  • The new test was verified as a real guard, not a restatement: restoring -ax in process-list.js makes it fail, restoring -x makes it pass. A live ps run on a single-user machine is identical either way, so nothing observational could substitute.
  • The existing detects antigravity process info from ps output test passes unchanged — the injected runner ignores argv, so parsing behaviour is provably untouched by this change.
  • -x checked on Linux, not assumed. macOS ps is BSD, where -x plainly means "own user, tty restriction lifted". Linux ps is procps and parses dash-prefixed options as UNIX-style, where -x is not an option — and isProcessListSupported returns true for every platform but win32, so Linux runs this argv. On a real Debian 12 / procps-ng 4.0.2 host: ps -x -o pid=,command= exits 0 and reports one user across 22 lines, while -ax on the same box reports seven users across 39. procps accepts it as the BSD x. Had it not, every Linux host would have fallen into process_list_failed — a permanent non-advisory warn, pinning degraded for a whole platform, which is the failure fix(doctor): count only non-advisory warns toward degraded #136 was written to remove. This also confirms feat(doctor): notice when Claude usage cannot be recorded at all #129's already-merged argv.
  • PS_ARGS is frozen. The claim that the two scans "cannot drift apart" was true of editorial drift only; sharing an unfrozen array does not stop PS_ARGS.push("-a") at runtime. The test asserts the mutation throws, not merely that the value is currently right.

Public Exposure Checklist

  • Public access rules defined — unchanged; loopback-only unauthenticated read, as before
  • Exposed fields explicitly listed and verified — unchanged shape; what changes is whose data can populate it
  • Avatar/image policy defined — N/A
  • Regression tests cover invalid link and auth fallback — N/A; the argv guard is the relevant control
  • Mark N/A if no public exposure

Regression Test Gate

Most likely regression surface

Antigravity quota detection itself: a narrower scan finds fewer processes, so a legitimate setup could silently become "not configured".

Verification method (choose at least one)

  • node --test test/usage-limits.test.js → 46/46, including the pre-existing detection and cache-persistence tests
  • Full npm run ci:local → exit 0

Uncovered scope

  • Not exercised against a real running Antigravity on this machine — none is installed, so the detection path is covered by injected fixtures only, as it was before this change.
  • The multi-user behaviour of -x vs -ax is now demonstrated on a real Linux host (see Evidence), but on a host whose second "users" are service accounts rather than a second human running Antigravity. The argv semantics are what that check establishes; an end-to-end cross-account fetch has not been staged.
  • Anyone who ran the old code on a shared host may still have another user's account_email in ~/.tokentracker/tracker/usage-limits-cache.json, and the not-configured branch will serve it precisely because detection now correctly finds nothing. This PR does not purge it — tracked as usage-limits: a cache written before #138 may still hold another account's email #139, which weighs the three remediation options rather than picking one here.

Closes #132.

…user

`detectAntigravityProcess` ran `/bin/ps -ax`, which walks every account on the
box, and attached to whichever Antigravity language server matched first.

Tracing what that could expose settles the question #132 left open: the CSRF
token and the pid never reach an HTTP response. `processInfo` is read field by
field and is never spread into a returned object — the pid goes to
`listAntigravityPorts`, the token becomes a request header, and all four return
shapes of `fetchAntigravityLimits` carry neither.

What the token *fetches* does reach the response. `normalizeAntigravityResponse`
returns `account_email` and `account_plan`, `finalize` spreads them into the
result, and `getUsageLimits` serves that at
`/functions/tokentracker-usage-limits`. On a shared host this displayed another
person's email, plan and quota as the local user's own, and
`writeAntigravityLimitsCache` persisted the address to disk, where the
not-configured branch kept serving it after their process exited. So: not
credential exposure, but cross-account PII — on multi-user hosts only, which is
why no observed output could have caught it.

The scan now uses the PS_BINARY / PS_ARGS already exported by process-list.js
rather than its own inline argv. Sharing the constant is the point: #129 fixed
the other scan, and two scans that must both stay own-user should not be able to
drift apart.

The regression test captures the literal argv from an injected commandRunner,
because a real `ps` run on a single-user machine returns identical lines either
way. Verified as a guard rather than a restatement: restoring `-ax` fails it.

Anyone who has run this on a shared host may have another user's account_email
cached in ~/.tokentracker/tracker/usage-limits-cache.json. It is a cache;
deleting the file is the whole remedy.

Closes #132
Two gaps a review found in the previous commit.

The `-x` scoping was only ever demonstrated on macOS, where `ps` is BSD and `-x`
plainly means "own user, tty restriction lifted". Linux `ps` is procps and parses
dash-prefixed options as UNIX-style, where `-x` is not an option — and
`isProcessListSupported` returns true for every platform except win32, so Linux
runs this argv. Had procps rejected it, `listProcessLines` would have returned
`process_list_failed` on every Linux host: a permanent non-advisory warn, pinning
`degraded` for a whole platform, which is the failure #136 exists to remove.

Checked on a real Debian 12 / procps-ng 4.0.2 host rather than reasoned from the
manual: `ps -x -o pid=,command=` exits 0 and reports one user across 22 lines,
while `-ax` on the same box reports seven users across 39. procps accepts it as
the BSD `x`, so the argv is correct on both supported platforms and #129's
shipped code is correct too. Recorded in the comment so nobody has to re-derive
it from a manpage.

The array is now frozen. Two modules share it, and the claim written in the
previous commit — that the two scans "cannot drift apart" — was only true of
editorial drift. Importing one constant does not stop `PS_ARGS.push("-a")`;
freezing does, and the test asserts the mutation throws rather than asserting
the flag alone.

Refs #132
@pitimon
pitimon merged commit 1ac9487 into main Aug 2, 2026
1 check passed
@pitimon
pitimon deleted the fix/132-scope-antigravity-ps-scan branch August 2, 2026 11:04
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.

usage-limits: machine-wide ps -ax lifts a CSRF token from whatever Antigravity process matches first

1 participant