8 min read

Discovery Calls vs Mutation Calls for Safer AI Agents

Discovery calls vs mutation calls gives AI agents room to inspect systems while keeping writes, deletion, and external actions under human control.

Discovery Calls vs Mutation Calls for Safer AI Agents

An agent that can inspect a system widely and change it casually has the wrong shape of authority. The useful split is simple: let the agent discover enough to form a grounded plan, then put a deliberate boundary around operations that create, modify, trigger, or delete state.

That split sounds obvious until you meet a real API. A supposedly read-only endpoint refreshes a cache. A preview allocates a remote job. An update endpoint accepts an empty filter and touches every record. An SSH command looks harmless until a shell expansion turns a narrow path into a broad one. Method names and good intentions do not protect a production account.

I have seen teams solve this by placing a human in front of every call. That protects the account for an afternoon and trains everyone to approve without reading by Friday. The better design gives agents room to inspect, makes state changes conspicuous, and puts the strongest friction where a wrong action has an irreversible or expensive result.

Discovery needs a different permission shape

Discovery calls should answer questions about current state without changing that state, while mutation calls attempt to create, edit, execute, or remove something. The distinction matters because an agent cannot plan safely from stale guesses, but it also should not receive a standing right to act merely because it needs context.

A coding agent investigating a deployment failure may need to list services, fetch recent events, compare a configuration revision, inspect repository status, and read a remote issue. Those requests narrow uncertainty. If you force approval on each one, the operator sees a series of tiny prompts with too little context to assess. The agent also loses the thread of its investigation while it waits.

That same agent might later restart a workload, merge a pull request, rotate a credential, close an issue, or delete an object. Each operation changes the world outside the agent's context window. The operator should see the proposed target and effect before authorizing it.

This is not the same as granting a broad "read-only" role and calling it done. Discovery can expose sensitive material. It can also create costs, consume rate limits, or activate behavior in an ill-designed service. The split is about controlling action, not declaring inspection harmless.

A useful classification asks what the remote system can observe after the request completes:

  1. A discovery call returns information and leaves no relevant business, operational, or billing state changed.
  2. A mutation call creates, updates, deletes, executes, publishes, sends, or otherwise changes externally visible state.
  3. An ambiguous call needs mutation treatment until someone proves otherwise.

That third category carries more weight than most teams give it. If nobody can explain an endpoint's side effects from its contract and a controlled test, do not promote it into the unattended discovery set because its name sounds benign.

HTTP verbs offer a clue, not a permission decision

HTTP method names help classify calls, but they cannot replace endpoint review. RFC 9110 defines GET, HEAD, OPTIONS, and TRACE as "safe" methods, meaning the client does not request a state change. The RFC also warns that a server can still log requests, charge an account, or have other incidental effects.

That distinction is easy to miss. HTTP safety describes the intended semantics of the request, not a cryptographic promise that the implementation did nothing. RFC 9110 puts responsibility on the resource owner to avoid unsafe actions when users follow safe-method conventions. Your agent cannot enforce that promise if the service breaks it.

Treat these as starting positions rather than final decisions:

  1. GET and HEAD usually belong in the discovery candidate set. Review query parameters and endpoint documentation first.
  2. POST, PUT, PATCH, and DELETE belong in the mutation set unless a specific endpoint has a documented, tested read behavior.
  3. OPTIONS can inspect server capabilities, but some platforms include account-specific details that still need scope control.
  4. A webhook test, job preview, report export, or search endpoint may use POST and still be observational. Verify it instead of granting all POST access.

The inverse failure is common too. Developers sometimes wire a link or a GET route to an action because it is convenient. A URL such as /reports/monthly?refresh=true can rebuild an expensive report cache. A GET endpoint with ?send=true can deliver a notification. An agent following an API description will use what it receives. Do not expect the model to notice that the server designer ignored HTTP semantics.

Read the endpoint documentation for words such as "creates," "initiates," "refreshes," "generates," "sends," "records," "synchronizes," and "charges." Those verbs should move the call out of discovery even when the route uses GET. Then test the endpoint against a disposable account and compare state before and after, including job queues, notifications, usage counters, and audit records.

An HTTP response also helps classify the request. A response that returns a job identifier, an operation identifier, or a new resource URL often indicates that the remote service started work. A 200 status code proves only that the server handled the request. It does not prove that the request was observational.

Define resources and effects before you write allowlists

An allowlist of paths is too crude when it ignores the resource and effect behind each path. Define a small action inventory that says what an agent may inspect, what it may propose, and what it must never do without a direct human decision.

Use a record like this for each external action. The names do not matter. The evidence does.

Action: repository pull request list
Channel: HTTP
Target pattern: GET /repos/{owner}/{repo}/pulls
Class: discovery
Data returned: title, status, branch names, review metadata
Side-effect evidence: API reference defines this endpoint as a list operation
Scope limit: named repositories only
Review date: 2025-02-14

Action: repository merge pull request
Channel: HTTP
Target pattern: PUT /repos/{owner}/{repo}/pulls/{number}/merge
Class: mutation
Effect: changes merge state and source history
Required control: explicit approval for each call

The inventory forces a decision that teams often leave fuzzy: does the agent have permission to know about this resource, or permission to alter it? Those are separate grants. A ticketing system may permit discovery of issue title and status while denying comment bodies because they contain customer data. A cloud account may permit listing a narrowly scoped workload while denying access to account-wide identities and billing records.

Keep the scope in the action record. GET /projects is not a useful permission description if the credential can list every project a company has ever created. A better description names the organization, repository, namespace, account, or path the agent needs. If the upstream service cannot constrain the credential that way, enforce a target allowlist at the action gateway or do not make that discovery automatic.

Do not populate this list from swagger files alone. API descriptions often identify the method and parameters but omit operational consequences. Pair the documentation with a test account, an audit trail from the service, and a maintainer who owns the remote system. The point is to make an explicit claim that somebody can revisit when the API changes.

A read request can still harm the system

Read access has a blast radius, and it deserves its own limits. An agent with permission to inspect every repository, secret name, incident note, customer record, and deployment event has enough context to cause damage if an attacker controls the agent process or its instructions.

The popular recommendation is "grant read-only first." It is popular because it sounds cautious and fits familiar IAM role names. It is wrong when it substitutes a broad read role for thought about data classification. Broad discovery often creates the largest information disclosure in the design.

Separate two questions:

  • Can the request alter remote state?
  • Can the response reveal information the agent should not receive?

A request needs discovery treatment only after it clears the first question. It needs permission at all only after it clears the second. Do not collapse these tests into one label.

For sensitive resources, return only the fields the agent needs. A deployment diagnosis might need pod phase, image digest, and recent event messages. It does not need environment values or a full configuration object. An issue triage agent may need labels and timestamps, not every private comment. If an API lacks field selection, put a narrow intermediary action in front of it or keep the operation behind approval.

Pagination needs attention here. A list endpoint might look bounded during a happy-path test and become an unbounded data extraction path when the agent follows cursors. Cap page size and total pages where the channel permits it. Record that cap in the action inventory. Rate limits protect a provider, but they do not set an appropriate information boundary for your agent.

Search endpoints need the same treatment. A full-text search over source code or support records can reveal more than a direct object lookup, and prompt text can influence the query. Constrain searchable collections and query syntax. Do not pass agent-created search expressions straight into a powerful backend merely because the request uses GET.

Credentials must enforce the split where possible

Keep discovery credentials out
Sallyport keeps API and SSH credentials in its encrypted vault while agents receive only action results.

The safest place to distinguish inspection from action is the remote service's authorization model. Use a credential that can read the approved resources for discovery and a separate credential that can perform the bounded mutation actions the workflow requires.

This arrangement limits a simple but ugly failure: a malicious or confused agent finds a mutation endpoint you forgot to deny. If the discovery credential cannot write, the endpoint call fails even if your action layer classifies it incorrectly. That is defense through independent controls, not a reason to become careless with the action inventory.

Some services make this easy with scopes, roles, project permissions, or separate machine identities. Others expose only a broad personal token. When a provider offers only the latter, use the narrowest account and project boundary available, then enforce specific endpoints and target patterns in your gateway. Never give an autonomous agent an administrator's personal credential because it is already available.

A clean setup often has three credential classes:

  1. A discovery credential with access to the precise resources the agent can inspect.
  2. A mutation credential for bounded writes that still require an approval decision.
  3. A break-glass credential that agents never use and humans retrieve only through an incident process.

Do not let a mutation credential answer discovery calls by default. It seems harmless because the endpoint is read-only, but it makes future review harder. A log that says a powerful credential queried a resource does not tell you whether the agent needed that authority. Keep identity meaningful.

Sallyport keeps API and SSH credentials in its encrypted vault and executes the outbound action itself, so an MCP agent receives the result rather than the secret. That design makes it practical to expose a narrow discovery route without placing a bearer token or private SSH material in the agent's context.

Approval should describe the change, not punish every request

Per-call approval works when the reviewer can see a concrete effect and make a fast decision. It fails when the system asks for consent after every harmless lookup, because the human learns that the prompt contains no decision worth making.

Use session authorization to establish which agent process may use the discovery set. Then require individual approval for mutation actions, with the request shown in terms a human can judge. "POST /v1/jobs" is poor approval text. "Start a data export for project northwind, destination archive bucket, estimated 30 days" gives the reviewer something to verify.

The approval card should include the target, operation, meaningful parameters, and credential identity. Do not bury the target in a long JSON body. If an action touches more than one resource, show the count and a compact sample, then require the operator to open the full set before approval when the count exceeds the normal expected range.

A request payload for a bounded update might look like this:

{
  "action": "update_issue",
  "target": {
    "repository": "payments-api",
    "issue": 1842
  },
  "changes": {
    "labels_add": ["needs-review"],
    "assignee": "release-manager"
  },
  "reason": "The release checklist is complete."
}

The agent can prepare that request after unrestricted discovery within its permitted scope. The human should approve the change, not reconstruct the agent's entire investigation. Preserve the reason because it makes later review less speculative, but do not trust the reason as a security control. An agent can produce a plausible sentence for a bad target.

Avoid approval policies based solely on natural-language intent. "Approve deployment changes" sounds sensible until an agent labels a database deletion as a deployment change. Bind approval to an operation class, target scope, and credential. Text helps humans understand the request. Typed action boundaries stop mismatches.

Deletion and external execution need their own class

See which process asks
Sallyport leads session approval with the process code-signing authority, not an opaque agent label.

Deletion, permission changes, credential rotation, financial operations, and calls that trigger external systems should receive stricter treatment than ordinary updates. They can remove recovery paths, change who has access, create costs, or cause work outside the system where the agent started.

Do not hide deletion inside the general mutation bucket. A patch that corrects an issue label usually has a simple reversal. A delete call might remove attachments, child records, history, or a named environment. The API may queue asynchronous cleanup after it returns success, which means an operator cannot always reverse the error by sending one compensating request.

Require a distinct per-call control for actions such as:

  1. Delete, purge, archive when archive changes availability, and bulk removal.
  2. Permission, membership, role, secret, credential, and access-policy changes.
  3. Deployment, restart, scale, migration, and remote command execution.
  4. Sending email, posting messages, opening external tickets, and starting paid jobs.

Before the approval reaches a person, force the agent to resolve identifiers into a preview. A deletion request against records?filter=status=inactive should show the exact count, the filter, and representative names. Better still, make the agent retrieve the candidate identifiers first and submit a list that the gateway compares against the execution request. This does not eliminate races, but it catches the most common error: a filter that means something different from what the agent assumed.

Time-bound confirmation is also sensible. If an operator approved a destructive request an hour ago, the agent should not use that approval after the surrounding state has changed. Tie the approval to one request body or an immutable request digest, not to a broad category like "delete access for today."

SSH requires command-level classification

SSH is harder to classify than a well-designed API because a command line can combine inspection and mutation, call a shell, follow aliases, or change behavior based on the remote environment. You cannot safely call an entire host read-only because the agent intends to run a read command.

Start with explicit commands and arguments. git status --short, git log -n 20 --oneline, and kubectl get pods -n staging are plausible discovery actions when you constrain the working directory, cluster context, and namespace. git push, kubectl apply, kubectl delete, rm, package installation, and service restarts belong in mutation or destructive execution classes.

Reject shell composition for unattended discovery. This is the sort of command that looks like a listing request but hands control back to the shell:

find "$WORKDIR" -maxdepth 2 -type f -name '*.log' -print; $EXTRA_COMMAND

Even if the agent supplies an empty EXTRA_COMMAND in testing, a later value can execute anything permitted by the SSH identity. Do not approve a command grammar that contains ;, &&, ||, command substitution, redirects, wildcard expansion over uncontrolled paths, or an interpreter invocation unless the action itself is reviewed as execution.

Use structured arguments instead. A gateway can accept an action named list_recent_logs, validate a fixed directory and a numeric limit, then construct the remote command itself. The agent should never submit a shell string when a typed action will do.

The same principle applies to tools with read verbs. kubectl get can leak secret data if the resource and namespace are broad. git show can expose a credential committed by mistake. Build command permissions around executable, subcommand, arguments, working directory, and remote identity. The verb alone tells you very little.

Audit records must show the boundary held

Gate risky calls per key
Require approval on every use of a sensitive key, including destructive or externally triggering calls.

An audit trail should let a reviewer answer who made a call, what credential route the action used, what target it reached, whether a person approved it, and what the remote system returned. If the trail only records "agent invoked tool," it cannot prove that discovery access stayed separate from mutation authority.

Log both attempted and completed actions. A denied delete request matters. It might indicate that the agent misunderstood its scope, that a prompt injection attempted to steer it, or that someone is testing the boundary. Record the denial reason without writing secrets or sensitive response bodies into a log that has wider access than the original system.

For each action, capture at least these fields in a tamper-evident record:

  • Agent process identity and session identifier.
  • Action class: discovery, mutation, destructive, or external execution.
  • Target identity, request method or command form, and a safe representation of parameters.
  • Approval event, including the approving human when one was required.
  • Outcome, remote status, result reference, and timestamps.

A hash chain helps detect log modification, but it does not make a vague event useful. Store a canonical action record before execution, then bind the response record to it. If the record says only POST /jobs, a later reviewer still cannot tell whether the agent started a harmless report or an expensive export.

Sallyport projects session and call journals from one encrypted hash-chained audit log, and sp audit verify checks the chain offline without needing vault access. Verification is useful only if the action taxonomy makes the event intelligible, so keep the classification close to the execution path rather than adding labels later in a reporting layer.

Test the boundary with failures, not happy paths

A permission split earns trust when it blocks bad requests you can predict. Treat every discovery action as a testable contract and run negative cases whenever the API, command wrapper, or agent workflow changes.

For an HTTP discovery route, test an allowed target, a neighboring forbidden target, an unsupported method, an oversized page request, and a query that attempts to cross a tenant or project boundary. Expect specific denials. A generic server error is poor evidence because it may become a success after a minor code change.

For a mutation route, test that a valid request pauses for approval, that changing any meaningful field invalidates the approval, and that a second agent process does not inherit the first process's session permission. For deletion, test empty selectors, wildcard-like selectors, lists larger than expected, and targets that disappeared between preview and execution.

Keep one worked trace for each action class. A good trace is short enough for a reviewer to read and concrete enough to expose a mismatch:

09:14:03  discovery  GET /projects/acme/services?limit=20  allowed
09:14:05  discovery  GET /projects/acme/services/api-7/events  allowed
09:14:11  mutation   POST /projects/acme/services/api-7/restart  approval required
09:14:32  mutation   POST /projects/acme/services/api-7/restart  approved by operator
09:14:34  mutation   result: accepted, operation=op_481

If the third line instead says GET /services/api-7?action=restart, your classification model has already found a defect. Fix the action contract or keep that route under the mutation control. Do not create an exception because the endpoint is awkward.

Start with the external actions your agents already perform. Mark each one by effect, verify its actual behavior, constrain the target, and test the denial cases. When the agent asks to do something new, require an explicit classification before the action reaches production. That small pause is cheaper than trying to explain an unreviewed change after the fact.

FAQ

Are GET requests always safe for an AI agent to run?

No. HTTP GET describes a safe method in HTTP semantics, but an application can still attach side effects to it. Treat the method as evidence, then test the endpoint and inspect its documentation before granting unattended access.

Do I need separate credentials for read and write access?

Use a separate read credential when the service supports it. If it does not, restrict the agent to an allowlist of verified read endpoints and require approval for anything outside that list.

Which discovery calls can run without human approval?

Start with automatic approval only for a small, tested set of inspection calls that cannot alter state or incur material cost. Keep session-level approval in place so a new agent process cannot silently inherit trust.

Can a dry run replace approval for destructive actions?

A dry run helps only when the service guarantees that it does not alter state and shows the exact target set. It is evidence for a later approval, not permission to skip approval for the real command.

How should I handle bulk update API calls?

Treat every bulk endpoint as a mutation until you verify its behavior. A single API call that updates many records needs stronger review than a single-record edit because an incorrect filter multiplies the impact.

Can SSH commands be split into read and write permissions?

Yes. A command such as git status or kubectl get normally inspects state, while rm, git push, and apply commands alter it. Classify SSH actions by the actual command and arguments, not merely by the SSH host.

What should an audit log capture for agent actions?

Record the agent process, target, operation, arguments or a safe digest of them, timestamp, outcome, and approval decision. For mutations, the log should also let a reviewer identify the changed resource and correlate the result with the request.

How do I verify that an endpoint is really read-only?

Do not rely on a human to remember which endpoints are safe. Keep a small reviewed inventory, test each entry against a nonproduction account where possible, and revisit it when the upstream API changes.

Should delete actions use stricter controls than updates?

Deletion deserves its own class. It removes recovery options, often affects related records, and can succeed before an operator notices a bad target, so require explicit per-call confirmation and a target preview.

Is broad read-only access harmless?

No. Read access can expose customer data, source code, tokens, billing details, or operational topology. Limit discovery by scope, log it, and assume that a compromised agent can turn broad read access into a serious incident.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov