{"slug": "what-should-an-ai-agent-be-allowed-to-do-without-asking-you", "title": "What Should an AI Agent Be Allowed to Do Without Asking You?", "summary": "A developer argues that AI agents need an authorization layer outside the language model to govern which actions they can take autonomously. The proposed design classifies agent actions by risk — such as read_public, write_internal, financial, and destructive — and routes each request through a policy engine that can allow, require approval, or deny it. The developer warns that relying on system prompts for safety amounts to \"hopeful wording\" rather than real enforcement.", "body_md": "Your agent notices a failing deployment. It reads the logs, identifies the bad commit, drafts a rollback, and now wants to apply it.\n\nShould it just do it?\n\nIf it asks for permission at every step, it becomes an expensive autocomplete. If it can do anything it can describe, it becomes a liability. The useful question is not “How smart is the agent?” It is:\n\nWhich actions are safe to pre-authorize, which actions need explicit approval, and which actions should be impossible?\n\nThat is not a prompt engineering problem. It is a permission design problem.\n\nMost teams start with one of two bad defaults.\n\nThe first is **maximum caution**: the agent must ask before everything. That feels safe, but it destroys the value of the agent. If every file read, search query, or draft suggestion requires approval, the human becomes the bottleneck.\n\nThe second is **maximum convenience**: the agent gets broad tool access because it is “good at knowing what to do.” That works until the agent misinterprets a request, hits an edge case, follows a poisoned instruction, or performs an action the user never meant.\n\nThe better approach is to classify actions by risk.\n\nAn agent can often do these without asking:\n\nAn agent should usually ask before doing these:\n\nThe distinction is not whether the agent is confident. The distinction is whether the action is **reversible, bounded, private, and expected**.\n\n**Scenario:**\n\nYour system prompt says, “Only take safe actions. Ask before doing anything destructive.” The agent has a `delete_repository` tool. One day, it decides a repository is no longer needed.\n\n**Why it matters:**\n\nLanguage model behavior is probabilistic. Prompt instructions influence the agent, but they do not guarantee enforcement. If the tool is available and the agent chooses it, the action may happen.\n\nA safe agent system needs an authorization layer outside the model.\n\n**Solution:**\n\nTreat every agent action as a permissioned API call. The agent can propose an action, but your system decides whether the action is allowed, requires approval, or is denied.\n\n```\ntype RiskClass =\n  | \"read_public\"\n  | \"read_sensitive\"\n  | \"write_draft\"\n  | \"write_internal\"\n  | \"external_communication\"\n  | \"financial\"\n  | \"destructive\"\n  | \"privilege_change\";\n\ninterface AgentActionRequest {\n  agentId: string;\n  userId: string;\n  action: string;\n  risk: RiskClass;\n  resource: string;\n  estimatedCost?: number;\n  context: Record<string, unknown>;\n}\n\ninterface AuthorizationDecision {\n  allow: boolean;\n  approvalRequired?: boolean;\n  reason?: string;\n}\n\nasync function authorizeAgentAction(\n  req: AgentActionRequest\n): Promise<AuthorizationDecision> {\n  const policy = await policyStore.forUserAndAgent(req.userId, req.agentId);\n\n  if (policy.isDenied(req)) {\n    return { allow: false, reason: \"Denied by policy\" };\n  }\n\n  if (policy.requiresApproval(req)) {\n    return { allow: false, approvalRequired: true };\n  }\n\n  if (!(await withinBudget(req))) {\n    return { allow: false, reason: \"Budget exceeded\" };\n  }\n\n  return { allow: true };\n}\n```\n\n**Why this works:**\n\nThe model is no longer the final authority. It can suggest `delete_repository`, but the policy engine can deny it regardless of how persuasive the model’s reasoning is.\n\n🚨 Production warning:\n\nIf your agent’s safety depends on the system prompt, you do not have safety controls. You have hopeful wording.\n\n**Scenario:**\n\nYou review agent permissions by looking at a list of tool names: `read_logs`, `create_ticket`, `send_email`, `restart_service`, `delete_branch`. Some are obviously dangerous. Others seem harmless. But tool names do not tell you the real risk.\n\n**Why it matters:**\n\nA flat tool list hides the important question: what kind of effect does this action have on the world?\n\n`create_ticket` can be harmless if it creates a draft. It can be noisy if it creates a real ticket in a production tracker. It can be serious if it pages on-call at 3 a.m.\n\n**Solution:**\n\nClassify actions on a risk ladder.\n\n| Stage | Example | Default treatment | \n|---|---|---|\n| Observe | Read public docs, list files | Usually allowed | \n| Analyze | Summarize logs, compare diffs | Usually allowed | \n| Propose | Suggest a fix, draft a plan | Usually allowed | \n| Draft | Create draft PR, draft ticket, local branch | Usually allowed if private | \n| Execute internal | Update staging config, create internal ticket | Allowed with limits | \n| Publish | Merge PR, publish page, deploy | Usually approval | \n| Externalize | Email customer, post to Slack channel, call partner API | Usually approval | \n| Spend | Buy credits, refund payment, provision paid resources | Approval plus budget | \n| Destroy | Delete record, drop table, terminate instance | Strong approval or impossible | \n| Change permissions | Add admin, modify scopes, change policy | Almost always approval | \n\nThis gives you a more stable mental model than tool names.\n\nFor example:\n\n```\nrisk_ladder:\n  allow_without_asking:\n    - observe\n    - analyze\n    - propose\n    - draft_private\n\n  allow_with_limits:\n    - write_staging\n    - create_internal_ticket\n\n  require_approval:\n    - publish\n    - external_communication\n    - spend\n    - destructive\n    - privilege_change\n\n  deny:\n    - self_permission_change\n    - policy_modification\n    - bulk_delete_unscoped\n```\n\n**Why this works:**\n\nYou are authorizing effects, not names. A new tool can be added later, but if its effect is “external communication,” it still lands in the approval bucket.\n\n💡 Practical note:\n\nWhen adding a new agent tool, ask: “What is the worst plausible effect if this is called at the wrong time?” Then map that effect to the ladder.\n\n**Scenario:**\n\nYour agent can read your issue tracker, internal wiki, and customer support database. It is only “reading,” so you allow it. Then a user asks it to summarize a customer issue. The agent includes an API key, internal email, or medical detail in the summary.\n\n**Why it matters:**\n\nRead access is still access. If the agent can read sensitive data, it can repeat, transform, summarize, or exfiltrate that data — intentionally or accidentally.\n\nThis is especially important when the agent can:\n\nA read-only action can become a data-leak path.\n\n**Solution:**\n\nApply data classification and field-level filtering before information reaches the agent.\n\n```\ninterface CustomerRecord {\n  id: string;\n  name: string;\n  email: string;\n  paymentMethod?: string;\n  supportNotes?: string;\n  ssnLast4?: string;\n}\n\nfunction sanitizeCustomerForAgent(\n  record: CustomerRecord,\n  policy: DataPolicy\n): Partial<CustomerRecord> {\n  return {\n    id: record.id,\n    name: policy.canSeePII ? record.name : redact(record.name),\n    email: policy.canSeePII ? record.email : redact(record.email),\n    supportNotes: policy.canSeeSupportNotes\n      ? record.supportNotes\n      : \"Support notes unavailable for this agent.\",\n  };\n}\n\nfunction redact(value: string): string {\n  return `[redacted:${value.length}]`;\n}\n```\n\nThe important part is that redaction happens before the agent sees the record, not after.\n\n**Why this works:**\n\nYou reduce the agent’s blast radius. Even if the model is tricked, misused, or overly chatty, it cannot reveal data it never received.\n\n⚠️ Gotcha:\n\n“Read-only” is a database property, not a safety property. If the agent can read secrets and then write to a public channel, read access becomes an exfiltration route.\n\n**Scenario:**\n\nYour agent wants to create a branch, write a proposed migration, open a draft pull request, or prepare a support response. You are unsure whether these writes should require approval.\n\n**Why it matters:**\n\nIf every small write requires approval, the agent becomes slow and annoying. But if the agent can publish or execute immediately, mistakes become visible too fast.\n\n**Solution:**\n\nPre-authorize writes that are:\n\nExamples:\n\nA useful pattern is to make the agent’s first write always produce a **pending artifact**:\n\n```\ninterface DraftPullRequest {\n  id: string;\n  title: string;\n  body: string;\n  branch: string;\n  status: \"draft\";\n  createdBy: \"agent\";\n  requiresHumanReview: true;\n}\n\nasync function agentCreatesDraftPr(input: {\n  repo: string;\n  branch: string;\n  title: string;\n  body: string;\n}) {\n  return prService.create({\n    repo: input.repo,\n    branch: input.branch,\n    title: input.title,\n    body: input.body,\n    draft: true,\n    metadata: {\n      createdBy: \"agent\",\n      requiresHumanReview: true,\n    },\n  });\n}\n```\n\n**Why this works:**\n\nThe agent can make progress without forcing a human to approve every microstep. The human reviews the artifact before it becomes real.\n\nThis is one of the best autonomy patterns: let the agent do the work, but make the result inherently reviewable.\n\n🧠 The important part:\n\nDrafts are powerful because they separate labor from commitment. The agent can do the labor. A human or policy can control the commitment.\n\n**Scenario:**\n\nThe agent is asked to “clean up old customer accounts.” It finds 3,000 inactive accounts and prepares a deletion batch. The user meant “mark inactive,” not “delete.”\n\n**Why it matters:**\n\nSome actions are not dangerous because they are technically complex. They are dangerous because they cross a boundary: from private to public, from reversible to irreversible, from cheap to expensive, from internal to external.\n\nThe approval question is really a boundary question.\n\n**Solution:**\n\nRequire approval when an action crosses one of these boundaries:\n\nA policy gate can look like this:\n\n```\nfunction requiresApproval(req: AgentActionRequest): boolean {\n  if (req.risk === \"external_communication\") return true;\n  if (req.risk === \"financial\") return true;\n  if (req.risk === \"destructive\") return true;\n  if (req.risk === \"privilege_change\") return true;\n\n  if (req.context.bulk === true) return true;\n  if (req.context.environment === \"production\" && req.action.includes(\"delete\")) {\n    return true;\n  }\n\n  return false;\n}\n```\n\n**Why this works:**\n\nYou are not asking the model to judge every situation. You are encoding boundaries that your organization already understands.\n\n🔍 Why this matters:\n\nApproval should be triggered by the nature of the action, not by the model’s confidence. Confidence is not authority.\n\n**Scenario:**\n\nYour agent is allowed to call a search API, run code in a sandbox, and retry failed steps. Individually, each action seems safe. But a bug causes it to retry in a loop. Within an hour, it has burned through API credits and created hundreds of duplicate artifacts.\n\n**Why it matters:**\n\nA single action can be safe while the cumulative behavior is unsafe. Permissions need to account for frequency, volume, and cost.\n\nAn agent that can spend $0.05 per action is not safe if it can perform 100,000 actions.\n\n**Solution:**\n\nAttach budgets and rate limits to agent capabilities.\n\n```\ninterface BudgetState {\n  agentId: string;\n  day: string;\n  estimatedCostUsd: number;\n  toolCalls: number;\n}\n\nasync function withinBudget(req: AgentActionRequest): Promise<boolean> {\n  const budget = await budgetStore.get(req.agentId);\n\n  const estimatedCost = req.estimatedCost ?? 0;\n\n  if (budget.toolCalls + 1 > policy.maxToolCallsPerDay) {\n    return false;\n  }\n\n  if (budget.estimatedCostUsd + estimatedCost > policy.maxDailyCostUsd) {\n    return false;\n  }\n\n  return true;\n}\n```\n\nFor rate limits:\n\n```\nasync function withinRateLimit(req: AgentActionRequest): Promise<boolean> {\n  const key = `rate:${req.agentId}:${req.action}`;\n  const count = await rateLimiter.increment(key, { window: \"1m\" });\n\n  return count <= policy.maxCallsPerMinute(req.action);\n}\n```\n\n**Why this works:**\n\nYou are treating autonomy as a bounded resource. The agent can act without asking, but only within a container of acceptable consumption.\n\nThis is especially important for agents that can:\n\n💡 Practical note:\n\nIf an agent can retry, it needs a circuit breaker. Unlimited retries turn small mistakes into expensive incidents.\n\n**Scenario:**\n\nYour agent sends a message saying, “I deleted the stale staging environment.” The user sees the notification two hours later. The environment contained a database seed needed for a demo.\n\n**Why it matters:**\n\nThere is a big difference between:\n\nMany systems blur these together. They say the agent “asked” when it really just announced.\n\n**Solution:**\n\nBe explicit about the approval mode.\n\n```\ntype ApprovalMode =\n  | \"allow\"\n  | \"notify_after\"\n  | \"require_approval_before\"\n  | \"deny\";\n\ninterface ApprovalRequest {\n  runId: string;\n  agentId: string;\n  userId: string;\n  action: AgentActionRequest;\n  mode: ApprovalMode;\n  expiresAt: string;\n}\n\nasync function handleAction(req: AgentActionRequest, runId: string) {\n  const decision = await authorizeAgentAction(req);\n\n  if (decision.allow) {\n    return executeAction(req);\n  }\n\n  if (decision.approvalRequired) {\n    const approval = await approvalStore.create({\n      runId,\n      agentId: req.agentId,\n      userId: req.userId,\n      action: req,\n      mode: \"require_approval_before\",\n      expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),\n    });\n\n    await runStore.pause(runId, approval.id);\n\n    return {\n      status: \"waiting_for_approval\",\n      approvalId: approval.id,\n    };\n  }\n\n  throw new Error(decision.reason ?? \"Action denied\");\n}\n```\n\nNow the run can pause until approval arrives.\n\n**Why this works:**\n\nThe system distinguishes between actions that can proceed, actions that need a human gate, and actions that merely deserve a record.\n\nUseful defaults:\n\n| Action type | Best mode | \n|---|---|\n| Read public docs | Allow | \n| Summarize internal data | Allow or notify | \n| Create draft PR | Allow | \n| Create real ticket | Notify or approval depending on impact | \n| Send customer email | Require approval before | \n| Delete production data | Require approval before or deny | \n| Change permissions | Require approval before | \n| Spend money | Require approval before | \n\n⚠️ Gotcha:\n\nIf the action cannot be undone, notifying afterward is not the same as consent.\n\n**Scenario:**\n\nYour agent can manage tools, install plugins, update its own configuration, or request new OAuth scopes. It encounters a task it cannot complete, so it tries to expand its permissions.\n\n**Why it matters:**\n\nThis is the permission equivalent of giving a program `sudo` because it asked nicely. If an agent can modify its own authority, the entire permission model collapses.\n\nThis risk shows up in several forms:\n\n**Solution:**\n\nMake permission changes a protected human action by default.\n\n``` js\nconst PROTECTED_ACTIONS = new Set([\n  \"policy.update\",\n  \"agent.scope.grant\",\n  \"oauth.scope.request\",\n  \"user.role.promote\",\n  \"tool.install\",\n  \"mcp_server.register\",\n  \"workflow.privilege.escalate\",\n]);\n\nfunction isProtectedAction(action: string): boolean {\n  return PROTECTED_ACTIONS.has(action);\n}\n\nasync function authorizeProtectedAction(req: AgentActionRequest) {\n  if (isProtectedAction(req.action)) {\n    if (req.context.requestedBy === req.agentId) {\n      return {\n        allow: false,\n        reason: \"Agents cannot modify their own privileges\",\n      };\n    }\n\n    return {\n      allow: false,\n      approvalRequired: true,\n      reason: \"Privilege changes require human approval\",\n    };\n  }\n\n  return authorizeAgentAction(req);\n}\n```\n\n**Why this works:**\n\nYou prevent the agent from becoming a privilege-escalation vector. Even if the agent is compromised, confused, or manipulated by untrusted input, it cannot directly widen its own authority.\n\n🚨 Production warning:\n\nWatch out for indirect escalation. If Agent A can ask Agent B to do something, and Agent B has broader permissions, you need to evaluate the full delegation chain.\n\nThe safest agent systems I’d design do not try to make the agent perfectly wise. They make the system legible and controllable.\n\nA practical production architecture looks like this:\n\n```\nUser request\n  ↓\nAgent planner\n  ↓\nProposed action\n  ↓\nPolicy engine\n  ↓\nBudget/rate check\n  ↓\nApproval gate if needed\n  ↓\nTool execution layer\n  ↓\nAudit log\n  ↓\nUser notification / UI\n```\n\nThe important components are:\n\nDecides whether an action is allowed, denied, or needs approval.\n\n```\ndefault: deny\n\nrules:\n  - allow:\n      action: logs.read\n      environment: staging\n\n  - allow:\n      action: pull_request.create_draft\n      repo_visibility: internal\n\n  - require_approval:\n      action: email.send\n      recipient_type: external\n\n  - require_approval:\n      action: payment.refund\n      when:\n        amount_cents_gt: 10000\n\n  - deny:\n      action: policy.update\n      requested_by: agent\n```\n\nPerforms the actual tool call only after authorization.\n\nRecords:\n\nLets a human pause, cancel, or roll back the agent.\n\n```\nawait runStore.cancel(runId, {\n  reason: \"user_requested\",\n  cancelledBy: userId,\n});\n```\n\n**Why this works:**\n\nYou are not asking the model to be the security boundary. You are building a system where the model can be useful without being omnipotent.\n\nIf I had to reduce the decision to one rule, it would be this:\n\nAn AI agent may act without asking when the action is low-risk, reversible, bounded, private, auditable, and within a pre-approved budget.\n\nIf any of those are false, pause.\n\nMore concretely, I would allow an agent to act without asking when all of these are true:\n\nI would require approval when any of these are true:\n\nThe last one deserves emphasis. If the agent read an email, issue comment, web page, or document that could contain hostile instructions, you should be more cautious about letting it take privileged actions immediately afterward.\n\nThe goal is not to make agents passive. The goal is to make their autonomy legible.\n\nA good agent permission system feels like a well-designed employee role:\n\nThe agent should not be trusted because it sounds confident.\n\nIt should be allowed because your system has already decided that this class of action is safe.", "url": "https://wpnews.pro/news/what-should-an-ai-agent-be-allowed-to-do-without-asking-you", "canonical_source": "https://dev.to/hosseinhezami/what-should-an-ai-agent-be-allowed-to-do-without-asking-you-4fb9", "published_at": "2026-09-10 07:52:55+00:00", "updated_at": "2026-09-10 08:22:32.073947+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-policy", "developer-tools", "ai-tools"], "entities": ["delete_repository", "create_ticket", "send_email", "restart_service", "delete_branch"], "alternates": {"html": "https://wpnews.pro/news/what-should-an-ai-agent-be-allowed-to-do-without-asking-you", "markdown": "https://wpnews.pro/news/what-should-an-ai-agent-be-allowed-to-do-without-asking-you.md", "text": "https://wpnews.pro/news/what-should-an-ai-agent-be-allowed-to-do-without-asking-you.txt", "jsonld": "https://wpnews.pro/news/what-should-an-ai-agent-be-allowed-to-do-without-asking-you.jsonld"}}