{"record":{"id":"e379a6664152136a","repo":"jumpserver/jumpserver","slug":"approved-operation-path-has-changed","errorCode":null,"errorMessage":"Approved operation path has changed.","messagePattern":"Approved operation path has changed\\.","errorType":"http","errorClass":"PermissionDenied","httpStatus":403,"severity":"error","filePath":"apps/chat_ai/approvals/service.py","lineNumber":129,"sourceCode":"                        approval.agent_run.assistant_message.error = 'APPROVAL_EXPIRED'\n                        approval.agent_run.assistant_message.save(update_fields=('status', 'error', 'date_updated'))\n                raise ValidationError('Approval has expired.')\n            if not hmac.compare_digest(approval.request_hash, self.request_hash(approval.request_payload)):\n                raise PermissionDenied('Approval request hash is invalid.')\n            try:\n                expected_signature = self.signature_for(approval)\n            except SigningKeyUnavailable as exc:\n                raise PermissionDenied('Approval signing key is unavailable.') from exc\n            if not hmac.compare_digest(approval.signature, expected_signature):\n                raise PermissionDenied('Approval signature is invalid.')\n\n            operation = self.registry.get(approval.operation_id)\n            if not operation or operation.method != approval.method:\n                raise PermissionDenied('Approved operation no longer exists.')\n            self.policy.enforce(operation, approval.request_payload)\n            path, _, _ = self.builder.build(operation, approval.request_payload)\n            if path != approval.path:\n                raise PermissionDenied('Approved operation path has changed.')\n            approval.status = Approval.Status.PROCESSING\n            approval.confirmed_by = user\n            approval.confirmed_at = timezone.now()\n            approval.expires_at = timezone.now() + timedelta(\n                seconds=getattr(settings, 'CHAT_AI_API_TIMEOUT', 15) + 60\n            )\n            approval.save(update_fields=(\n                'status', 'confirmed_by', 'confirmed_at', 'expires_at', 'date_updated'\n            ))\n        return approval, operation\n\n    @staticmethod\n    def finish(approval, result):\n        ok = bool(result.get('ok'))\n        approval.status = Approval.Status.CONFIRMED if ok else Approval.Status.FAILED\n        approval.result_summary = summarize(result)\n        approval.error = '' if ok else f'Core API returned HTTP {result.get(\"status_code\", 0)}'\n        approval.save(update_fields=('status', 'result_summary', 'error', 'date_updated'))","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/jumpserver/jumpserver/blob/6ec464fabd61b95912d539455a3a5f15f5c59fe0/apps/chat_ai/approvals/service.py#L111-L147","documentation":"Raised when rebuilding the request from the stored payload produces a different URL path than the one recorded on the approval. The path is rebuilt via builder.build(operation, request_payload); drift means the operation template or path params changed since creation, so executing would hit a different endpoint.","triggerScenarios":"Path template or path-param serialization in the registry/builder changed after the approval was created (e.g. '/files/{id}' → '/files/{file_id}', or encoding changes), so the rebuilt path differs.","commonSituations":"API spec redeployments renaming path params or restructuring routes; builders changing URL encoding of params; approvals pending across such a release.","solutions":["Treat as terminal: cancel the approval and recreate it against the current spec","Keep path templates stable, or drain pending approvals before merging breaking route changes","Add tests that rebuild paths for pending approvals during spec-change CI","If unexpected, diff the approval.path against builder.build output to find the template drift"],"exampleFix":"# before\npath, _, _ = builder.build(operation, approval.request_payload)\nassert path == approval.path  # fails after route rename\n\n# after\n# deployment step: drain pending approvals before renaming routes\nfor a in Approval.objects.filter(status=Approval.Status.PENDING):\n    ApprovalService.cancel(a.id, a.user, a.org_id)\ndeploy_new_spec()","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"try {\n  await confirm(approvalId);\n} catch (e) {\n  if (e.status === 403 && /path has changed/i.test(e.message)) {\n    await cancelApproval(approvalId);\n    await restartAgentRun(conversationId);\n  } else throw e;\n}","preventionTips":["Avoid renaming route templates while approvals are pending","Drain pending approvals as part of deploying breaking route changes","Test path rebuilding against pending approvals in deploy previews"],"tags":["approvals","routing","versioning"],"backgroundTag":"route-changed-after-approval","analyzedSha":"6ec464fabd61b95912d539455a3a5f15f5c59fe0","analyzedAt":"2026-08-28T11:33:00.925Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}