# Integration-owned API clients

This increment supports the subset below for Swagger 2.0, OpenAPI 3.0 and OpenAPI 3.1
JSON descriptions. Supported methods are GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS.
TRACE fails compilation because it is not supported by the device HTTP capability.
Nested object parameter encodings, reserved expansion, binary bodies, remote
references, OAuth flows, additional unsupported constraints and body/response amendments
are deferred; unsupported selected contracts fail compilation. Standalone JSON Schema
is not an API description, and SOAP/WSDL is a separate future project.

Package the API description and its API definition together in one named `api-client`
under `api-clients`. The client belongs to the integration version; it is not seeded
into the tenant external-schema registry and does not create a shared schema resource.
```xml
<integration id="com.example.people" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"swagger":"2.0","produces":["application/json"],"paths":{"/people/{id}":{"get":{"operationId":"getPerson","parameters":[{"name":"id","in":"path","required":true,"type":"integer"}],"responses":{"200":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="49a439854084d1c5f2beef5eb00249a6fd0dd54ce28a120963091f6340f264db"><operation id="getPerson"/></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Read person"><source><![CDATA[
    local client = ctx.api.get("people"):init(ctx.device.get("service"))
    local ok, response = client:getPerson({path={id=1}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

`schema` holds the exact Swagger 2.0, OpenAPI 3.0 or OpenAPI 3.1 JSON bytes as XML text or CDATA. `definition`
holds the DataXML API contract, including selected operations, typed parameter overrides,
and any auth-provider source. Its `name` must match the enclosing client and its
`schema-sha256` must hash the decoded schema text exactly. Changing whitespace in the
schema therefore requires updating the pin. The compiler supports scalar path/query/header parameters (including enums), JSON request bodies,
exact and default response statuses, OpenAPI status ranges and empty responses. Declared HTTP error responses
return typed status/body values; uncovered statuses produce a recoverable failure.
Exact statuses take precedence over ranges, which take precedence over `default`.
Each response keeps a distinct set of status literals so Lua can narrow its body by status.
Repeated generated response shapes share type declarations to keep exports compact.

Operation definitions can add, replace or remove typed path, query and header parameters
with `<parameter location="..." name="..." action="..."/>`. The GraphQL equivalent is
`APIClientOperationInput.parameters`. Add/replace requires `type` (`string`, `integer`,
`number`, `boolean` or `array`); arrays additionally require `item-type` (GraphQL
`itemType`). `required` controls presence. `collection-format` (GraphQL `collectionFormat`)
selects the existing device serialization: `csv` by default, or supported `ssv`, `tsv`,
`pipes` and `multi` modes. Location restrictions still apply.

Amendments operate on resolved parameters, including path-level inheritance and local
parameter references. Header matching ignores case. `add` fails if the parameter exists;
`replace` and `remove` fail if it does not. A remove has no type or serialization fields.
The earlier `<query>` / GraphQL `queries` form remains supported for scalar queries,
and duplicates across both forms are rejected. Path parameters must be required and
match an existing template placeholder. These declarations cannot change the route,
introduce a URL, store values, or take ownership of authentication/transport headers.
All changes remain pinned to the original schema hash and affect generated types before
scripts are checked. This example adds an undocumented header and repeated query array:

```xml
<integration id="com.example.parameters" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"swagger":"2.0","produces":["application/json"],"paths":{"/people/{id}":{"get":{"operationId":"getPerson","parameters":[{"name":"id","in":"path","required":true,"type":"integer"}],"responses":{"200":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="49a439854084d1c5f2beef5eb00249a6fd0dd54ce28a120963091f6340f264db"><operation id="getPerson"><parameter location="header" name="X-Mode" action="add" type="string" required="true"/><parameter location="query" name="status" action="add" type="array" item-type="integer" collection-format="multi"/></operation><authentication><scheme name="access" kind="bearer"/></authentication></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Read person">
    <input-schema><schema type="object" xmlns="urn:cococo:json-schema:1">
      <property name="token" type="string"/><required name="token"/>
    </schema></input-schema>
    <source><![CDATA[
    local input = ...
    local client = ctx.api.get("people"):init(ctx.device.get("service"), {access={value=input.token}})
    local ok, response = client:getPerson({path={id=1},headers={["X-Mode"]="fast"},query={status={1,2}}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

Header parameters use `headers`, for example `{headers={["X-Mode"]="fast"}}`.
Transport-controlled and authentication headers cannot be supplied as ordinary parameters.
Header control characters and parameters that conflict with declared auth are rejected.
Explicit default scalar styles are accepted. Scalar-element arrays support comma-delimited
headers and query values: Swagger `collectionFormat="csv"`, or OpenAPI `simple` headers
and `form` query parameters with `explode=false`. Query arrays also support Swagger
`ssv`, `tsv` and `pipes`, and OpenAPI `spaceDelimited` and `pipeDelimited` with
`explode=false`. For example, `{query={tags={"one","two"}}}` produces a single query
value. Empty arrays produce an empty value. Empty elements or elements containing the
chosen separator fail before I/O because they cannot be represented unambiguously.
Repeated query keys are supported with Swagger `collectionFormat="multi"` and OpenAPI
`form` with `explode=true` (the default for form parameters). Array order and duplicate
values are preserved, and each value is encoded separately by the device. For example,
`{query={status={"OrderEntered","AwaitingGoods"}}}` becomes two `status` query values.
Empty arrays produce one empty value. Repeated values may contain commas or other
separators; these are encoded as part of the individual value.
Path arrays support Swagger `csv` and OpenAPI `simple` (either `explode` setting).
Elements are percent-encoded separately, with structural commas preserved between them.
Embedded commas become `%2C`, and empty elements are preserved. The same simple
path-array type is available through parameter amendments. OpenAPI 3 also supports
scalar/array `label` and `matrix` path styles declared in the uploaded schema. Label
arrays expand as `.red,green` or, with `explode=true`, `.red.green`; matrix arrays
expand as `;id=red,green` or `;id=red;id=green`. Empty lists have no expansion.
These follow RFC 6570. Reserved query expansion remains unsupported.
OpenAPI 3 flat object parameters retain their declared property types and required keys.
Path objects support `simple`, `label` and `matrix`; headers support `simple`; queries
support `form`, `spaceDelimited`, `pipeDelimited` and `deepObject`. For example,
`{query={filter={name="Leia",age=19}}}` with `deepObject` and `explode=true` emits
`filter[age]=19` and `filter[name]=Leia`, encoded by the existing device layer.
With `form` and `explode=true`, it emits the property names directly. Expansion cannot
replace other parameters or authentication keys. Space/pipe forms require `explode=false`.
Declared nested or nullable members are unsupported; unconstrained extra values must be
non-null scalars at runtime. Empty or bracketed deep-object member names and ambiguous delimiters
in compact query/header values fail before I/O. Empty query/path objects have no expansion.
Swagger 2 does not support object parameters. Objects/styles are read from uploaded
schemas; the parameter amendment vocabulary still authors scalar and array shapes.
Serialization settings are part of integration resource binding compatibility.
Unsupported schema and serialization features produce compilation errors. Standalone
JSON Schema and SOAP are not API-client inputs.

Numeric format annotations retain the underlying Lua numeric types: `int32` and `int64`
use `integer`; `float` and `double` use `number`. Integers remain integers through JSON
encoding and decoding, including values beyond double-precision's exact integer range.
Formats alone do not add width or range checks. Explicit `minimum`, `maximum`,
`exclusiveMinimum` and `exclusiveMaximum` constrain requests and responses, including
path, query, header and array-item parameters. Swagger 2 and OpenAPI 3.0 use boolean
exclusive flags alongside their bounds; OpenAPI 3.1 uses numeric exclusive bounds.
`minLength` and `maxLength` count Unicode code points, independently of UTF-8 byte size.
These value constraints retain ordinary Lua numeric/string types and are checked at
runtime before device I/O and when decoding responses. They also participate in binding
compatibility. `multipleOf` accepts a strictly positive numeric step and validates exact
decimal divisibility, including negative values and zero. It does not use a floating-point
tolerance: values already rounded by Lua arithmetic are checked as the resulting numeric
value. Patterns remain unsupported.
Array `minItems`, `maxItems` and `uniqueItems`, plus object `minProperties` and
`maxProperties`, are enforced on requests and responses. Counts include additional
properties and properties whose value is JSON null. Uniqueness compares JSON values:
object key order does not matter, array order does, and booleans are distinct from
numbers. These refinements retain the ordinary Lua array/object types and affect
binding compatibility. Use matching explicit array/object types in the schema.

Empty Lua request tables become `[]` when the schema unambiguously requires an array,
including nested arrays. Open JSON values and ambiguous array/object unions stay objects;
no defaults or missing fields are inserted. Nullable arrays retain JSON null separately
from empty arrays. Uniqueness has bounded visit/depth and payload work in addition to
the existing request/response size limits.

All string-valued formats, including service-specific names, are annotations. They
never change the declared primitive type or validate syntax; for example, a string
with `format: date-time` stays a string without timestamp validation. Malformed
non-string format metadata fails compilation.

`format: byte` is a string in all three dialects. OpenAPI 3.1 also accepts string-valued
`contentEncoding` and `contentMediaType` annotations. These preserve the supplied string;
they do not automatically encode, decode, parse embedded content or assert base64 validity.
Scripts choose when to apply a codec. Declared types, enums and length constraints still
apply. The content annotations in older dialects, malformed annotation values and
`contentSchema` produce compilation errors.

String, integer and boolean enums produce literal union types and reject values outside
the declaration in requests and responses. Required fields and nullable values are checked
independently on the JSON data, including nested objects and arrays. Use `json.null()`
for an explicit null; Lua `nil` omits a field. Nullable values project as `T | JSONNull`,
while optional fields additionally allow `nil`. A required nullable field must be supplied,
for example `{body={name=json.null()}}`; omission is rejected statically and before I/O.
Responses preserve null fields and null array elements. `json.isnull(value)` checks their
identity and narrows nullable objects and arrays in guarded branches.
OpenAPI 3.1 null-only schemas (`type: "null"` or `type: ["null"]`) project as
`JSONNull` and accept only explicit nulls. Empty Lua tables currently infer as `any`,
so the runtime also checks null identity and rejects forged values before device I/O.
Use `json.array({})` for an explicit empty JSON array. An ordinary `{}` also becomes an
array when its request schema unambiguously requires one; ambiguous/open JSON stays an object.
These rules also apply to the scripted provider's `login()` request and response.
API inputs reject cycles and are limited to 20,000 nested values per argument and depth below 32.


Objects with schema-valued `additionalProperties` and no named properties become typed
string-keyed dictionaries. Their values are validated for requests and responses, and empty maps
remain JSON objects. `additionalProperties: false` rejects unknown response fields.
Objects that also declare named properties preserve those fields and their required/optional
status. Other keys use the additional-value schema. A known field may have a different
type from the additional values; its declared schema takes precedence. Reads of additional
keys are optional because a key may be absent. Omitted/true/empty-schema additional values
use recursive JSON-only types; functions and device handles remain invalid.

Request and response types differ where the schema marks fields `readOnly` or
`writeOnly` (OpenAPI 3). Read-only fields are excluded from requests; write-only
fields are excluded from responses. Required flags apply only on the relevant side.
The runtime rejects excluded fields, including a response that leaks a write-only value.
For local schema references, Swagger 2 and OpenAPI 3.0 ignore adjacent access annotations
and use the target's annotations. OpenAPI 3.1 combines adjacent annotations with those
on the target; `false` cannot remove a target restriction. Conflicting read-only and
write-only restrictions fail compilation. Supported assertion siblings apply in OpenAPI 3.1;
older dialects reject them. See adjacent schema assertions below.

`allOf` supports compatible structural intersections. OpenAPI `anyOf` and `oneOf`
produce union types; the runtime additionally enforces exactly one matching branch for
`oneOf`. Composition requires 1–32 branches and supported adjacent assertions intersect
with those branches. Unsupported intersections and conflicting access annotations produce diagnostics.
Expanded type and validation trees are bounded to 20,000 nodes and a 1 MiB size estimate
per projection; excessively deep or expanding schemas are rejected.

Recursive local schema references use finite named types. Self-references, mutually
recursive models and recursive array elements retain their field types and directional
access rules, including in provider login scripts. References never fetch external
schemas. Alias cycles without a concrete schema fail compilation. Runtime validation
shares a 20,000-visit budget across branches and permits at most 128 schema steps;
exhaustion fails validation rather than accepting a partially checked value. The JSON
value depth and size limits still apply.

Lua obtains the factory with `ctx.api.get("people")` and initializes it with an
explicit `ctx.device.get("service")` handle. The installation must grant that device
through a declared resource. Schema base URLs do not create outbound routes. Credentials
are supplied at initialization from installation/runtime inputs; variable declarations
contain types, never credential values. Auth-provider login uses the same device.

Declared Basic, Bearer (OpenAPI) and header/query API-key security requirements infer
initialization inputs. A scheme called `login` using Basic authentication requires
`init(device, {login={username=user, password=password}})`. A key or Bearer scheme
called `token` requires `init(device, {token={value=tokenValue}})`. These variables come
from runtime inputs. No auth argument is needed for an API without security requirements.
Operation-level `security: []` removes inherited authentication. Combined requirements
need each named credential. Schema alternatives require an explicit API policy; OAuth
flows and cookie keys remain unsupported. A scripted Basic provider can supply Basic
credentials; its login operation must itself be public.

Declare `<authentication>` in the API definition to replace schema authentication for
its selected operations. Each `<scheme>` has a credential-slot `name` and a `kind`:
`basic`, `bearer`, `headerKey` or `queryKey`. API-key policies additionally require `key`,
the header or query name. Schemes in the same policy are all required. No credential
values or endpoint settings are accepted. This also resolves ambiguous schema alternatives.

An `<authentication>` inside an `<operation>` takes precedence over the API policy.
An empty `<authentication/>` explicitly makes that operation public, including when a
scripted provider is configured. If all selected operations are public, initialization
requires only the device and never invokes the provider. Omit the element to inherit. For a scripted provider
with a global Basic policy, declare its login operation with `<authentication/>`.

GraphQL uses the equivalent `authentication` list on `APIClientInput` and
`APIClientOperationInput`: omitted inherits, `[]` explicitly declares no authentication.
Every item has `name`, `kind` and optional `key`. Policy changes alter binding compatibility
and provider-cache identity; credentials remain arguments to typed `init(device, ...)`.
This example receives a token as an action input:

```xml
<integration id="com.example.authpolicy" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"swagger":"2.0","produces":["application/json"],"paths":{"/people/{id}":{"get":{"operationId":"getPerson","parameters":[{"name":"id","in":"path","required":true,"type":"integer"}],"responses":{"200":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="49a439854084d1c5f2beef5eb00249a6fd0dd54ce28a120963091f6340f264db"><operation id="getPerson"/><authentication><scheme name="access" kind="bearer"/></authentication></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Read person">
    <input-schema><schema type="object" xmlns="urn:cococo:json-schema:1">
      <property name="token" type="string"/><required name="token"/>
    </schema></input-schema>
    <source><![CDATA[
    local input = ...
    local client = ctx.api.get("people"):init(ctx.device.get("service"), {access={value=input.token}})
    local ok, response = client:getPerson({path={id=1}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

A client declaration matching an `apiclient` resource requirement instead describes
the contract expected from a user-selected tenant client. It is not a fallback grant:
the instance must bind that resource, and its operation/auth-variable signatures must
match, including wire constraints and operation authentication. Other client declarations are integration-owned. Normal exposes permitted tenant
clients, Integration exposes its grants and owned clients, and Pure exposes neither.
Tenant-authored CustomApp versions, Workspace handlers, MCP tools and Lua discovery,
IaC Lua validation and Normal workflow-output inference use the authorized tenant catalog.
Downloadable Normal IaC testing contracts carry the same declarations; their ETag changes
with the catalog and the caller’s read grants. Authors with different grants can receive
different ETags for the same reference; account for this when sharing IaC lockfiles.
Workspace publication rechecks the authorized catalog without embedding that catalog
in the saved artifact.

Generic Integration-role editor requests have no instance bindings and therefore expose
no API clients; the integration authoring compiler supplies the declared contracts.
API declarations packaged in an integration belong to its actor and Actions. They do
not grant API access to embedded CustomApp server APIs or inline workflow scripts, which
are separate Normal-profile assets. Portable embedded assets cannot assume a tenant API
catalog exists; keep packaged API operations in the Integration Actions. Direct tenant API
calls can be authored on tenant CustomApp versions with the authorized catalog.

A tenant may persist up to 256 API definitions, including failed drafts. Updates and
deletions remain available at the limit. An old definition that no longer compiles
is omitted from the runtime catalog so unrelated scripts remain usable. Delete dependent
API definitions before deleting their uploaded external schema.

The existing `sourceXML @iac_artifact(language: XML, validators: ["integration-source"])`
annotation covers the entire pair. `nativeProjection` remains computed. IaC uses the
same native compiler as source validation, create, update and publication. The generated
`integration_document` model includes `apiClients`; `schema` is a string to preserve
pinned bytes, and `definition` is XML text. No extra IaC resource or YAML authoring
surface is introduced. Exact XML export includes the pair; revision preconditions and
published-version immutability apply to the whole document.

Limits are 256 clients per document, 1 MiB per schema, 256 KiB per API definition,
and the existing 2 MiB integration source/projection budgets. Invalid schemas, pins,
names, overrides or provider scripts reject the document before persistence. Diagnostics
identify the client and the `schema` or `definition` field.

This deliberately invalid example has a mismatched schema pin and is rejected before
any draft is written:

```xml
<integration id="com.example.people" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"swagger":"2.0","produces":["application/json"],"paths":{"/people/{id}":{"get":{"operationId":"getPerson","parameters":[{"name":"id","in":"path","required":true,"type":"integer"}],"responses":{"200":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="0000000000000000000000000000000000000000000000000000000000000000"><operation id="getPerson"/></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Read person"><source><![CDATA[
    local client = ctx.api.get("people"):init(ctx.device.get("service"))
    local ok, response = client:getPerson({path={id=1}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

OpenAPI 3.0 and 3.1 use the same device-bound Lua API. These examples describe the
same JSON POST and typed 201 response:

```xml
<integration id="com.example.people.openapi30" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"openapi":"3.0.3","info":{"title":"People","version":"1.0"},"components":{"schemas":{"Person":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}},"paths":{"/people":{"post":{"operationId":"createPerson","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Person"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Person"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="16b3fb30e7117deddd35f9190e480ed61c333821265e417f65501d9600bf7862"><operation id="createPerson"/></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Create person"><source><![CDATA[
    local client = ctx.api.get("people"):init(ctx.device.get("service"))
    local ok, response = client:createPerson({body={name="Leia"}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

```xml
<integration id="com.example.people.openapi31" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"openapi":"3.1.0","info":{"title":"People","version":"1.0"},"components":{"schemas":{"Person":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}},"paths":{"/people":{"post":{"operationId":"createPerson","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Person"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Person"}}}}}}}}}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="f51c7332a928f0107785563d17e07328f0207fd960c293cf770a3810a3d1b135"><operation id="createPerson"/></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Create person"><source><![CDATA[
    local client = ctx.api.get("people"):init(ctx.device.get("service"))
    local ok, response = client:createPerson({body={name="Leia"}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```


Operation `id` is the Lua method name. Omit selectors to use the same operation ID
from the uploaded schema. Use `source-id` to give a uniquely named source operation
a different Lua name. For duplicate or missing operation IDs, supply both `path` and
`method` to select one existing operation; optional `source-id` verifies its original
ID too. These fields select a route already in the pinned upload and cannot create or
rewrite a destination. Duplicate names, ambiguous selections, mismatched source IDs
and missing routes fail compilation.

GraphQL uses `APIClientOperationInput.sourceId`, `path` and `method`. Parameter and auth
overrides remain children of the selected operation and apply only to that Lua method.
A provider's `login-operation` uses this same declared name. This example disambiguates
two source operations while keeping their original paths:

```xml
<integration id="com.example.operation-selection" version="1.0.0" sdk-version="2.0">
  <resources><resource id="service" type="protocol/http"/></resources>
  <api-clients>
    <api-client name="people">
      <schema><![CDATA[{"paths":{"/people/{id}":{"get":{"operationId":"getPerson","parameters":[{"in":"path","name":"id","required":true,"type":"integer"}],"responses":{"200":{"schema":{"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}}}}},"/robots/{id}":{"get":{"operationId":"getPerson","parameters":[{"in":"path","name":"id","required":true,"type":"integer"}],"responses":{"200":{"schema":{"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}}}}}},"produces":["application/json"],"swagger":"2.0"}]]></schema>
      <definition><![CDATA[<api-client name="people" schema-sha256="cd89cf4faa4c872892c60d85c5be393c083ada9dc8706146a5e6f5b3fc183e56"><operation id="getHuman" source-id="getPerson" path="/people/{id}" method="GET"><parameter location="header" name="X-Mode" action="add" type="string" required="true"/><parameter location="query" name="status" action="add" type="array" item-type="integer" collection-format="multi"/></operation><operation id="getRobot" source-id="getPerson" path="/robots/{id}" method="GET"/><authentication><scheme name="access" kind="bearer"/></authentication></api-client>]]></definition>
    </api-client>
  </api-clients>
  <actor><actions><action name="person" label="Read person">
    <input-schema><schema type="object" xmlns="urn:cococo:json-schema:1">
      <property name="token" type="string"/><required name="token"/>
    </schema></input-schema>
    <source><![CDATA[
    local input = ...
    local client = ctx.api.get("people"):init(ctx.device.get("service"), {access={value=input.token}})
    local ok, response = client:getHuman({path={id=1},headers={["X-Mode"]="fast"},query={status={1,2}}})
    if not ok then error(response) end
    return response.body.name
  ]]></source></action></actions></actor>
</integration>
```

### Local references

Uploaded schemas may use local JSON Pointer references, including array indices and
escaped or empty property names. Percent encoding is decoded once, then `~0` and `~1`
represent literal tilde and slash. Array indices must be canonical nonnegative integers
within the array. Malformed pointers and missing targets fail compilation. Named anchors
and references to other files or URLs remain unsupported; compilation never fetches them.

### Adjacent schema assertions

OpenAPI 3.1 schemas may place supported assertions beside `$ref`; both the target and
siblings are enforced. Swagger 2/OpenAPI 3.0 assertion siblings remain rejected. Supported
constraints may also accompany composition keywords. They intersect with every applicable
composition result, including the exact-one requirement of `oneOf`.

A constraint-only schema (for example `maxLength: 40`) refines matching JSON values without
asserting their type. This is useful beside a reference that already supplies a type.
The generated Lua type retains the target's structural projection; value constraints are
checked before device I/O and on responses. Typeless structural schemas and arbitrary
union intersections remain unsupported and fail compilation. This composition support
applies to body schemas and nested values; parameter serialization still rejects
composition and reference assertion siblings.

### Body media types

Clients support JSON (`application/json` and application media types ending in `+json`)
and UTF-8 `text/*`. Text bodies require string schemas and use raw text on the wire;
JSON bodies retain their structured Lua types. Empty text is a valid body. OpenAPI
content maps currently require one supported media type with an explicit schema.
Swagger selects application/json when offered, otherwise requires one supported type.

Response Content-Type, when present, must agree with the selected schema media type.
UTF-8 charset declarations are accepted; other charsets and conflicting headers fail.
A missing header uses the single declared schema. Both body directions retain the
1 MiB limit and schema validation. Arbitrary binary and multiple-media
selection remain unsupported. Form and multipart requests are described below.

### URL-encoded form requests

Swagger formData and OpenAPI 3 URL-encoded request bodies use the same typed `body` table
as JSON clients. Scalars, primitive arrays and flat objects use the existing device HTTP
call. OpenAPI arrays default to repeated fields; Swagger uses collectionFormat. Explicit
form, space/pipe-delimited array and supported flat-object query styles are available.
Delimited arrays reject values containing their separator or empty members.

OpenAPI 3.0 object fields default to form expansion; OpenAPI 3.1 defaults them to JSON-valued
fields unless style/explode/allowReserved is explicit. Both support explicit JSON contentType
for nested or nullable form fields. Headers in form encoding metadata are ignored. UTF-8
values are escaped, with spaces encoded as `+`; empty strings and empty repeated arrays
emit `name=`. Form responses and non-JSON contentType overrides are unsupported.

Required fields and value constraints are checked before sending. Expanded object keys
cannot collide with declared fields. The encoded body must fit within 1 MiB. Nullable
style-serialized fields and allowReserved=true remain unsupported. Binary transport requires a byte-preserving device contract.

### Text and JSON multipart requests

Multipart/form-data accepts typed text/number/boolean fields, JSON object parts and arrays
of supported parts through the supplied device. Swagger uses formData and collectionFormat;
OpenAPI 3 uses its body schema and optional part contentType. Multipart is not a file handle
or a new HTTP capability. Binary/file parts remain unsupported by the current string body
contract; base64 strings can still be supplied as ordinary JSON/text data when declared that way.

Part names are quoted safely and boundaries cannot be injected through payload text. All
headers and delimiters count toward the 1 MiB body limit. Arbitrary per-part headers and
filenames are unsupported, and no defaults are injected. OpenAPI 3.1 style/explode/allowReserved
multipart overrides and multipart responses are explicitly rejected. Untyped parts require
an explicit JSON contentType; additional undeclared parts need a concrete value schema.
Empty repeated arrays fail before I/O because zero parts cannot preserve field presence.
OpenAPI 3.1 contentEncoding/contentMediaType on a part requires an explicit supported
contentType; format remains an annotation.