{"record":{"id":"692c7abedfb60e59","repo":"paperclipai/paperclip","slug":"agents-may-only-read-their-own-decision-stats","errorCode":null,"errorMessage":"Agents may only read their own decision stats","messagePattern":"Agents may only read their own decision stats","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"server/src/routes/decisions.ts","lineNumber":169,"sourceCode":"    if (!query.success) { res.status(400).json({ error: \"Invalid decision filters\", details: query.error.flatten() }); return; }\n    res.json(await svc.list(companyId, query.data));\n  });\n  /**\n   * Gardener telemetry contract:\n   * { groupBy: \"ruleKey\", filters: { originAgentId: string|null, since: ISO-8601|null },\n   *   totals: { proposed, accepted, rejected, expired },\n   *   groups: [{ ruleKey: string|null, proposed, accepted, rejected, expired,\n   *     chosenOptions: [{ optionId, count }] }] }\n   * Accepted means a non-dismissed decided outcome; rejected means an explicit dismiss;\n   * chosenOptions counts accepted outcomes only; expired is separate, and cancelled\n   * decisions contribute only to proposed.\n   */\n  router.get(\"/companies/:companyId/decisions/stats\", async (req, res) => {\n    const companyId = req.params.companyId as string; assertBoardOrAgent(req); assertCompanyAccess(req, companyId);\n    const query = statsQuerySchema.safeParse(req.query);\n    if (!query.success) { res.status(400).json({ error: \"Invalid decision stats filters\", details: query.error.flatten() }); return; }\n    if (req.actor.type === \"agent\" && query.data.originAgentId && query.data.originAgentId !== req.actor.agentId) {\n      res.status(403).json({ error: \"Agents may only read their own decision stats\" }); return;\n    }\n    const originAgentId = req.actor.type === \"agent\" ? req.actor.agentId : query.data.originAgentId;\n    res.json(await svc.stats(companyId, { originAgentId, since: query.data.since }));\n  });\n  router.get(\"/decisions/:id\", async (req, res) => {\n    assertBoardOrAgent(req);\n    const decision = await getAccessibleResource(req, res, svc.get(req.params.id as string), \"Decision not found\");\n    if (!decision) return;\n    if (req.actor.type === \"agent\" && req.actor.agentId !== decision.originAgentId) { res.status(403).json({ error: \"Only the origin agent may read this decision\" }); return; }\n    res.json(await svc.outcome(decision.id));\n  });\n  router.post(\"/decisions/:id/decide\", validate(decideSchema), async (req, res) => {\n    const userId = boardUserId(req);\n    const decision = await getAccessibleResource(req, res, svc.get(req.params.id as string), \"Decision not found\");\n    if (!decision) return;\n    res.json(await svc.decide({ id: decision.id, decidedByUserId: userId, userActor: req.actor, ...req.body }));\n  });\n  router.post(\"/decisions/:id/dismiss\", validate(dismissSchema), async (req, res) => {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/decisions.ts#L151-L187","documentation":"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.","triggerScenarios":"Thrown at server/src/routes/decisions.ts:169 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}