Skip to content

fix(mcp-server): honour the API key's nonce mode in REST and SDK requests (PREDICT-11349) - #76

Open
SohumDesai27 wants to merge 3 commits into
mainfrom
predict-11349-support-per-key-gemini-api-nonce-modes-in-mcp
Open

SohumDesai27 wants to merge 3 commits into
mainfrom
predict-11349-support-per-key-gemini-api-nonce-modes-in-mcp

Conversation

@SohumDesai27

Copy link
Copy Markdown
Collaborator

Linear Board: https://linear.app/gemini/issue/PREDICT-11349/support-per-key-gemini-api-nonce-modes-in-mcp

What's Included

Per-key nonce mode setting: GEMINI_API_NONCE_MODE. incremental (default, unchanged PR #75 behaviour) or time-based. Any other value fails startup. The mode is never inferred from the base URL.

Legacy REST signs with the configured unit. Incremental keeps the shared millisecond sequence. Time-based sends epoch seconds with no state, so concurrent requests may share a second, which the server accepts.

SDK auth follows the same mode on every base URL. Previously standard hosts were hardcoded to seconds and custom hosts to milliseconds. Incremental routes through the shared sequence, so SDK and legacy requests never interleave out of order. Time-based uses the SDK's native time-based mode.

WebSocket auth is unchanged. It always uses epoch seconds, as before.

Operator hint on nonce rejection. The tool layer appends the mode to set when the server message identifies a mismatch. A shared incremental key also gets a warning that its sequence is per process.

Alerts daemon and docs. GEMINI_API_NONCE_MODE is added to the daemon's env whitelist so an installed daemon keeps the mode. The README documents the variable and adds a time-based claude mcp add example.

Not in this PR: the onboarding-generated MCP setup (web/ in the gemini monorepo) does not yet emit GEMINI_API_NONCE_MODE=time-based for keys created with timeBasedNonce: true. That change is tracked in a separate web ticket. Until it ships, Agentic onboarding keys need the variable set by hand.

Testing

  • npm test: 368/368 passing (348 existing + 20 new)
  • npm run typecheck clean
  • npm run build clean
  • Mutation check: forcing the old unit behaviour in http.ts and sdk.ts fails 5 of the new tests, and the files were restored
  • Unit tests cover:
    • mode parsing: default, exact values, and rejection of unknown values
    • signer: time-based nonce is seconds within the ±30s window, the HMAC is valid, and time-based calls do not advance the incremental counter
    • legacy REST: default is milliseconds, time-based is seconds with a valid signature, and the mode is captured at construction
    • SDK: incremental shares one sequence with legacy REST on standard hosts, time-based sends seconds on custom hosts, and the existing custom-host sharing test is pinned to incremental
    • tool layer: the hint is added for nonce errors and not for others
    • alerts whitelist contains the new variable
  • Live, read-only, with a time-based production key and GEMINI_API_NONCE_MODE=time-based: gemini_get_balances succeeds, and so does one SDK-authenticated call (positions)
  • Live, read-only, with a QA100 incremental key and the default mode: two balance calls back to back succeed, and a burst of about 20 parallel legacy and SDK calls has no InvalidNonce
  • Live, negative: the production key with the mode unset reproduces InvalidNonce and shows the hint
  • Sandbox key mode confirmed before the live run

Manual, against real sandbox and production (to run before merge):

  • Production time-based key, GEMINI_API_NONCE_MODE=time-based: balances should succeed. Before this change they failed with Nonce '1794333647192' is not within 30 seconds of server time.
  • QA100 incremental key, default mode: balances should succeed in a burst, which is the PR fix(mcp-server): sign requests with a millisecond nonce #75 regression guard.

What Success Looks Like

  • A reviewer can set GEMINI_API_NONCE_MODE=time-based on a production onboarding key and get balances, and the same key with the variable unset gets InvalidNonce plus a hint that names the variable.
  • A reviewer can run the QA100 incremental key with no variable set and see no change from PR fix(mcp-server): sign requests with a millisecond nonce #75: balances and SDK calls both succeed, including a same-millisecond burst.
  • A reviewer can set GEMINI_API_NONCE_MODE=bogus and see the server refuse to start with a clear message.
  • WebSocket auth headers are byte-for-byte unchanged in both modes, and the existing WebSocket tests still pass.
  • No regression to existing tools: npm test passes and the public-only mode still fails closed for authenticated calls.

🤖 Generated with Claude Code

…ests (PREDICT-11349)

Keys created with "Uses a time based nonce" (including Agentic onboarding
keys) reject millisecond nonces, while the QA100 keys from PR #75 need them.
The legacy REST signer always sent milliseconds, and the SDK hardcoded
seconds on standard hosts and milliseconds on custom hosts, so a single
key could not work on every path.

- Add GEMINI_API_NONCE_MODE = incremental | time-based. Unset defaults to
  incremental, preserving PR #75 behaviour. Any other value fails startup.
- Legacy REST signs with the configured unit. Incremental keeps the shared
  millisecond sequence; time-based uses epoch seconds with no state.
- SDK auth follows the same mode on every base URL. Incremental routes
  through the shared sequence so SDK and legacy requests never interleave
  out of order. Time-based uses the SDK's native time-based mode.
- WebSocket auth is unchanged and always uses epoch seconds.
- Nonce rejections get an operator hint naming the mode to set.
- Add the variable to the alerts daemon's env whitelist and the README.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@sdlc-pr-bot

sdlc-pr-bot Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Change Control Evidence Check

Authorization — ✅ Pass

Evidence:

  • Linear ticket: PREDICT-11349
  • Change owner: Sohum Desai
  • Type of change: Normal
Testing — ⚠️ Incomplete

Evidence:

  • Tests from CI checks: ➖ No tests ran
  • Security scans: ⚠️ Not configured — Semgrep OSS, scan_for_secrets, Scan with Semgrep, Socket Security: Pull Request Alerts not found
  • Testing summary: ✅ Found in PR description
    • Summary: All 368 unit tests passed (348 existing + 20 new), TypeScript type checking and build compilation completed without errors, and mutation testing confirmed that reverting changes to http.ts and sdk.ts caused 5 new tests to fail, validating test effectiveness. New test coverage includes mode parsing, signer validation (time-based nonce window, HMAC validity, and counter behavior), and legacy functionality.
  • Evidence link: View run
Approval — ⏳ Awaiting approval

Evidence:
(no approvers yet)

Segregation of Duties — ⏳ Awaiting approval

Validated:

  • PR author: Sohum Desai
  • Linear assignee: Sohum Desai

Last checked: 2026-11-10 19:29 UTC

↩ Re-run Clarissa if tickets, CI, or approvals have changed.

@linear-code

linear-code Bot commented Nov 10, 2026

Copy link
Copy Markdown

PREDICT-11349

@SohumDesai27
SohumDesai27 marked this pull request as ready for review November 10, 2026 19:08
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Risk Rating — High

The PR modifies HMAC-SHA384 nonce generation in packages/mcp-server/src/auth/signer.ts, src/client/http.ts, and src/client/sdk.ts — all Critical-tier components that sign every authenticated financial API request (orders, withdrawals, funds transfers). The change introduces a configurable nonce mode (GEMINI_API_NONCE_MODE) with strict startup validation and no new unauthenticated attack surface, so Change Characteristics are Medium; combined with Critical component sensitivity, the NIST SP 800-30r1 matrix yields High.

High/Critical — a full threat model follows.

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agentic Review

Nice, this covers the main nonce-mode paths, but SDK-authenticated nonce errors still miss the mode hint, so users can hit the mismatch without the guidance they get on the REST path. There are also a few test and setup-example gaps around the time-based mode; those are worth a follow-up but aren’t what’s driving the requested changes.

Review process

Grace version: v0.0.254
Files reviewed (13): README.md, config.test.ts, config.ts, http.request.test.ts, http.ts, index.test.ts, index.ts, sdk.test.ts, sdk.ts, signer.test.ts, signer.ts, types.ts
Guidelines: none discovered
Verification: 4 of 6 findings verified
Findings: 0 critical, 1 important, 3 suggestions
Linked tickets: PREDICT-11349
LLM usage: 100 calls — gpt-6-luna: 86 calls, 2434176 tokens, us.anthropic.claude-opus-5-5: 14 calls, 363225 tokens

Comment thread packages/mcp-server/src/tools/index.ts Resolved
Comment thread packages/mcp-server/src/config.test.ts Resolved
Comment thread packages/mcp-server/README.md Resolved
@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Threat Model - Passed - APPSEC-6108

…ts (PREDICT-11349)

- SDK nonce rejections now get the mode hint. The SDK keeps only the
  rejection code (reason=InvalidNonce) and a generic "HTTP <status>"
  message, so the hint now matches the full error text and falls back to a
  generic hint covering both mismatch directions when the server sentence
  is absent.
- An out-of-window rejection under time-based mode now reports clock skew.
- Cover the time-based "has not increased" branch through wrapHandler, and
  an SDK-shaped InvalidNonce error end to end.
- Load config in a child process with GEMINI_API_NONCE_MODE set, and with an
  invalid value, to cover the env-to-config startup path.
- Add GEMINI_API_NONCE_MODE to the Claude Desktop and ChatGPT README examples.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agentic Review

All four carried-over concerns are resolved — nice progress. I also missed the shared nonce counter’s SDK/REST dispatch issue on an earlier pass; that code hasn’t changed, so it’s a follow-up worth a look, not a blocker. This pass also flags a Windows-specific config test import issue; the inline note has the details.

Review process

Grace version: v0.0.254
Files reviewed (7): README.md, config.test.ts, index.test.ts, index.ts, sdk.test.ts, signer.test.ts, signer.ts
Files skipped: 12
Guidelines: none discovered
Verification: 2 of 5 findings verified
Findings: 0 critical, 0 important, 2 suggestions
Linked tickets: PREDICT-11349, APPSEC-6108
LLM usage: 78 calls — gpt-6-luna: 65 calls, 1685668 tokens, us.anthropic.claude-opus-5-5: 13 calls, 420875 tokens

Comment thread packages/mcp-server/src/client/sdk.ts Resolved
Comment thread packages/mcp-server/src/config.test.ts Resolved
… file: URL (PREDICT-11349)

import() reads a native Windows path such as C:\repo\config.ts as a URL
scheme, so the valid-mode config test failed on Windows. Pass a file: URL
derived from import.meta.url instead.
@SohumDesai27

Copy link
Copy Markdown
Collaborator Author

@svc-grace review

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agentic Review

The earlier setup-example, environment-config, Windows import, wrapper-coverage, and SDK-hint issues are all resolved. The shared nonce counter still doesn’t serialize SDK and REST dispatch; that’s a non-blocking follow-up if concurrent calls can interleave. This change carries the API key’s configured nonce mode through REST and SDK requests, and the key is keeping both paths aligned on the same mode and counter where incremental signing is used.

Persisted from prior passes (1 open)
Review process

Grace version: v0.0.254
Files reviewed (1): config.test.ts
Files skipped: 23
Guidelines: none discovered
Findings: 0 critical, 0 important, 0 suggestions
Linked tickets: PREDICT-11349, APPSEC-6108
LLM usage: 16 calls — gpt-6-luna: 16 calls, 201994 tokens

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.

2 participants