{"record":{"id":"cf7aa990abd851f7","repo":"paperclipai/paperclip","slug":"run-telemetry-is-outside-this-actor-s-authorizatio","errorCode":null,"errorMessage":"Run telemetry is outside this actor's authorization boundary","messagePattern":"Run telemetry is outside this actor's authorization boundary","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"server/src/routes/agents.ts","lineNumber":1075,"sourceCode":"      resource: { type: \"agent\", companyId: agent.companyId, agentId: agent.id },\n    });\n  }\n\n  async function assertAgentReadAllowed(req: Request, res: Response, agent: { id: string; companyId: string }) {\n    const decision = await decideAgentRead(req, agent);\n    if (decision.allowed) return true;\n    res.status(403).json({ error: \"Agent is outside this actor's authorization boundary\" });\n    return false;\n  }\n\n  async function assertRunTelemetryReadAllowed(req: Request, res: Response, companyId: string) {\n    const decision = await access.decide({\n      actor: req.actor,\n      action: \"company_scope:read\",\n      resource: { type: \"company\", companyId },\n    });\n    if (decision.allowed) return true;\n    res.status(403).json({ error: \"Run telemetry is outside this actor's authorization boundary\" });\n    return false;\n  }\n\n  async function filterAgentsForActor<T extends Record<string, unknown>>(\n    req: Request,\n    rows: T[],\n    fallbackCompanyId?: string,\n  ) {\n    const decisions = await Promise.all(rows.map((agent) => {\n      const id = typeof agent.id === \"string\" ? agent.id : null;\n      const companyId = typeof agent.companyId === \"string\" ? agent.companyId : fallbackCompanyId ?? null;\n      if (!id || !companyId) return Promise.resolve({ allowed: false });\n      return decideAgentRead(req, { id, companyId });\n    }));\n    return rows.filter((_, index) => decisions[index]?.allowed);\n  }\n\n  /**","sourceCodeStart":1057,"sourceCodeEnd":1093,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/routes/agents.ts#L1057-L1093","documentation":"This is an HTTP 403 JSON error emitted by `assertRunTelemetryReadAllowed` in the agents router. Before serving run telemetry (per-run events, heartbeat run logs) for a company, the route asks the access-control service whether the actor may perform `company_scope:read` on that company. If the decision is denied, the endpoint responds with this message instead of the data, enforcing that agent API keys (and other limited actors) cannot read telemetry outside their own company authorization boundary.","triggerScenarios":"A client calls a run-telemetry endpoint (run events / heartbeat run log routes that route through assertRunTelemetryReadAllowed) with an actor whose access.decide() for action `company_scope:read` on the target companyId returns { allowed: false } — typically an agent bearer key scoped to a different company or restricted keyScope.","commonSituations":"Using an agent_api_keys bearer token to fetch telemetry for another company's runs; a key scoped to a task bridge or limited scope that lacks company_scope:read; passing the wrong companyId in the URL after switching workspaces; stale key after the agent was moved between companies.","solutions":["Verify the actor's key actually belongs to the same company as the telemetry being requested; correct the companyId in the request.","Use a board/operator actor (full-control context) for cross-company or admin telemetry reads.","Issue a new agent API key with the appropriate scope if the agent legitimately needs company-scope read.","Check the client config/env for the wrong base company selection (e.g., pointing at another workspace's company id).","Re-authenticate if the key was rotated or the agent's company assignment changed."],"exampleFix":"// before: agent key hitting telemetry of another company\nconst res = await fetch(`/api/companies/${otherCompanyId}/runs/${runId}/events`, { headers: agentAuth });\n// after: use the actor's own company, or board auth for cross-company reads\nconst res = await fetch(`/api/companies/${myCompanyId}/runs/${runId}/events`, { headers: boardAuth });","handlingStrategy":"validation","validationCode":"// client-side check before calling telemetry endpoints\nconst companyId = resolveActorCompanyId(actor);\nif (requestedCompanyId !== companyId && actor.type !== 'board') {\n  throw new Error('actor cannot read telemetry outside its own company');\n}","typeGuard":"function canReadCompanyScope(actor: Actor, companyId: string): boolean {\n  return actor.type === 'board' || (actor.type === 'agent' && actor.companyId === companyId);\n}","tryCatchPattern":"const res = await fetch(telemetryUrl, { headers: auth });\nif (res.status === 403) {\n  const body = await res.json();\n  if (body.error.includes('authorization boundary')) {\n    throw new ForbiddenError('telemetry outside actor boundary; use board auth or own company id');\n  }\n}","preventionTips":["Always derive companyId from the authenticated actor, never from untrusted client input.","Use board/operator credentials for cross-company telemetry reads.","Audit agent key scopes when moving agents between companies.","Centralize telemetry URL construction so companyId comes from one validated source."],"tags":["authorization","http-403","telemetry","multi-tenancy"],"backgroundTag":"permission-denied","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}