跳到正文
DEV Community· Oleksandr Sainchuk·· 3 小時前AI 評分66

AI 智能體要求停用帳戶,Rails 應用下一步應怎樣處理?

An AI Agent Wants to Disable an Account. What Should the Rails App Do Next?

AI 導讀

Rails 應用處理 AI 智能體的安全操作時,應先保存提案,再檢查權限和政策,並按風險要求完成審批後才執行。文章以停用帳戶、撤銷工作階段和隔離端點為例,説明審批須綁定操作指紋,計劃修改後須重新評估。執行後還要核實下游狀態並保留審計紀錄;連接器逾時時應標記 execution_unknown,先對賬再決定是否重試。

正文

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.

Save the proposal before calling a security tool

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.

Let Rails assign the effective risk

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.

Make the approval specific

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.

Approval applies to one specific action. If its target or parameters change, the action must be reviewed again

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.

Recheck every edited remediation plan

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.

Claim the action once

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.

1. When execution is uncertain, block duplicate attempts and verify the real downstream state first

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.

Verify the security outcome

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.

Keep an audit trail people can use

For a high-risk action, I want the audit history to answer practical questions.

  • Which incident produced the proposal?
  • Which agent proposed it?
  • Which evidence supported it?
  • Which policy version evaluated it?
  • Who approved, rejected, or edited it?
  • Which fingerprint did that person approve?
  • What did the connector attempt?
  • What did the downstream system report?
  • Did verification pass?
  • Did someone roll the action back later?

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.

Test the uncomfortable paths before release

I would run one incident through the following cases before letting the agent disable a real account.

  1. The agent proposes an allowed action for a permitted target.
  2. The same agent requests a tool outside its role.
  3. An unauthorized employee tries to approve the action.
  4. An administrator edits the target after reviewing the plan.
  5. Two approval requests arrive almost together.
  6. The connector finishes after Rails times out.
  7. Execution succeeds and verification fails.
  8. A different authorized administrator requests rollback.

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.

來源:DEV Community · dev.to