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

  1. 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).
  2. 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.