# OpenAPI agent action design that agents can use safely

An OpenAPI document can tell an agent how to call an endpoint. It cannot, by itself, tell the agent what a call is allowed to mean, when a human must intervene, or how the agent should behave after an ambiguous failure. Treating every documented operation as an agent action produces tools that look complete in a demo and become dangerous in ordinary use.

A useful action is smaller than an endpoint. It has a bounded purpose, inputs the agent can justify, a result the agent can act on, an approval decision tied to consequence, and an explicit plan for failures. Do that design work before you wire an operation into an agent. Retrofitting it after the first duplicate charge, accidental production change, or leaked token is a miserable way to learn the lesson.

## An operation is not yet an agent action

An HTTP endpoint, an OpenAPI operation, and an agent action answer different questions. People blur them because an OpenAPI operation gives a convenient starting point, but the distinctions determine whether automation stays understandable.

An endpoint is an address such as `/v1/deployments`. An operation adds an HTTP method, so `POST /v1/deployments` differs from `GET /v1/deployments`. An agent action adds the human and operational contract: what goal it pursues, which arguments it accepts, what effects it can produce, what evidence counts as success, and who must consent.

The OpenAPI Specification defines an Operation Object with fields such as `operationId`, `parameters`, `requestBody`, `responses`, and `security`. Use those fields as evidence, not as an automatic publication checklist. An operation with a fully specified schema can still be a terrible agent action if its description hides a production effect behind a harmless name.

Consider these two operations:

```text
GET  /v1/projects/{project_id}/builds/{build_id}
POST /v1/projects/{project_id}/builds/{build_id}/promote
```

The first one retrieves a record. The second one might change traffic, publish artifacts, or alter a release channel. The route only hints at that difference. The action design must state it plainly.

I have seen teams expose a generic `request` tool because their API already had a clean OpenAPI file. The agent then had permission to assemble arbitrary paths, query strings, and bodies. That is not an action catalog. It is remote code execution against a business API with better punctuation.

Expose an operation only after you can write one sentence in this form: "This action [does a specific thing] to [a bounded object] and returns [evidence of the resulting state]." If you cannot write that sentence without vague verbs such as "manage," "process," or "handle," the action remains too broad.

## Start with the consequence, not the request schema

Approval should follow the consequence of a call, not its HTTP verb or the apparent simplicity of its JSON body. A small `POST` can create an irreversible obligation. A verbose `GET` can expose private data. A `DELETE` might only remove a disposable draft, while a `PATCH` can revoke everyone else's access.

Before reviewing fields, describe the effect in terms a person responsible for the system would recognize. Ask what changes if the server executes the call twice, executes it for the wrong object, or executes it five minutes later than the agent intended. Those questions separate routine retrieval from an action that needs scrutiny.

I use four consequence classes when reviewing a candidate operation:

- Observation: fetches bounded information and produces no server-side change.
- Reversible change: creates, updates, or removes something with a documented and practical undo path.
- External commitment: sends a message, starts a paid job, publishes material, or changes a customer-facing state.
- Irreversible or broad change: deletes records permanently, rotates access, changes permissions, or affects many objects.

These classes are not a permission model. They force honest descriptions. A "create invoice" operation belongs in external commitment even if the request has only two fields. A "restart environment" operation can become broad change when one environment contains many services.

Do not infer safety from a method name. HTTP defines `GET` as safe in the protocol sense, meaning the client should not request state changes through it. That is a convention, not proof that a particular server obeys it. I have encountered diagnostic endpoints that refreshed caches, triggered report generation, and consumed scarce capacity when called repeatedly. Test the behavior you have, not the behavior the verb suggests.

Also separate an action's effect from the sensitivity of its result. Fetching an access token might be read-only, but returning that value to an agent defeats the point of controlling the call. Fetching a private customer record may need approval even if the API never changes a byte.

A good action card records both dimensions in plain language:

```text
Action: promote_preview_build
Effect: Changes one named preview build into the staging release channel.
Scope: One project and one build ID.
Result: Release ID, resulting channel, and server timestamp.
Human consent: Required for every call.
Retry: Never retry automatically unless the server accepts the same idempotency token.
```

That card often exposes missing API semantics before an agent writes a line of code. If nobody can say whether a retry is safe, the action is not ready.

## Inputs need boundaries an agent cannot talk its way around

An agent action needs a smaller input contract than the endpoint often accepts. OpenAPI schemas define types and structure, but an agent also needs constraints that prevent it from widening a task through creative arguments.

Take a deployment creation operation. The raw API may support many options for internal clients: environment, artifact reference, region, replica count, environment variables, feature switches, labels, and a free-form configuration object. Giving every field to an agent turns a simple request into an unreviewed administration surface.

Create an action with inputs that match the task. If the task is "deploy the build that passed tests to a preview environment," the agent might need only `project_id`, `build_id`, and a short `reason`. The executor can select the permitted environment and reject anything outside the action's scope.

This request shape makes the boundary concrete:

```json
{
  "project_id": "proj_4821",
  "build_id": "build_9017",
  "reason": "Preview requested after integration tests passed"
}
```

Do not add a `target_url`, arbitrary `headers`, a raw request body, or a general `options` object merely because the underlying endpoint supports them. Each escape hatch turns your carefully named action back into a generic client.

Use the OpenAPI fields that already carry useful limits. Set `additionalProperties: false` when an object should accept only named fields. Use `enum` for a genuinely small set of allowed values. Set length and pattern restrictions when identifiers have an established format. Mark fields required when the executor cannot safely infer them.

For example, this fragment rejects unreviewed configuration fields and makes the intended scope visible in the schema:

```yaml
DeployPreviewRequest:
  type: object
  additionalProperties: false
  required:
    - project_id
    - build_id
    - reason
  properties:
    project_id:
      type: string
      pattern: '^proj_[A-Za-z0-9]+$'
    build_id:
      type: string
      pattern: '^build_[A-Za-z0-9]+$'
    reason:
      type: string
      minLength: 8
      maxLength: 240
```

`additionalProperties: false` prevents a familiar failure: an agent learns from another API example that it can send `environment_variables`, places secrets or unsafe overrides inside that field, and the server quietly honors them. Rejecting the field gives the agent a useful error rather than a surprise deployment.

Schemas do not replace object-level authorization. A valid `project_id` can still refer to a project outside the task. The executor must check that the requested object sits within the allowed account, workspace, repository, or environment. Put that check near the action executor, where it cannot depend on an agent's explanation.

Free text needs special treatment. A reason field can help a reviewer, but it should never become an instruction channel for the executor. Store it as an audit annotation. Do not parse it for commands, resource selectors, or permission exceptions.

## Expected results must support the next decision

An agent needs an outcome it can reason about, not a raw HTTP response dumped into context. Returning every header, debug field, and nested object increases confusion and may disclose data the agent did not need to complete the task.

Define success in business terms before choosing response codes. For a deployment action, a useful result identifies the deployment, its state, and the location where the server will report later progress. For a record update, it identifies the record and confirms the fields that changed. For a deletion, it confirms the target and whether recovery remains possible.

A compact result for an asynchronous operation might look like this:

```json
{
  "status": "accepted",
  "deployment_id": "dep_2388",
  "project_id": "proj_4821",
  "build_id": "build_9017",
  "target": "preview",
  "operation_status": "queued"
}
```

That response says something precise: the server accepted work, but deployment has not completed. An agent should not report "deployed" after receiving it. It should either use a separate, read-only status action or tell the user that the operation is queued.

This is where many OpenAPI documents mislead agents. A `202 Accepted` response has a specific meaning: the server accepted the request for processing, and processing may not have started or completed. Treating `202` as success in the same sense as a completed `200` creates false claims in logs and user messages.

Separate transport outcome from action outcome. An HTTP `200` might wrap a domain failure such as `{"state":"rejected","reason":"build is not eligible"}`. Conversely, `409 Conflict` may tell the agent that the desired state already exists. The action wrapper should turn these cases into a small set of explicit states such as `completed`, `pending`, `already_in_desired_state`, `rejected`, and `unknown`.

Avoid a false promise of uniformity. Some APIs only return an opaque job ID, and that is fine if you expose a status action that can resolve it. The mistake is hiding the gap. State exactly what the first call establishes and what it does not establish.

Filter error detail before it returns to the agent. A server error may include internal URLs, authorization headers, stack traces, or another user's data. The agent needs a reason it can act on, such as "build ID does not belong to project ID," plus a safe correlation ID for a human to investigate. It does not need the upstream exception page.

## Approval belongs at the point of commitment

Ask for approval when the call can create a meaningful commitment, and make the approval screen describe the object and effect. Asking once for a vague bundle of future powers trains people to click through a warning they cannot evaluate.

Session approval and call approval solve different problems. Session approval says, "I recognize this agent process and allow it to use this action set while it runs." Call approval says, "I approve this specific consequential request now." Do not substitute one for the other.

An agent that can inspect build status may run for an hour without bothering anyone. An agent that promotes a build should present the project, build ID, release channel, and reason at the moment it asks for consent. A reviewer can judge that request. "Allow deployment tool" gives them almost nothing to judge.

Do not use an approval prompt as a replacement for input validation. If an action lets the agent specify an arbitrary destination or arbitrary permission scope, a reviewer must decipher a large, unstable payload under time pressure. Limit the inputs first. Then approval confirms a bounded action.

The right frequency depends on effect. Require each call for actions that publish, alter access, initiate an external payment, or touch broad production scope. Session consent can fit a group of read-only calls or narrow reversible changes, but only after the process identity and action catalog are visible to the reviewer.

Sallyport applies this distinction with session authorization for a newly seen agent process and optional approval on every use of a particular credential. Its vault gate also denies all actions while locked, so approval cannot turn a locked secret store into an accidental exception.

Do not make a person approve failures that software can prevent. If a build is not eligible for promotion, the executor should reject it before any approval request. Prompts are for legitimate choices, not for asking a tired reviewer to catch malformed state.

## A timeout creates an unknown state, not a retry instruction

A network timeout after a mutating request is the failure path that exposes sloppy agent action design. The agent sent the request, then lost the response. The server might have done nothing, completed the change, or still be processing it. The agent cannot discover the truth by assuming its preferred answer.

Walk through a familiar failure. An agent calls `POST /v1/invoices` with customer, amount, and a request timeout. The connection drops after the server commits the invoice but before the response arrives. The agent sees a timeout, retries with the same data, and the server creates a second invoice. The audit log now says the agent followed its retry policy, which is technically true and operationally useless.

An idempotency token fixes this only when the server actually implements it. The client generates a token once per intended action, sends it with the initial request, and sends the exact same token on a retry. The server must bind that token to the original request and return the original result, or a compatible conflict result, instead of repeating the effect.

```text
Idempotency-Key: act_01HZX7FQ2Z9K8M6R4T3V1W0Y
```

The action wrapper must keep this token out of the agent's improvisation. Generate it at execution time, persist it with the action attempt, and reuse it only for that attempt. An agent-provided token can collide, be reused across unrelated requests, or become another prompt injection surface.

If the API lacks documented idempotency semantics, do not retry a mutating operation automatically after a timeout. Return `unknown` with the action identifier and offer a read-only lookup action that can inspect the server's state. If no lookup exists, a human must investigate before anyone repeats the request. That answer feels inconvenient because it is inconvenient. Pretending certainty does not improve it.

OpenAPI can document a header parameter named `Idempotency-Key`, but documentation alone does not guarantee server behavior. Test it deliberately: send the same token and payload twice, then send the same token with a changed payload. The server should make the first pair converge and reject or clearly handle the changed request. If it silently performs both changes, the header is decoration.

Other failures need their own rules. Treat `401` and `403` as a stop condition, not a cue to hunt for another credential. Treat `429` as a wait condition only when the API communicates a retry delay or your action has a bounded backoff policy. Treat validation errors as feedback the agent can use only when the error identifies a permitted correction.

## Authentication does not grant an agent judgment

An OpenAPI `security` declaration describes how a client proves its identity to an API. It does not express whether an agent should invoke the operation, whether it may use a particular credential for a particular object, or whether a human needs to review the effect.

The Specification's Security Requirement Object associates an operation with named security schemes. A bearer scheme may tell a client to send an authorization header. Basic authentication may tell it to construct a credential header. That is transport authentication. Do not read more into it than it says.

Keep four questions separate:

- Who or what calls this action?
- Which credential does the executor use with the upstream API?
- Which objects and effects does that credential permit?
- Which action attempts does a person approve?

When teams merge these questions, they usually hand a token to the agent and call that authorization. The token then appears in tool output, shell history, debug logs, prompts, or a configuration file. Revoking it becomes a cleanup project instead of a single action.

The safer shape keeps the credential in the executor. The agent supplies bounded action inputs. The executor selects an eligible credential, injects it into the HTTP request, evaluates the response, and returns the filtered result. The agent never needs plaintext access to an API key to request an action.

For SSH, apply the same rule. An agent may need to request a command against a named host, but a generic command plus a broadly trusted private key is far wider authority than most tasks require. Restrict host identity, account, command form, and output handling according to the action's purpose.

Sallyport uses this execution model for HTTP calls and SSH commands: credentials remain in its encrypted vault and the agent receives the action result rather than the secret. That design helps only if you still expose narrow actions and choose approvals that fit their effects.

## The action description must state what the schema cannot

OpenAPI descriptions matter because agents read them as instructions, but prose should clarify boundaries rather than smuggle in a second, contradictory API contract. Put enforceable limits in schemas and executors. Use descriptions to explain intent, consequence, and conditions that a type system cannot express.

Name operations after the user's intended outcome. `getBuildStatus` says more than `getBuildById`; `createPreviewDeployment` says more than `postDeployment`. The name should not overpromise. If the server queues work, do not call the operation `deployBuild` unless its result distinguishes acceptance from completion.

Write descriptions with the details an agent would otherwise guess:

```yaml
operationId: createPreviewDeployment
summary: Queue one tested build for the preview environment
requestBody:
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/DeployPreviewRequest'
responses:
  '202':
    description: Request accepted. Deployment work may still be pending.
  '409':
    description: The build already has a preview deployment or cannot enter preview.
```

A summary is not enough for higher-consequence operations. Record the intended target, effect class, approval requirement, retry rule, and result states in action metadata that sits beside the OpenAPI document. You can use `x-` extensions if your tooling owns them, but label them clearly as private conventions. Standard OpenAPI parsers will ignore unknown extensions, which means the executor must enforce them rather than merely display them.

Do not rely on a description that says "use with caution." Caution is a human feeling, not an executable rule. Replace it with a limit: one project, preview only, no arbitrary environment variables, approval for each invocation, and no automatic retry after an unknown result.

Descriptions should also tell the agent when to refuse the action. A promotion operation can require a completed test run. A data export can require a customer-provided case reference. A deletion operation can require a prior lookup that confirms the object is a draft. Those preconditions reduce pointless prompts and make audit records easier to interpret.

## Test the action against a careless but capable operator

A happy-path test only proves that the API works when every assumption holds. Test an action as though a fast, capable operator has incomplete context, stale identifiers, and a tendency to repeat itself after an error. That is close enough to how autonomous agents fail to be useful.

Build a small test fixture with disposable objects and an account whose permissions match the intended executor. Then run the action through cases that challenge its boundaries:

- Send an unknown input field and confirm the executor rejects it.
- Request an object outside the allowed project or workspace.
- Deny approval and confirm that no upstream request occurs.
- Force a timeout after the server receives a mutating request.
- Return a response containing sensitive debug material and confirm filtering removes it.

Inspect more than the final API state. Review the prompt a human sees, the exact request the executor made, the result the agent received, and the audit record. A successful request can still fail the action contract if the prompt hid the target, the result claimed completion too early, or the log cannot distinguish a denied request from an upstream rejection.

For an action with every-call approval, test the ordering. The executor should validate static constraints and resolve enough safe context to show a meaningful request before it asks for consent. It should not send the request first and then seek approval. It should also avoid making a long chain of hidden read calls that expose more data than the final action needs.

Test revoked and expired credentials deliberately. The executor should fail closed, return a safe explanation, and avoid repeated calls with the same unusable credential. A retry loop against a rejected credential can fill logs, trigger rate limits, and make a simple access problem harder to diagnose.

Finally, test cancellation. If a user stops the agent while an upstream job runs, the record should say whether the request never left, reached the server, or entered an unknown state. Cancellation of the local agent process does not necessarily cancel a remote side effect.

## Publish fewer actions and make each one defensible

A small action catalog beats a generic API client because each action can carry a reasoned contract. Adding operations is easy. Maintaining truthful result semantics, object boundaries, approval prompts, and failure behavior is where the work lives.

Start with an operation that retrieves a bounded status record. Give it a name that states the object, restrict identifiers to the intended scope, and return only the fields the agent needs. Then add one reversible action and force yourself to write its retry and approval rules before you implement it.

Do not promote an operation into an agent action because an OpenAPI generator can expose it in an afternoon. Promote it when you can explain what happens after a timeout, what the human approves, what the agent sees, and how you will prove later which request occurred. If any of those answers depends on "the agent will probably do the sensible thing," keep the endpoint out of the catalog.
