Skip to content

Let an openapi block drop parts of a result - #104

Open
haruotsu wants to merge 2 commits into
mainfrom
openapi-omit
Open

haruotsu wants to merge 2 commits into
mainfrom
openapi-omit

Conversation

@haruotsu

@haruotsu haruotsu commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Why

params and only constrain what the model writes to an operation, but nothing constrained what it reads back. A published record can carry far more than an agent needs. Zendesk's published Support API description is a public example: a ticket (ShowTicket) carries its custom field values twice, as custom_fields and as fields, which the description calls an alias. Every extra part of a result is something the model may misread or quote, and the only way to keep it out was to replace the block with a Python tool.

What changes

  • omit on an openapi: block, keyed by operationId like params, only and describe. Each entry is a dotted path, or an object with path and where_empty:

    omit:
      ListSearchResults: [results.fields]
      ShowTicket:
        - ticket.fields
        - path: ticket.custom_fields
          where_empty: value
  • Runtime. The tool drops the paths from the JSON result before returning it. Every array on the way is stepped through element by element, so results.fields drops the key from each search result. A result that does not carry the path (an error body, a record in another shape, a 204) is left as it came; nothing raises.

  • where_empty keeps the array at path and drops only the elements whose key holds nothing: null, "", [], {}, or no such key at all. false and 0 are values and stay, since they are meaningful answers in general; an element that is not an object has no key to judge by and stays too. The fields that do hold a value are often exactly what a lookup needs, so dropping the whole array is not enough.

  • validate refuses an omit entry for an operation the block does not select, as it does for params. It also holds each path against the operation's 2xx JSON response schemas, but only refuses what they rule out for certain: a path through a value declared as a scalar, a name an object with additionalProperties: false does not declare, where_empty on a value declared as something other than an array, or where_empty on an array whose elements are declared as scalars. A name the schema merely leaves undeclared passes: published response schemas routinely under-declare. In the same public Zendesk description, SearchResultObject declares nine shared properties for results it says may be tickets, users, groups, or organizations, so refusing undeclared names would reject the very paths omit exists for. With several success responses, a path is refused only when all of them rule it out; $ref and one level of allOf are followed, and oneOf/anyOf are never chosen between. An OpenAPI 3.1 type list such as ["object", "null"] is read as its one non-null type; a list of several is not chosen between either.

  • Redaction is unchanged. Dropping runs inside the tool and can only remove keys or array elements, never rename or add anything; the after_tool_callback still redacts whatever is left, as for every other tool.

  • Write blocks may declare omit as well; nothing about it is specific to reads.

  • Schema. omit maps operation ids to non-empty, unique lists. A path is dot-separated with no empty segment. The object form requires both path and where_empty, and where_empty is a single key (no dots).

  • README documents omit, what counts as empty, and what validate does and does not refuse.

Not part of this: replacing field ids in a result with their names. That would take another request per result; an instruction can carry the mapping.

Compatibility

Additive. Declarations without omit build the same tools. An agent that uses omit has to be archived with a gete that includes this change, since the runtime inside the archive is what applies it.

Testing

  • Runtime: a plain path drops the key; arrays on the path are walked element by element; results without the path (error body, null, a scalar where an object was expected, a top-level list, plain text) come back unchanged; where_empty drops null, "", [], {} and missing keys while keeping false, 0, values and non-object elements; a where_empty path that holds no array is left alone; omit on one operation leaves another's result whole; through a built agent, the after_tool_callback still redacts a key the omitted result carries.
  • Validate: omit for an unselected operation; an undeclared name passes; a path through a declared scalar (reached through $ref), past a closed object, or below an array of scalars is refused; arrays are followed through items; where_empty on a declared non-array or on an array of scalars is refused; no JSON success response, or a second 2xx response that may hold the path, lets it pass; an array that is its own items (a YAML alias can build one) ends the walk instead of looping; a nullable 3.1 type list is followed as its other type, and a list of several types rules nothing out.
  • Schema: both entry forms pass; an empty list, a bare string, empty segments, an object without where_empty, a dotted where_empty and an unknown key are refused.
  • pytest, ruff check, ruff format --check and mypy pass locally.

params and only constrain what the model writes, but nothing constrained
what it reads back. A published record can carry far more than an agent
needs: a helpdesk ticket may list its custom fields twice, under two
names. Each extra part is something the model may misread or quote, and
the only way around it was a Python tool.

omit names dotted paths to drop, by operationId, stepping through every
array on the way. The where_empty form keeps an array and drops only the
elements whose key holds nothing, because the fields that do hold a value
are often what a lookup needs. false and 0 count as values.

validate refuses only a path the success responses rule out: one through
a declared scalar, or past an object that allows no other properties.
Published response schemas commonly leave out fields the service sends,
so an undeclared name proves nothing; refusing it would reject the very
paths omit exists for.

Dropping runs inside the tool, so the redaction callback still sees, and
masks, whatever is left.

🤖 Generated with Claude Code
OpenAPI 3.1 writes a nullable value as a list of types, such as
["object", "null"]. The omit check looked the type up in a set, so a path
through such a value stopped validate with a TypeError, and where_empty on
a nullable array was refused as if it named no array at all.

A null holds no properties, so whenever there is a value, it is what the
one other type says; that type is what the check now reads. A list of
several other types is left unchosen, as oneOf is.

The messages also put "an" before a type that starts with a vowel.

🤖 Generated with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant