feat(limits): add untrusted pool quota for anonymous + external usage#16
Merged
Conversation
Wildcard-only MCP names are not valid in permission allow rules, so the "mcp__*" entry was being skipped (flagged by /doctor). Replace it with valid per-server wildcards covering the vuetify, playwright-test and context7 MCP servers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed pool quota Adds a group-level 'untrusted' quota that caps anonymous and external usage combined, sitting between the per-user role limits and the global account cap, so untrusted public traffic can no longer exhaust the whole account budget. - new 'untrusted' RoleQuota in settings schema + defaults (0 = off, back-compat) - track a shared pool:untrusted usage aggregate in recordUsage - extract shared resolveUsageIdentity + enforceQuotas into usage/enforce.ts, deduplicating the gateway and summary enforcement paths - enforce order: global -> untrusted pool -> per-user - exclude pool:* from per-user history; retain on account-level cleanup schedule Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reqIp from lib-express works reliably when reverse-proxy configuration is enforced, so the try/catch fallback masked misconfiguration rather than guarding a real runtime edge case. The anonymous API tests bypass nginx, so they now set the X-Forwarded-For header themselves to mimic the reverse proxy that reqIp requires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test asserted that the sub-agent panel stays auto-expanded showing "Analysis complete" as a stable state. But this conversation ends on a trailing "done" reply from the mock main-agent (its response to the sub-agent's tool result), which by design collapses the sub-agent panel once the turn "ends on text" (AgentChatMessages auto-open logic). The sub-agent's final text renders at almost the same instant the panel collapses, so "Analysis complete" is only visible for a sub-second transient — the assertion passed only when Playwright happened to sample that window, making it flaky under load. Wait for the turn to fully settle (the trailing "done" message), then expand the collapsed panel explicitly before asserting the chain ran. The auto-expand-while-streaming behaviour remains covered by the single-turn test in chat-subagent.e2e.spec.ts. Verified 12x with no flakes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The trace overview sorts entries purely by timestamp. Session-setup entries (the system prompt and the initial tool snapshot) are recorded before the first turn starts, so their raw timestamps predate turns[0]. The system-prompt entry was already anchored to turns[0].timestamp, but the initial tools-changed entry kept its earlier raw timestamp, so it sorted above the system prompt. Tests only passed because, in a fast unit run, snapshotTools and startTurn usually land in the same millisecond and the stable sort preserves insertion order — but ~0.1% of runs tick the millisecond between them, floating tools-changed to index 0 and displacing the system prompt (proven by backdating the snapshot 1ms). Anchor any setup entry that predates the first turn to the first turn's timestamp, so the preamble keeps a stable document order (system prompt first) regardless of sub-millisecond timing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
albanm
added a commit
that referenced
this pull request
Jun 9, 2026
Reconcile the trace-storage work with main's quota refactor (#16) and streaming parallel-tool-call fix (#17): - gateway/summary routers adopt resolveUsageIdentity/enforceQuotas from usage/enforce.ts while keeping trace recording and the streamed tool-call capture; merged the per-id toolCallIndex with streamedToolCalls - defaultQuotas (now incl. untrusted) stays centralized in settings/service.ts; routers fall back to it, enforce.ts uses NonNullable<Settings['quotas']> - settings.quotas/models remain optional (storeTraces form work) - regenerated put-req validate.js and vjsf components from the merged schema Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add an untrusted pool quota: an aggregate cap shared by all anonymous and
external usage combined, so untrusted traffic can't consume the whole account
budget. The gateway and summary endpoints now share one quota gate
(
api/src/usage/enforce.ts) that enforces, in order: account-wide → untrustedpool → per-user/per-IP.
Why: anonymous + external callers could each stay under their own per-user
limit yet collectively exhaust the account's budget. The pool caps them as a group.
What changed
usage/enforce.ts(resolveUsageIdentity+enforceQuotas) shared by thegateway and summary routers, replacing the duplicated inline gates.
recordUsagealso upserts apool:untrustedaggregate;cleanup.tsretainspool aggregates on account-level retention; usage history hides
pool:*rows.untrustedrole-quota in the settings schema (regenerates the VJSF formsand put-req types).
reqIp wrapper removal, a chat-subagent e2e de-flake, a trace-ordering fix, and
a
.claude/settings.jsonMCP-permission cleanup.Regression risks
reqIpnow throws (→500) on a request without a validX-Forwarded-Forinstead of falling back to
127.0.0.1. Correct behind an enforced reverseproxy; the API tests set the header explicitly since they bypass nginx.
untrustedis notrequiredand the gate isguarded, so existing settings and accounts that don't set it are unaffected.
per-user
externalquota) once it's configured.cleanup.ts:pool:*aggregates move to the longer account-level retention;anon:*per-IP rows keep the short retention.