{"record":{"id":"e465980585e3681c","repo":"paperclipai/paperclip","slug":"native-interaction-self-approval","errorCode":"native_interaction_self_approval","errorMessage":"native_interaction_self_approval","messagePattern":"native_interaction_self_approval","errorType":"error_code","errorClass":"NativeInteractionBridgeError","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/native-interaction-bridge.ts","lineNumber":185,"sourceCode":"      eq(issues.id, input.issueId),\n      eq(issues.companyId, input.companyId),\n    )).limit(1).then((rows) => rows[0] ?? null),\n  ]);\n  if (!issue) throw new NativeInteractionBridgeError(\"native_interaction_binding_mismatch\", \"Issue binding not found\");\n  const responses: NativeInteractionResponseEnvelope[] = [];\n\n  for (const interaction of interactions) {\n    if (!requestedIds.has(interaction.id)) continue;\n    if (interaction.companyId !== input.companyId || interaction.issueId !== input.issueId) {\n      throw new NativeInteractionBridgeError(\n        \"native_interaction_binding_mismatch\",\n        `Interaction ${interaction.id} is not bound to the native company and issue`,\n      );\n    }\n    if (interaction.kind === \"request_confirmation\" && interaction.payload.toolAction) {\n      const action = interaction.payload.toolAction;\n      if ([\"accepted\", \"rejected\"].includes(interaction.status) && (interaction.resolvedByAgentId || interaction.resolvedByRunId === input.runId)) {\n        throw new NativeInteractionBridgeError(\"native_interaction_self_approval\", \"Agents cannot resolve governed tool reviews\");\n      }\n      const [request] = await input.db.select().from(toolActionRequests).where(and(eq(toolActionRequests.id, action.actionRequestId), eq(toolActionRequests.companyId, input.companyId), eq(toolActionRequests.issueId, input.issueId), eq(toolActionRequests.interactionId, interaction.id), eq(toolActionRequests.invocationId, action.invocationId)));\n      const [invocation] = await input.db.select().from(toolInvocations).where(and(eq(toolInvocations.id, action.invocationId), eq(toolInvocations.companyId, input.companyId), eq(toolInvocations.issueId, input.issueId), eq(toolInvocations.agentId, input.agentId)));\n      if (!request || !invocation || request.requestedByAgentId !== input.agentId || request.canonicalArgumentsHash !== action.argumentsHash) {\n        throw new NativeInteractionBridgeError(\"native_interaction_governed_request_unresolved\", \"Tool review has no matching authoritative invocation\");\n      }\n      if ([\"expired\", \"cancelled\"].includes(request.status)) {\n        if (interaction.status !== request.status && !(interaction.status === \"accepted\" && interaction.result?.toolAction?.status === \"expired\")) throw new NativeInteractionBridgeError(\"native_interaction_governed_result_mismatch\", \"Tool review lifecycle does not match its request\");\n        responses.push({ interactionId: interaction.id, kind: interaction.kind, response: { status: interaction.status, result: structuredClone(interaction.result), executionStatus: request.status } });\n        continue;\n      }\n      if (!request.decidedByUserId || request.decidedByUserId !== interaction.resolvedByUserId || ![\"executed\", \"failed\", \"rejected\"].includes(request.status) || (request.status === \"rejected\" ? interaction.status !== \"rejected\" : interaction.status !== \"accepted\")) {\n        throw new NativeInteractionBridgeError(\"native_interaction_governed_request_unresolved\", \"Tool review must have a human decision and an authoritative terminal execution outcome\");\n      }\n      const expectedInvocationStatus = request.status === \"executed\" ? \"succeeded\" : request.status === \"rejected\" ? \"denied\" : \"failed\";\n      if (invocation.status !== expectedInvocationStatus || (request.status !== \"rejected\" && interaction.result?.toolAction?.status !== request.status)) throw new NativeInteractionBridgeError(\"native_interaction_governed_result_mismatch\", \"Tool review outcome does not match its invocation\");\n    }\n    const interactionResult = record(interaction.result);","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/native-interaction-bridge.ts#L167-L203","documentation":"`materializeNativeInteractionResponses` rejects any attempt by an agent to resolve a governed `request_confirmation` tool review itself. If the interaction is accepted/rejected and it was resolved by an agent (`resolvedByAgentId` set) or by the same run submitting the response (`resolvedByRunId === input.runId`), the bridge throws `NativeInteractionBridgeError` with code `native_interaction_self_approval`. This enforces the approval-gate invariant: governed tool actions require a human decision, never agent self-approval.","triggerScenarios":"Calling `materializeNativeInteractionResponses` with a response payload for a `request_confirmation` interaction whose `payload.toolAction` is set, where `interaction.status` is \"accepted\" or \"rejected\" and either `resolvedByAgentId` is non-null or `resolvedByRunId` equals the submitting run's id.","commonSituations":"An agent adapter (or a buggy automation acting as the run) tries to mark its own tool-approval request as accepted instead of waiting for a human; a replayed/duplicated response from the same run after an agent-side resolution was recorded; a tool written against non-native interactions assumes agents may self-confirm governed actions.","solutions":["Do not resolve governed tool reviews from agent code: surface the request_confirmation interaction to a human operator and let them accept/reject it.","Check `interaction.resolvedByAgentId` / `resolvedByRunId` before submitting; if already resolved by an agent, treat the interaction as invalid rather than attempting to finalize it.","If a human already decided, ensure the human's userId is recorded as `resolvedByUserId` (not the agent/run) before materializing the response.","If the run is re-submitting after a retry, make the submission idempotent: skip interactions whose status is already terminal instead of re-materializing them."],"exampleFix":"// before: agent tries to accept its own governed tool review\nawait materializeNativeInteractionResponses(input); // throws native_interaction_self_approval\n\n// after: only human-resolved interactions are materialized; agent-side ones are skipped\nconst responses = input.responses.filter(\n  (r) => !(r.status === \"accepted\" || r.status === \"rejected\") || r.resolvedByUserId,\n);\nawait materializeNativeInteractionResponses({ ...input, responses });","handlingStrategy":"validation","validationCode":"function canMaterialize(interaction) {\n  if (interaction.kind !== \"request_confirmation\" || !interaction.payload.toolAction) return true;\n  const terminal = [\"accepted\", \"rejected\"].includes(interaction.status);\n  const selfResolved = Boolean(interaction.resolvedByAgentId) || interaction.resolvedByRunId === input.runId;\n  return !(terminal && selfResolved);\n}","typeGuard":"function isHumanResolved(i: { resolvedByAgentId?: string | null; resolvedByUserId?: string | null }): boolean {\n  return i.resolvedByUserId != null && i.resolvedByAgentId == null;\n}","tryCatchPattern":"try {\n  await materializeNativeInteractionResponses(input);\n} catch (err) {\n  if (err instanceof NativeInteractionBridgeError && err.code === \"native_interaction_self_approval\") {\n    // route the review to a human approver; never retry from the agent\n  }\n  throw err;\n}","preventionTips":["Never set resolvedByAgentId / submit resolutions from the run that requested the tool action.","Always attribute human decisions to a real userId.","Skip already-terminal interactions on retries instead of re-materializing them.","Treat governed tool reviews as human-only in adapter code."],"tags":["governance","approval-gate","security","agents"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}