cd /news/ai-agents/an-ai-agent-wants-to-disable-an-acco… · home › topics › ai-agents › article
[ARTICLE · art-160589] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

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.

by read8 min views8 publishedOct 29, 2026

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. 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 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 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 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 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.

── more in #ai-agents 4 stories · sorted by recency
── more on @uitop 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/an-ai-agent-wants-to…] indexed:0 read:8min 2026-10-29 · —