paperclipai/paperclip · error
Agents may only read their own decision stats
Error message
Agents may only read their own decision stats
What it means
Scoping guard on the decision stats route: the caller is an agent actor, and agents may only read decision statistics scoped to themselves; a stats request not filtered to the calling agent's origin is refused with 403 to prevent cross-agent visibility.
Source
Thrown at server/src/routes/decisions.ts:169
if (!query.success) { res.status(400).json({ error: "Invalid decision filters", details: query.error.flatten() }); return; }
res.json(await svc.list(companyId, query.data));
});
/**
* Gardener telemetry contract:
* { groupBy: "ruleKey", filters: { originAgentId: string|null, since: ISO-8601|null },
* totals: { proposed, accepted, rejected, expired },
* groups: [{ ruleKey: string|null, proposed, accepted, rejected, expired,
* chosenOptions: [{ optionId, count }] }] }
* Accepted means a non-dismissed decided outcome; rejected means an explicit dismiss;
* chosenOptions counts accepted outcomes only; expired is separate, and cancelled
* decisions contribute only to proposed.
*/
router.get("/companies/:companyId/decisions/stats", async (req, res) => {
const companyId = req.params.companyId as string; assertBoardOrAgent(req); assertCompanyAccess(req, companyId);
const query = statsQuerySchema.safeParse(req.query);
if (!query.success) { res.status(400).json({ error: "Invalid decision stats filters", details: query.error.flatten() }); return; }
if (req.actor.type === "agent" && query.data.originAgentId && query.data.originAgentId !== req.actor.agentId) {
res.status(403).json({ error: "Agents may only read their own decision stats" }); return;
}
const originAgentId = req.actor.type === "agent" ? req.actor.agentId : query.data.originAgentId;
res.json(await svc.stats(companyId, { originAgentId, since: query.data.since }));
});
router.get("/decisions/:id", async (req, res) => {
assertBoardOrAgent(req);
const decision = await getAccessibleResource(req, res, svc.get(req.params.id as string), "Decision not found");
if (!decision) return;
if (req.actor.type === "agent" && req.actor.agentId !== decision.originAgentId) { res.status(403).json({ error: "Only the origin agent may read this decision" }); return; }
res.json(await svc.outcome(decision.id));
});
router.post("/decisions/:id/decide", validate(decideSchema), async (req, res) => {
const userId = boardUserId(req);
const decision = await getAccessibleResource(req, res, svc.get(req.params.id as string), "Decision not found");
if (!decision) return;
res.json(await svc.decide({ id: decision.id, decidedByUserId: userId, userActor: req.actor, ...req.body }));
});
router.post("/decisions/:id/dismiss", validate(dismissSchema), async (req, res) => {View on GitHub (pinned to a7e689b3c3)
Solutions
- This is an authorization rule, not a bug: perform the action with an actor that satisfies the stated constraint (board user, the owning agent, or an in-scope resource).
- If access should be allowed, verify the actor's credentials/company scope and the resource's ownership before retrying.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at server/src/routes/decisions.ts:169 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18).
Data as JSON: /api/errors/692c7abedfb60e59.
Report an issue: GitHub.