fix(approvals): force create attribution from the actor + per-agent creation caps - #305
Open
claudegoogl-sudo wants to merge 3 commits into
Open
fix(approvals): force create attribution from the actor + per-agent creation caps#305claudegoogl-sudo wants to merge 3 commits into
claudegoogl-sudo wants to merge 3 commits into
Conversation
…reation caps Co-Authored-By: Paperclip <noreply@paperclip.ing>
…AC10 inspected count The fake limiter shared one hit bucket across agents, so AC7's second agent saw the first agent's spend and the AC10 inspected-count expectation counted the spoof request (which short-circuits before the caps) as a limiter hit. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…caps Co-Authored-By: Paperclip <noreply@paperclip.ing>
Owner
Author
|
Security-sensitive change (authorization surface on the approval create route), so requesting a security review before merge per our process. Review focus: (1) the 403/400 attribution guard and the forced attribution at the row build, (2) the cap values in |
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.
Follows
CONTRIBUTING.mdPR template — sections present: Thinking Path, Linked Issues or Issue Description, What Changed, Verification, Risks, Model Used, Checklist.Thinking Path
Linked Issues or Issue Description
Refs #296 (related work in the same file: request-payload requirements and the agent withdraw route; this change is independent and stays conflict-free with it).
Described in-PR (no public issue exists for this yet):
What happened?
The approval create route accepted
requestedByAgentIdfrom the request body for every caller. An agent-authenticated caller could set it to any agent's uuid. The created card was then attributed to that other agent: the card showed the wrong requester, wakeups went to the wrong agent, and the requester-only resubmit right followed the forged field. The route also had no per-agent creation cap, so an agent could create unlimited pending cards.Expected behavior
An approval card is always attributed to its real requester. An agent caller may omit
requestedByAgentId(then the card is attributed to the caller) or echo its own id. Any other value is rejected with403and no row is created. User and board callers must not send the field; a non-null value is rejected with400. Agent callers are capped: at most 10 creates per 60-second sliding window, and at most 5 simultaneouslypendingcards. Exceeding either cap returns429and creates no row.Steps to reproduce
On a build without this change, as an agent with a valid agent API key:
The request returns
201and creates a card attributed to the other agent. With this change it returns403, creates no row, and logsapproval.create_denied.Paperclip version or commit
Fork
masteratb2d6b8bb0(this branch bases on it).Deployment mode
Any deployment that issues agent API keys (single-node and multi-node alike). No database migration is involved.
Access context
The spoof path needs a valid agent API key for the target company, so the caller already passes tenant checks. The forged field crosses an authorization boundary inside the tenant: it changes who owns the approval card and who may act on it.
What Changed
server/src/routes/approvals.ts:requestedByAgentIdpresent and different from the caller →403, no row. Equal to self → accepted (idempotent clients keep working). Omitted → falls back to the caller. User/board actor + non-null field →400, no row.requestedByAgentIdfrom the authenticated actor. The body value can no longer influence attribution even if a guard regresses.approval.create_denied). The log names the rule; details never include payload contents.429withRetry-Afterwhen exhausted. The hit is recorded only after the row is created, so a rejected request never spends budget.pendingcards before the create,429naming the cap and the remedy when at the cap. NoRetry-After— the cap frees on a human resolve or withdraw, not on a clock.server/src/services/approval-create-rate-limit.ts(new):createApprovalCreateRateLimiter()built on the shared sliding-window store (sliding-window-rate-limit-store.ts) — the same primitive as the plugin webhook limiter. No new dependency, sweep-on-write, bounded live keys, module-scope default so re-registering routers cannot reset the ceiling.countPendingApprovalsForAgent()for the pending-cap budget check.packages/shared/src/constants.ts:APPROVAL_CREATE_RATE_LIMIT_WINDOW_MS(60s),APPROVAL_CREATE_RATE_LIMIT_MAX_PER_AGENT(10),APPROVAL_CREATE_PENDING_CARD_CAP_PER_AGENT(5), each with rationale. Values are a starting point; happy to tune them.server/src/__tests__/approval-routes-hardening.test.ts(new): 12 route-level tests that drive the real router.docs/api/approvals.md: documents the attribution rules, theapproval.create_deniedactivity action, and both caps.Internal callers of the approvals create service (not the HTTP route) were audited and are unchanged:
server/src/routes/built-in-agents.ts(~L190),server/src/routes/agents.ts(~L3256 and ~L3276) — all three derive the field from the authenticated actor;server/src/services/plugin-capability-escalation.ts(~L210) — passesnull(system-attributed card). None accept client input for this field.Verification
All commands run from the repo root on this branch:
cd server && ../node_modules/.bin/vitest run src/__tests__/approval-routes-hardening.test.ts --testTimeout=120000→ 12 passed. The suite drives the realapprovalRoutesrouter with an agent actor. Cases: spoof →403with row count unchanged; self-echo →201; omitted field →201attributed to caller; user actor with field →400; both caps →429(burst withRetry-After, pending naming cap and remedy); cap exemption for board/user actors; rejected requests never consume burst budget; per-agent bucket isolation; dedupe-path statement test.vitest runoverapproval-routes-idempotency,approvals-service,agents-pending-approval-config,built-in-agent-routes,built-in-agents, plus the fouragents-*service suites → 137 passed across 10 files.tsc --noEmitinserver/and inpackages/shared/→ both clean.docs/api/approvals.mdcreate section updated.Risks
requestedByAgentIdto another agent's id now get403instead of a forged card. Legitimate clients either omit the field or echo their own id, and both keep working. The MCP create tool never sends the field.Retry-After. The constants are trivially tunable.count(*)per agent create; negligible next to the create itself.Model Used
Exact model id not exposed to this session; operating as agent "Coder" under the Paperclip runtime (extended thinking + tool use: code search, edits, test execution, and GitHub CLI).
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template#NNN/github.com/paperclipai/paperclipURLs)docs/...,fix/...) and contains no internal Paperclip ticket id or instance-derived details