Repository navigation
fix(mcp-server): honour the API key's nonce mode in REST and SDK requests (PREDICT-11349) - #76
Conversation
…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.
|
@svc-grace review |
Change Control Evidence CheckAuthorization — ✅ PassEvidence:
Testing — ✅ PassEvidence:
Approval — ✅ PassEvidence:
Segregation of Duties — ✅ PassValidated:
Last checked: 2026-11-12 16:51 UTC ↩ Re-run Clarissa if tickets, CI, or approvals have changed. |
Nostradamus Risk Rating — HighThe PR modifies HMAC-SHA384 nonce generation in High/Critical — a full threat model follows. |
svc-grace
left a comment
There was a problem hiding this comment.
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
|
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.
|
@svc-grace review |
svc-grace
left a comment
There was a problem hiding this comment.
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
… 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.
|
@svc-grace review |
svc-grace
left a comment
There was a problem hiding this comment.
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
… desktop config example (PREDICT-11349) - ALERTS.md: list the env vars the daemon bakes into its unit as they are in code, adding GEMINI_API_NONCE_MODE and the previously missing GEMINI_SDK_ENV, and note that a time-based key needs the mode baked into the unit. - claude_desktop_config.example.json: set GEMINI_API_NONCE_MODE to time-based, matching the README examples, so copying it with an Agentic key works.
Whats included
GEMINI_API_NONCE_MODEsetting:incremental(default) ortime-based. It has to match the API key's "Uses a time based nonce" setting, so it is never inferred from the base URL. Any other value stops the server from starting.InvalidNonce: Nonce '1794333647192' is not within 30 seconds of server time '1794333647'.api.gemini.com/sandbox and milliseconds on custom hosts. Both now use the configured mode. Incremental keeps fix(mcp-server): sign requests with a millisecond nonce #75's shared millisecond counter for both clients. Time-based sends stateless epoch seconds, using the SDK's native time-based mode.InvalidNonceerrors: settime-basedwhen a key rejects milliseconds, useincrementalwhen it rejects seconds, check the host clock for an out-of-window error undertime-based, and a general hint for SDK errors (the SDK keeps onlyreason=InvalidNonce, not the server's message).GEMINI_API_NONCE_MODEis added to the daemon's env whitelist so an installed daemon keeps it.README.md,ALERTS.mdandclaude_desktop_config.example.jsondocument it, and the Claude Code, Claude Desktop and ChatGPT examples set it.GEMINI_API_NONCE_MODE=time-basedis set, and the error says exactly that. Incremental keys (the default for keys created without the time-based box) need no change.confirm: truegate.Linear: https://linear.app/gemini/issue/PREDICT-11349/support-per-key-gemini-api-nonce-modes-in-mcp
Test plan
npm run build— cleannpm run typecheck— cleannpm test— 374/374 passing, including:time-basedtime-basedsends secondswrapHandler, and an SDKInvalidNonceerror end to end through the real SDK with a fakefetchhttp.ts/sdk.ts, matching the hint onmessageonly, always passingincrementalfrom the wrapper, and ignoring the env inconfig.tseach make the new tests failgemini_get_balances(legacy REST) andgemini_get_prediction_positions(SDK).Test 1 — production, Agentic (time-based) key,
GEMINI_API_NONCE_MODE=time-basedPrompt:
Using only the gemini-prod-test MCP server, call gemini_get_balances, then call gemini_get_prediction_positions.Both succeeded. Balances returned the account's USD balance, which is the call that failed before this PR. Positions returned
{"positions": []}.Test 2 — production, same key, burst
Prompt:
call gemini_get_balances 5 times one after another, then 5 times in parallel10/10 succeeded, with no
InvalidNonce.Test 3 — production, same key, variable unset (reproduces the original bug)
Prompt:
Using only the gemini-prod-test MCP server, call gemini_get_balances, then call gemini_get_prediction_positions.gemini_get_balances:gemini_get_prediction_positions:Test 4 — QA100, time-based key
With the variable unset,
gemini_get_balancesfailed with:With
GEMINI_API_NONCE_MODE=time-based, balances and positions both succeeded, and 5 sequential + 5 parallel balance calls all succeeded.Test 5 — QA100, incremental key, default mode
A new QA100 key created with "Uses a time based nonce" unchecked, and no variable set. Balances, positions, and 5 sequential + 5 parallel balance calls all succeeded, so #75's behavior is unchanged.
Test 6 — QA100, same incremental key,
GEMINI_API_NONCE_MODE=time-basedgemini_get_balances:Back on the default mode, balances succeeded again, so the wrong setting didn't affect the key.
Note for anyone repeating this: a brand-new incremental key accepts a seconds nonce for the first few minutes, because the server saves a key's last nonce after a delay. The rejection above showed up once that had happened.
Test 7 — invalid setting
Command:
GEMINI_API_NONCE_MODE=bogus node dist/index.jsThe server exits without starting.
Not tested live: an incremental key on production or sandbox, a sandbox key, and the alerts daemon or WebSocket streams with a time-based key. Unit tests cover the daemon whitelist and WebSocket auth.