{"record":{"id":"8ea6e41bb57bbd2b","repo":"paperclipai/paperclip","slug":"runcontext-must-include-agentid-runid-companyi","errorCode":null,"errorMessage":"\"runContext\" must include agentId, runId, companyId, and projectId","messagePattern":"\"runContext\" must include agentId, runId, companyId, and projectId","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/src/routes/plugins.ts","lineNumber":1040,"sourceCode":"      res.status(400).json({ error: \"Request body is required\" });\n      return;\n    }\n\n    const { tool, parameters, runContext } = body;\n\n    // Validate required fields\n    if (!tool || typeof tool !== \"string\") {\n      res.status(400).json({ error: '\"tool\" is required and must be a string' });\n      return;\n    }\n\n    if (!runContext || typeof runContext !== \"object\") {\n      res.status(400).json({ error: '\"runContext\" is required and must be an object' });\n      return;\n    }\n\n    if (!runContext.agentId || !runContext.runId || !runContext.companyId || !runContext.projectId) {\n      res.status(400).json({\n        error: '\"runContext\" must include agentId, runId, companyId, and projectId',\n      });\n      return;\n    }\n\n    assertCompanyAccess(req, runContext.companyId);\n    const scopeError = await validateToolRunContextScope(runContext);\n    if (scopeError) {\n      res.status(403).json({ error: scopeError });\n      return;\n    }\n\n    if (req.actor.type === \"agent\" && toolGatewayDeps) {\n      try {\n        const result = await toolGatewayDeps.toolGateway.executePluginTool({\n          actor: {\n            type: \"agent\",\n            agentId: req.actor.agentId,","sourceCodeStart":1022,"sourceCodeEnd":1058,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1022-L1058","documentation":"Returned as HTTP 400 by POST /api/plugins/tools/execute (server/src/routes/plugins.ts:1025) when runContext is an object but at least one of agentId, runId, companyId, projectId is falsy (missing, empty string, null). All four identify the execution scope checked afterwards by assertCompanyAccess and validateToolRunContextScope, so partial contexts are rejected up front.","triggerScenarios":"runContext: {agentId, runId, companyId} without projectId (common after projectId was added to the contract); any of the four set to '' after trimming form input; contexts built from optional session fields that are undefined outside a run (e.g. invoking tools from a background job with no runId).","commonSituations":"Clients updated for three of the four ids; test harnesses reusing a stale context object; code paths executing tools outside an active run where runId was never created; company/project context not selected yet in the UI.","solutions":["Populate all four fields — create a run first if runId is missing, and select/derive companyId and projectId before executing","Assert non-empty values client-side before sending (a quick every() over the four keys)","Update older callers that predate the projectId requirement to thread project context through"],"exampleFix":"// before\nconst runContext = { agentId, runId, companyId };\n\n// after\nconst runContext = { agentId, runId, companyId, projectId };\nconst required = [\"agentId\", \"runId\", \"companyId\", \"projectId\"] as const;\nif (!required.every((k) => runContext[k])) {\n  throw new Error(`runContext missing: ${required.filter((k) => !runContext[k]).join(\", \")}`);\n}","handlingStrategy":"validation","validationCode":"const required = [\"agentId\", \"runId\", \"companyId\", \"projectId\"] as const;\nconst missing = required.filter((k) => !runContext[k]);\nif (missing.length) {\n  throw new Error(`runContext missing required fields: ${missing.join(\", \")}`);\n}\nawait api.executePluginTool({ tool, parameters, runContext });","typeGuard":"type CompleteRunContext = { agentId: string; runId: string; companyId: string; projectId: string };\nconst isCompleteRunContext = (v: unknown): v is CompleteRunContext => {\n  const c = v as Record<string, unknown>;\n  return [\"agentId\", \"runId\", \"companyId\", \"projectId\"].every((k) => typeof c?.[k] === \"string\" && c[k] !== \"\");\n};","tryCatchPattern":null,"preventionTips":["Create the run and select the company/project before any tool execute call so all four ids exist","Type the context as CompleteRunContext at the call boundary so missing fields fail to compile"],"tags":["plugins","plugin-tools","validation","http-400","rest-api","run-context"],"backgroundTag":"request-validation-failed","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}