An AI Agent Wants to Disable an Account. What Should the Rails App Do Next? UITOP, working through a three-year enterprise cybersecurity product partnership, built a Rails-based control layer that treats AI agent recommendations as untrusted input at the authorization boundary. Agent proposals become durable action records with application-owned action allowlists, canonical fingerprints, and policy checks that verify the proposing agent's role scope, the initiating user, and the incident before any account disablement or session revocation executes. The design ensures a model-generated tool name never becomes an executable operation on its own. An incident response agent connects several alerts to the same user session. Someone opened a sensitive file, started an outbound transfer to an unauthorized destination, and used the same token from an unfamiliar endpoint. The agent recommends revoking the session, disabling the account, and isolating the endpoint. At this point, the model has done useful work. It has grouped the evidence and proposed a response. It still hasn't changed anything in the customer's environment. The next decision belongs to the application. The Rails backend has to check the agent's permissions, the target account, the current policy, the risk of each action, and the person allowed to approve it. If any part of that check fails, the action stops. UITOP has been working on an enterprise cybersecurity product through an ongoing three-year product partnership. The platform uses role-scoped sub-agents for security investigation, sensitive-data protection, remediation, and audit. Our work includes product design, interface implementation, Ruby on Rails development, agent logic, and product maintenance. I've changed the product name and left out client-specific architecture. The control model in this article reflects the engineering decisions we worked through in the real product. I treat an agent response as untrusted input at the authorization boundary. Valid JSON doesn't make an operation safe to run. The application first turns the recommendation into a durable action record. That record stays with the operation during policy evaluation, approval, execution, verification, and audit. create table :agent actions do |t| t.references :incident, null: false, foreign key: true t.string :proposed by agent id, null: false t.string :action type, null: false t.string :target type, null: false t.string :target id, null: false t.jsonb :parameters, null: false, default: {} t.string :risk level, null: false t.string :policy version, null: false t.string :fingerprint, null: false t.string :status, null: false, default: "proposed" t.references :approved by user, foreign key: { to table: :users } t.string :approved fingerprint t.datetime :approved at t.datetime :expires at t.string :execution reference t.jsonb :verification data, null: false, default: {} t.datetime :verified at t.integer :lock version, null: false, default: 0 t.timestamps end add index :agent actions, :fingerprint add index :agent actions, :incident id, :status action type comes from an application-owned allowlist. In this case, it may contain revoke session , disable account , and isolate endpoint . A model-generated tool name never becomes an executable operation on its own. The fingerprint ties approval to one version of the action. class AgentAction < ApplicationRecord before validation :set fingerprint private def set fingerprint self.fingerprint = ActionFingerprint.call action type: action type, target type: target type, target id: target id, parameters: parameters, policy version: policy version end end ActionFingerprint serializes parameters in a canonical order. Two equivalent payloads should produce the same value even if their keys arrive in a different sequence. The agent may suggest that an action is Low, Medium, High, or Critical. Rails makes the final decision. In the product we worked on, each sub-agent had its own role, data scope, tools, and permissions. Access to identity evidence did not grant permission to disable an identity. Any operation outside an explicit policy was blocked. The policy service receives the proposing agent, the initiating user, the incident, and the requested action. decision = AgentActionPolicy.call actor: current user, agent: proposing agent, incident: incident, action: action case decision.outcome when :denied action.update status: :policy denied, policy version: decision.policy version when :approval required action.update status: :awaiting approval, risk level: decision.risk level, policy version: decision.policy version, expires at: decision.expires at when :automatic action.update status: :approved, risk level: decision.risk level, policy version: decision.policy version, approved fingerprint: action.fingerprint ExecuteAgentActionJob.perform later action.id end The decision can depend on the environment, data sensitivity, destination, target type, and current incident state. A production account suspension should not inherit the same path as a read-only evidence lookup. We kept automatic execution narrow. A policy could allow evidence collection or an immediate block on a forbidden outbound transfer. Disabling an account, isolating production infrastructure, rotating credentials, and changing IAM or firewall rules required an authorized person. OWASP recommends a similar split in its AI Agent Security Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/AI Agent Security Cheat Sheet.html . The agent proposes an operation. Application-controlled policy and execution layers validate scope, privilege, and approval before a high-impact side effect. Imagine that an administrator reviews the proposed account suspension. The original plan disables the account indefinitely and revokes every active session. The administrator changes it to a two-hour freeze and revokes only the suspicious session. The old approval is no longer valid because the operation has changed. class ApproveAgentAction def self.call action:, approver:, expected lock version: action.with lock do raise StaleAction if action.lock version = expected lock version raise InvalidState unless action.awaiting approval? raise ExpiredAction if action.expires at&.past? decision = AgentActionPolicy.call actor: approver, agent: action.proposing agent, incident: action.incident, action: action raise Forbidden unless decision.approver? raise PolicyChanged unless decision.policy version == action.policy version action.update status: :approved, approved by user: approver, approved at: Time.current, approved fingerprint: action.fingerprint AuditEvent.record incident: action.incident, actor: approver, event type: "agent action.approved", subject: action, data: { fingerprint: action.fingerprint, policy version: action.policy version } end ExecuteAgentActionJob.perform later action.id end end Rails gives us with lock https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html for the row-level lock. It prevents two approval requests from consuming the same pending action at the same time. lock version also catches a reviewer acting on an older version. The browser sends the action ID, the decision, and the version shown to the reviewer. The server loads its own record, authenticates the person, checks authorization again, and stores the decision atomically. The OpenAI Agents SDK human-in-the-loop guide https://openai.github.io/openai-agents-js/guides/human-in-the-loop/ makes the same boundary clear. Approval state should live in server-controlled storage. A run ID or decision ID cannot prove that the person submitting it has permission to approve the tool call. Administrators in the product could remove a proposed action, change its target, or submit a different response. The revised plan went through policy evaluation again. It also required a second confirmation before execution. I would clear the previous approval inside the edit transaction. action.with lock do action.assign attributes parameters: revised parameters action.status = :proposed action.approved by user = nil action.approved at = nil action.approved fingerprint = nil action.save end EvaluateAgentActionJob.perform later action.id The callback creates a new fingerprint. Policy evaluation may assign another risk level or block the edited action. The audit history still shows the earlier proposal, while execution accepts only the current fingerprint. This behavior has to match the approval interface. If the administrator changes a target and presses confirm, the backend must not reuse a decision attached to the old parameters. The execution job uses a short transaction to claim the approved action. It calls the external security connector after releasing the database lock. class ExecuteAgentActionJob < ApplicationJob self.enqueue after transaction commit = true def perform action id action = AgentAction.find action id claimed = action.with lock do next false unless action.approved? next false unless action.approved fingerprint == action.fingerprint next false if action.expires at&.past? action.update status: :executing true end return unless claimed response = SecurityConnector.for action .call action type: action.action type, target id: action.target id, parameters: action.parameters, idempotency key: "agent-action- {action.id}- {action.fingerprint}" action.update status: :verifying, execution reference: response.reference VerifyAgentActionJob.perform later action.id rescue SecurityConnector::Timeout action.update status: :execution unknown ReconcileAgentActionJob.perform later action.id end end The idempotency key represents one approved operation. A timeout leaves us with uncertainty. The connector may have disabled the account even though Rails never received the response. Blindly repeating the request can create a second ticket, rotate a credential twice, or apply another policy change. I prefer an honest execution unknown state. A reconciliation job can query the downstream system by operation ID or inspect the target before it retries anything. Rails supports enqueue after transaction commit https://guides.rubyonrails.org/active job basics.html transactional-integrity-on-jobs for jobs that need committed data. The worker cannot pick up an approval record that is still inside an open transaction. Now return to the original incident. Session revocation succeeds. The account is disabled. Endpoint isolation times out. The remediation plan is still open because the endpoint may still have access. The execution worker stores the connector response. A separate worker checks the downstream state. verification = SecurityConnector.for action .verify action type: action.action type, target id: action.target id, execution reference: action.execution reference if verification.confirmed? action.update status: :completed, verified at: Time.current, verification data: verification.safe metadata else action.update status: :failed, verification data: verification.safe metadata end Each action needs its own verification rule. Session revocation checks that the session no longer exists. Account suspension reads the current account state. Endpoint isolation confirms the containment policy or network state. The incident closes after its required actions reach an accepted terminal state. A partial failure stays visible with its owner and remaining exposure. Rollback, when supported, becomes another policy-checked action with its own approval and verification record. For a high-risk action, I want the audit history to answer practical questions. I would leave raw secrets, access tokens, full sensitive files, and hidden model reasoning out of the payload. Stable evidence references and application-written summaries are enough to preserve the basis of the decision and the resulting action. Microsoft's shared responsibility model for AI agents https://learn.microsoft.com/en-us/azure/security/fundamentals/shared-responsibility-ai-agent keeps per-action authorization, approval for high-impact operations, agent identity, and audit logging with the application owner. I would run one incident through the following cases before letting the agent disable a real account. For every run, I would compare the server state, the interface, and the audit history. They should describe the same operation. An enterprise AI security agent becomes useful when it can move an incident forward. It becomes safe enough to operate when the surrounding product limits what it can see, what it can propose, what it can execute, and whose approval it needs. That work spans the full product. The interface has to show evidence, risk, scope, and current state clearly. The Rails application has to enforce the policy even if the model, the browser, or the connector behaves unexpectedly. I lead product development at UITOP. If you're building an enterprise AI agent that can touch company accounts, cloud resources, or sensitive data, feel free to message me. I'd be interested to hear how your team handles edited approvals, uncertain execution, and verification after a security action.