Skip to content

fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) - #165

Open
patlux wants to merge 9 commits into
mainfrom
fix-review-findings
Open

patlux wants to merge 9 commits into
mainfrom
fix-review-findings

Conversation

@patlux

@patlux patlux commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes from a full review of the extension, one commit per finding (most severe first).

Commit Finding Change
fix(core): report stalled-body timeouts … A per-attempt timeoutMs that fired while the response body was stalled surfaced as AbortError and was classified as a user cancellation: reason: "aborted", "Request aborted", never retried. The existing retry test only covered the fetch phase (the mock never flushes headers for events: []). Classify the attempt timeout before the user-abort check; retry when no visible content was emitted, otherwise report timed out after Nms. Regression tests for both paths.
fix(auth): read ambient credentials through one reader … src/converters.ts carried a second copy of the auth-file reader with a different precedence (~/.omp before ~/.pi) and without the "command-code": "user_…" string form, so the generate fallback could stream with a different key than the Provider API path and the quota commands. Both readers used homedir() regardless of PI_CODING_AGENT_DIR. One reader (src/api-key.ts), re-exported for the generate path; README order everywhere; pi auth file read from PI_CODING_AGENT_DIR so isolated profiles never fall back to another profile. Test asserts both entry points are the same function.
fix(oauth): validate pasted keys against the configured API base … validateApiKey always called production and had no timeout: wrong host under COMMANDCODE_API_BASE, and a stalled whoami hung /login (quota got a deadline in #152, this path did not). Derive the alpha base like index.ts; 15 s deadline that also covers fetch implementations ignoring the signal.
fix(oauth): require the state token before honoring callback errors The callback server settled a pending login on any {error} payload before checking state, so any local client — or any page on the trusted http://localhost:3000 dev origin — could cancel a browser login with an arbitrary message. State is validated first; error payloads without it get 403 and the server keeps waiting. http://localhost:3000 removed from the allow list.
fix(core): make taste learning opt-in on the generate transport x-taste-learning: true was sent unconditionally (also under CMD_ZDR=1) and undocumented. The official CLI sends its tasteLearning setting; this extension never reads the learned profile back. false by default, COMMANDCODE_TASTE_LEARNING=1 opts in, always false under ZDR. README documents the working dir / project slug / session id the generate request carries.
fix(core): re-probe the transport on /commandcode-refresh transport.reset() was dead code; an account upgraded from Go stayed on the generate fallback until key change or restart. Wired into the refresh command.
fix(cost): keep context tiers when applying peak rates Peak table dropped tiers on the Provider API path (latent; no time-priced model has tiers today). Keep and double tiers.
fix(core): scope the stream warning to the latest Responses request /commandcode-status stream warning was set once and never cleared. Reset per Responses request.
refactor(runtime): reuse the shared credential redactor for diagnostics Two redaction implementations with different pattern sets. Runtime strips URL queries, then delegates to redactCommandCodeErrorText.

Behavior changes to be aware of

  • x-taste-learning now defaults to false on the generate transport (opt in with COMMANDCODE_TASTE_LEARNING=1).
  • Browser-login error callbacks must carry the state token; otherwise the server keeps waiting and the normal timeout/paste fallback applies.
  • http://localhost:3000 is no longer an accepted callback origin.

Deliberately left out

  • index.ts is not covered by npm run typecheck (tsconfig.json include omits it, and tests/test-package-manifest.ts intentionally forbids pi packages as devDependencies). Checking it against the real pi 1.3.0 types yields 18 errors from structural differences between src/types.ts (*Like types) and pi-ai (JsonObject tool arguments, AssistantMessageEventStream class vs. the *Like interface, SimpleStreamOptions, OAuthCredentials index signature, readonly cost tiers). Aligning those is a separate type-alignment task with OMP-compat trade-offs; the real-host runs in CI remain the guard for now.
  • isRecord is duplicated across seven modules; left as is to keep this PR reviewable (no behavior impact).

Verification

  • npm run typecheck, npm run format:check, git diff --check: clean.
  • npm test: all suites green, including tests/test-pi-local.mjs against a real pi 1.3.0 binary ([pi-local] PASS). tests/test-omp-compat.mjs skipped locally (no omp); runs in CI.
  • Each new regression test was confirmed red on the pre-fix code (3 failures in test-retry.ts, 2 in test-oauth.ts) and green after.
  • pi --no-extensions -e ./index.ts --list-models commandcode lists the catalog.

… content

The per-attempt timer aborts the attempt controller, so a body that stalls
after the headers surfaced as an AbortError and was classified as a user
cancellation: the host saw "Request aborted" and never retried. Classify
it as a timeout before the user-abort check so it is retried when no
content was emitted and reported as a timeout otherwise.
…orts

converters.ts carried a second copy of the auth-file reader with a
different precedence (~/.omp before ~/.pi) and without the string form of
"command-code", so the generate fallback could stream with a different
key than the Provider API path and the quota commands. Re-export the
shared reader and honor PI_CODING_AGENT_DIR for the pi auth file so an
isolated profile does not fall back to another profile's credentials.
… a deadline

validateApiKey always called the production whoami endpoint and had no
timeout, so /login validated against the wrong host under
COMMANDCODE_API_BASE and hung on a stalled response. Derive the alpha base
the same way index.ts does and race the request against a 15s deadline
that also covers fetch implementations ignoring the abort signal.
The callback server settled the pending login on any error payload before
checking the state token, so a local client or a page on the trusted
http://localhost:3000 development origin could cancel a browser login with
an arbitrary message. Validate the state first and drop the development
origin from the allow list.
The generate request sent x-taste-learning: true unconditionally, even
with CMD_ZDR=1, mirroring the CLI's default although this extension never
reads the learned profile back. Send false unless
COMMANDCODE_TASTE_LEARNING=1 is set and always false under ZDR, and
document the working directory, project slug, and session id the request
carries.
The router's reset() existed but was never wired, so an account upgraded
from Go stayed on the generate fallback until the key changed or the host
restarted. Reset the probe from the user's explicit refresh command.
commandCodeCostRatesAt rebuilt the table from the four base rates during
DeepSeek V4 peak windows, dropping tiers on the Provider API path. Keep
the full table and double tiers alongside the base rates.
The /commandcode-status stream warning was set once and never cleared, so
a single malformed reasoning event stayed visible until restart.
runtime.ts kept its own, narrower redaction patterns next to
redactCommandCodeErrorText. Strip URL queries as before, then delegate to
the shared redactor so both surfaces agree.
@patlux patlux self-assigned this Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

Copy link
Copy Markdown
Contributor

Memory benchmark

Compared base 3e43a7c with PR head 8a2dc73 on the same GitHub-hosted darwin runner. Lower values are better.

Metric Base PR PR − Base Change
Stable RSS 120.4 MiB ± 0.9 MiB 120.9 MiB ± 0.7 MiB +0.9 MiB ± 1.3 MiB +0.8%
Physical footprint 67.0 MiB ± 0.1 MiB 66.9 MiB ± 0.5 MiB -0.1 MiB ± 0.6 MiB -0.2%
Physical peak 78.0 MiB ± 1.0 MiB 78.5 MiB ± 0.7 MiB +1.3 MiB ± 1.7 MiB +1.7%

Interpretation: No metric shows a clear Base-to-PR difference beyond its measured run-to-run variation.

Extension overhead above pi baseline

Metric Base overhead PR overhead Difference
Stable RSS +6.9 MiB ± 1.1 MiB +7.8 MiB ± 0.9 MiB +0.9 MiB ± 1.3 MiB
Physical footprint +4.4 MiB ± 0.4 MiB +4.3 MiB ± 0.2 MiB -0.1 MiB ± 0.6 MiB
Physical peak +5.3 MiB ± 1.4 MiB +6.4 MiB ± 1.3 MiB +1.3 MiB ± 1.7 MiB

Values are medians of 6 alternating, paired runs. The value after ± is the median absolute deviation (MAD). Each process was sampled 12 times after a 2500 ms warm-up.

Environment: pi 0.84.4, Bun 1.4.0, darwin arm64. RSS comes from ps; physical footprint and peak come from macOS footprint.

This is a comparative signal, not a pass/fail threshold. GitHub-hosted runner noise can affect absolute values.

@karaaslanz karaaslanz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent review requested via GitHub notifications. I targeted the high-impact OAuth callback-state fix at exact PR head 8a2dc7310d275665baa96c0a4c2fb14547ed2d33, not all 17 modified files.

I fetched and pinned the actual src/auth-server.ts source from base 3e43a7c787d3e6263a98fafa7b962de453f39e08 (blob 9e76149a0acd5dc7fa86cc992ff1e06fc98eb19c) and PR head (blob 284cf92ffe73cb269f3a470b4f3179fb28a26e8d). With Node 24 TypeScript stripping, I executed the actual startAuthServer() implementation from both commits on ephemeral 127.0.0.1 ports. Only synthetic credentials and state tokens were used; no actual login, paid API, or user secrets.

7/7 pinned-source behavior assertions PASS:

  • Base responds HTTP 200 to an attacker-like { error: 'access_denied' } callback without the required state and settles the pending login with the forged cancellation.
  • PR responds HTTP 403 to a missing state and to a mismatched state, without closing the pending login server.
  • PR rejects Origin: http://localhost:3000 with HTTP 403 even when a state is present.
  • PR accepts a valid state-matched callback with synthetic user/key fields (HTTP 200 and the expected resolved result).
  • Both versions still honor a legitimate state-matched cancellation error with HTTP 200 and a rejected login promise.

The test script is stored read-only to upstream as pi-165-oauth-state-callback-base-vs-pr-probe.cjs (SHA-256 e07d9dc90db95b6119299b356985d272d4633849d88f92e45782ac952e691f5c), and the pinned source harness completed without repository edits. Head GitHub CI, Memory benchmark and catalog sync checks report success.

Scope/merge note: These are real isolated loopback HTTP behaviors, not a full Pi/OMP authentication E2E test or review of all nine fix groups. This PR still contains fixes for the internal Generate transport while separately requested breaking PR #164 removes Generate from the registered provider. Please account for that relationship when ordering/rebasing the branches, so security fixes not tied to Generate survive and redundant Generate-only work does not complicate the breaking release. I found no issue in the specific state-validation paths tested.

@pierreraby pierreraby left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — head 8a2dc731 (9 commits, base 3e43a7c)

Verified independently at this exact head in an isolated worktree (npm ci --ignore-scripts, no pi config touched): npm run typecheck, npm run format:check and git diff --check clean, full npm test green including the real-host suites ([pi-local] PASS against pi 1.3.0, [omp-compat] PASS). GitHub CI on this head is green as well (14 success, 1 skipped). I also re-ran the new regression tests against the pre-fix src/ (head tests, base sources): test-retry.ts 3 red, test-stream.ts 2 red, test-cost.ts 1 red — the timeout reclassification, the taste-learning default and the peak-tier fix are genuinely covered.

The fixes are correct and well placed:

  • a918138 (security): the state token is checked before every settle path, including error payloads and before the field validation, and every non-settling path (403, malformed body, oversized body, disallowed origin, OPTIONS, 404/405) was checked — I found no remaining unauthenticated settle, and the 403 body echoes nothing. http://localhost:3000 is out of the allowlist and the 120 s timeout → paste fallback is intact.
  • cef7402: the per-attempt timer aborts only attemptController, so the new controller.signal.aborted line cannot misclassify a genuine user abort; the retry stays gated on output.content.length === 0, and every stream-visible event (text, reasoning delta, tool call) also lands in output.content, so a retry cannot duplicate visible output.
  • 35a774b: exactly one ambient reader is left (src/api-key.ts, re-exported through converters.ts/core.ts), with no missed call sites, the "command-code" string form and PI_CODING_AGENT_DIR are covered by tests, and the precedence change on the generate path is documented.
  • 8a2dc73: the shared redactor is a strict superset of the inline patterns it replaces, and the URL query stripping is preserved.
  • 188a344, c6c8842, 0025369: minimal and correct; the ?/?? base derivation matches index.ts and the re-probe uses the same router instance.

Non-blocking notes, in the order I would address them:

  1. Error-callback contract (worth one live check before release). state is now required to honor an error payload. The base code only proves the Studio echoes state on success callbacks. If the Studio omits it on a user-denied or server-error callback, a cancelled browser login no longer settles immediately and the user falls through the 120 s timeout to the paste prompt — a safe fallback, but degraded. A real browser-login cancellation against production would settle it.
  2. CHANGELOG over-claims profile isolation. Only the pi slot is redirected by PI_CODING_AGENT_DIR; ~/.commandcode/auth.json and ~/.omp/agent/auth.json still resolve from homedir(), so a sandbox that sets PI_CODING_AGENT_DIR without HOME can still pick up another profile's .omp/.commandcode keys. "never falls back to another profile's pi credentials" would be accurate, and the test only proves the pi slot.
  3. README over-states the warning reset. streamWarning is cleared at the start of the next Responses request (index.ts), not "once a later request streams cleanly"; a later clean generate request leaves it up, and two concurrent Responses requests share one slot.
  4. PR description verification counts. tests/test-oauth.ts gained six tests: one is a preservation guard that is green on base, two hang on base (no deadline) rather than "failing", so "2 in test-oauth.ts" is not reproducible as written (the head test file also cannot import validationApiBase from the base module). The new tests in test-api-key.ts, test-cost.ts, test-runtime.ts, test-stream.ts are not mentioned.
  5. x-taste-learning under ZDR. ...options?.headers is spread after the taste header, so a caller-supplied x-taste-learning: "true" still wins under CMD_ZDR=1, which contradicts "always false under CMD_ZDR=1" in the README and CHANGELOG. Either set the header after the spread / drop caller overrides under ZDR, or qualify the docs.
  6. index.ts is the only touched file without a test (the warning reset and the resetTransport wiring); it is also outside npm run typecheck by design, as the PR honestly discloses. Real-host CI is the only guard today.
  7. validationApiBase uses || where index.ts uses ??: COMMANDCODE_API_BASE="" sends /login key validation to production while the chat transports use a relative base.
  8. Hardening nits: state !== options.expectedState is not a constant-time comparison (impractical over localhost in the 120 s window), and an undefined expectedState skips the check entirely — production always sets it, but failing closed would be more defensive.
  9. Pre-existing, surfaced by the wiring: a request that observed upgrade_required before /commandcode-refresh can re-stick generate if its assignment lands after the reset (the apiKey === requestApiKey guard holds again); and attemptTimedOut can mask a genuine stream-error message that arrives at the timeout boundary. Both cosmetic.

Approving: the changes do what their commit messages claim, the security fix is correctly placed, and no P0/P1 code defect was found. The notes above are documentation accuracy and hardening follow-ups; item 1 is worth a live browser check before the next release.

Reviewer context: independent read-only review plus a full local run at the exact head; struck at 8a2dc7310d275665baa96c0a4c2fb14547ed2d33.

@pierreraby
pierreraby requested a review from karaaslanz October 16, 2026 12:53
@pierreraby

Copy link
Copy Markdown
Collaborator

Hi @patlux — with #157 and #167 now merged, the remaining stack (#158, #159, #162, #163, #164) is waiting on your call, and I'd rather ask than guess:

  1. Your review notes vs fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) #165. You listed "before merging" conditions inline on several PRs (fail-closed handling for the corrupt optional file and the compat merge on feat(models): add persistent per-model endpoint overrides #158; the Pi pin version on fix(core): preserve OMP reasoning and add host protocol contracts #162; three items on feat(core)!: require documented Provider API access #164 plus the intended version). Does fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) #165 ("address review findings") resolve those to your satisfaction, or do the branches still need their own follow-ups?
  2. Merge order. Ömer's note on fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) #165 points out the relationship with feat(core)!: require documented Provider API access #164: fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) #165 still contains Generate-transport fixes while feat(core)!: require documented Provider API access #164 removes Generate from the registered provider. Should feat(core)!: require documented Provider API access #164 land first and fix: address review findings (stream timeouts, credential reader, login hardening, taste learning) #165 be re-scoped/rebased so the security fixes survive without redundant Generate-only work, or do you prefer the other way around? Also feat(core): add safe diagnostics and unknown-price warnings #159's CI never ran its correctness gates (it targets model-endpoint-overrides), so it needs a re-push once feat(models): add persistent per-model endpoint overrides #158 lands.
  3. Version for feat(core)!: require documented Provider API access #164. I'll record 0.8.0 as the intended release for the breaking change so the release PR doesn't ship it as 0.7.9 — shout if you had something else in mind.

One heads-up unrelated to the stack: the check job is red on every PR right now for environmental reasons — command-code@1.84.0 was published after #167's CI ran (version-string drift only, the 1.83.0→1.84.0 model metadata is byte-identical) and reflection/beam:free is newly advertised by the API without a local MODEL_COSTS entry (its official deal is 100% free). The daily sync job will pick up 1.84.0; the beam price entry is a one-liner if you want me to send a PR for it.

This branch has not been deployed

No deployments
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