AIP-31: HTTP.md — agenthttp/v1 (HTTP driver specialisation)
A markdown + frontmatter format for declaring an HTTP-API driver — its base URL, per-tool endpoint/method/body templates, header bindings, response extraction, and (when needed) streaming hints. Specialises AIP-30 DRIVER for the `kind: http` case. Most third-party APIs (OpenAI, Stripe, Replicate, Anthropic) wrap as HTTP drivers.
| Field | Value |
|---|---|
| AIP | 31 |
| Title | HTTP.md — agenthttp/v1 (HTTP driver specialisation) |
| Status | Draft |
| Type | Schema |
| Family | Driver |
| Driver kind | http |
| Specialises | AIP-30 DRIVER.md |
| Requires | AIP-14 (TOOL), AIP-16 (IO), AIP-19 (SECRETS), AIP-30 (DRIVER) |
| Resources | ./resources/aip-31 — HTTP.schema.json, ADAPTER.md, EXAMPLES.md, SKILL.md |
Abstract
HTTP.md is the kind: http specialisation of
AIP-30 DRIVER. It packages an HTTP-API integration —
the base URL, headers, per-tool endpoint + method + body template +
response extraction. Most third-party APIs (OpenAI, Stripe, Replicate,
Anthropic, GitHub, Slack, Linear, Notion, …) wrap as HTTP drivers;
this AIP is the canonical pattern.
The frontmatter inherits everything from DRIVER (identity, kind,
implements, auth, install (typically empty), version_check (typically
empty), network, region, policy_tags, cost_override, runner, …) and
adds HTTP-specific fields under per-tool metadata.http.… blocks plus
optional top-level base_url and default_headers.
The format pairs with the standard defineDriver({ kind: "http", ... })
signature; no separate defineHttp is required (it's sugar at most).
Motivation
HTTP integrations are the single largest class of agent tools: every LLM API, every payment processor, every SaaS productivity tool, every search engine, every weather/maps/news data source. Without a shared HTTP-driver format, every integration ships its own auth/parse/retry boilerplate, often with subtle incompatibilities.
Five problems disappear when HTTP drivers share a format:
-
Per-API boilerplate. Authentication header construction, request body templating, response field extraction — every integration reinvents these. A shared format declares them in one place.
-
Auth inconsistency. Bearer tokens, API-key-in-header, API-key-in-query, OAuth bearer with refresh — five variants today, each handled bespokely. The driver's
auth.loginand the HTTP subtype'sdefault_headerscover the common cases declaratively. -
Streaming chaos. Some APIs stream via SSE, some via chunked JSON, some via WebSockets. v1 declares unary HTTP only; streaming is explicit
metadata.http.streamingand constrained. -
Response extraction. Most APIs return responses with envelope keys (
{ data: {...} },{ result: {...} }); the contract wants the inner shape.response_extractdeclares the path-to-data once. -
Body templating. API request bodies are often only loosely related to contract inputs — an LLM API might want
{ model: "...", messages: [...], max_tokens: ... }from a contract input of{ prompt }.body_templatedoes the mapping.
Design principles
-
Inherit, don't repeat. Identity, auth, network, region, policy, region, cost-override all live on DRIVER. HTTP only adds what's HTTP-specific: endpoint, method, body, headers, response extraction, streaming hints.
-
Declarative over imperative. Body templates use
${input.X}substitution (no shell, no JS). Response extraction uses JSONPath-lite ($.data[0].url). Custom logic when needed lives in the driver's optional entry, not in the manifest. -
One base_url per driver. A single HTTP driver serves one API host. Cross-host drivers (multi-region failover) are two drivers, not one with conditional URL.
-
Standard auth shapes. Bearer-in-header is default. API-key-in- header, API-key-in-query, custom-header are declared via
default_headersandauth.state.envmapping. OAuth flows live in the DRIVER'sauth.loginwith custom callback handling in the entry. -
Unary v1, streaming v2. v1 contracts assume request → response. Streaming responses (SSE, chunked JSON, NDJSON) are declared via
metadata.http.streamingbut constrained — full streaming semantics defer to a future revision.
Specification
File location
HTTP drivers live in a single folder, separate from the TOOL.md contracts they implement:
.drivers/
openai-images-http/
DRIVER.md ← this AIP, kind: http
driver.ts ← optional entry (custom auth flow, response parser)
SECRETS.md ← AIP-19 inventory (OPENAI_API_KEY)
README.mdConvention: id ends with -http to make the kind self-evident in
catalogs. The folder name SHOULD match id.
Frontmatter
YAML frontmatter, delimited by ---. Inherits all fields from
AIP-30 DRIVER.md plus the HTTP-specific fields
below.
Required HTTP-specific fields
| Field | Type | Description |
|---|---|---|
kind | const "http" | Per AIP-30. |
base_url | string | The API's base URL. ALL implements[].metadata.http.endpoint paths are relative to this. Schema-validated as URI. |
Optional HTTP-specific fields
| Field | Type | Default | Description |
|---|---|---|---|
default_headers | object | {} | Headers attached to every request. Templating allowed: { "Authorization": "Bearer ${secrets.OPENAI_API_KEY}" }. Per-tool overrides via metadata.http.headers. |
default_method | enum | POST | Default HTTP method when a per-tool entry omits metadata.http.method. |
streaming | object | none | Default streaming config for tools whose contract supports it. Per-tool override via metadata.http.streaming. v1 keys: transport: "sse" | "ndjson" | "chunked", event_field: string (SSE event-name to filter on). |
rate_limit | object | none | Hint for the resolver/runtime: { requests_per_minute: int, burst: int }. Used to throttle calls; not enforcement. |
Per-tool metadata.http (in implements[N].metadata.http)
Every entry in implements[] adds a metadata.http block describing
how this driver serves that specific tool:
| Field | Type | Description |
|---|---|---|
endpoint | string | Path relative to base_url (/v1/images/generations). |
method | enum | GET | POST | PUT | PATCH | DELETE. Defaults to driver's default_method. |
headers | object | Per-tool header overrides; merged into default_headers (per-tool wins). |
body_template | object | Request body shape with ${input.X} substitutions. Omitted = pass args.input verbatim as JSON. |
query_template | object | Query-string params with ${input.X} substitutions. |
response_extract | string | JSONPath-lite expression to extract the response body matching the contract's outputs shape. Default: identity ($). Examples: $.data[0], $.result.image.url. |
streaming | object | Per-tool streaming override of the driver's streaming. |
idempotency_key_header | string | Header name to send a deduplication token (Idempotency-Key). When set, the runtime synthesises a UUID per call. |
Body templating
body_template is a JSON object with string-valued leaves. Strings
containing ${input.X} substitute the validated input value. Strings
containing ${secrets.X} substitute the resolved secret. Arrays and
nested objects are templated recursively.
body_template:
model: "dall-e-3"
prompt: "${input.prompt}"
size: "${input.size | default('1024x1024')}"
user: "${context.user.id}"Template syntax (mirrors AIP-29 § argv):
${input.X}— value of input X. JSON-encoded by the runtime.${input.X \| default('Y')}— fallback when X is undefined/null.${secrets.X}— resolved secret value. Never logged.${context.X}— per-call context (user.id, workspace.id, …).${input.X \| json}— force JSON-stringify (for nested objects passed verbatim).
Conditional/computed bodies (e.g. "include seed only if input has
seed") use the optional entry's body-construction function (see
Custom body construction).
Response extraction
response_extract is a JSONPath-lite expression returning the value
the contract's outputs schema expects. Supported operators:
| Syntax | Meaning |
|---|---|
$ | Whole response body. |
$.foo | Property foo. |
$.foo.bar | Nested property. |
$.foo[0] | First array element. |
$.foo[*].bar | Map array → field, returns array. |
$.foo[?(@.kind=='X')] | Filter (basic predicate). |
Anything more complex than the above lives in the optional entry's
responseExtract function.
Auth shapes
HTTP drivers' auth block (inherited from DRIVER) typically uses
one of three patterns:
1. API key in header (most common)
auth:
ref: ./SECRETS.md
state: { env: ["OPENAI_API_KEY"] }
expiry: { detect: "http_status:401" }
default_headers:
Authorization: "Bearer ${secrets.OPENAI_API_KEY}"
Content-Type: "application/json"2. API key in query string
auth:
ref: ./SECRETS.md
state: { env: ["GOOGLE_MAPS_API_KEY"] }
expiry: { detect: "http_status:403" }
# No default_headers needed; query template per tool:
implements:
- tool: ./tools/maps-geocode/TOOL.md
metadata:
http:
endpoint: "/maps/api/geocode/json"
method: GET
query_template:
address: "${input.address}"
key: "${secrets.GOOGLE_MAPS_API_KEY}"3. OAuth bearer with refresh
auth:
ref: ./SECRETS.md
state:
env: ["GITHUB_OAUTH_ACCESS_TOKEN", "GITHUB_OAUTH_REFRESH_TOKEN"]
login:
url: "https://github.com/login/oauth/authorize?client_id=…"
interactive: true
requires_callback_url: true
completes_when:
http: { method: GET, url: "https://api.github.com/user", expect_status: 200 }
refresh:
# Refresh requires a custom flow; lives in driver.ts
cmd: ""
every: "PT1H"
expiry:
detect: "http_status:401"
default_headers:
Authorization: "Bearer ${secrets.GITHUB_OAUTH_ACCESS_TOKEN}"The OAuth refresh flow lives in the driver's optional driver.ts
entry, which exports a refresh() function per AIP-30.
Custom body construction
When body_template isn't expressive enough (conditional fields,
client-side hashing, multi-stage requests), declare an optional
driver.ts entry that exports a buildRequest function:
import { defineDriver } from "@agentproto/driver-runtime"
export default defineDriver({
id: "openai-images-http",
kind: "http",
// ...frontmatter mirrors...
buildRequest: ({ toolId, input, context, driverCtx }) => {
if (toolId === "image.create") {
return {
url: `${driverCtx.baseUrl}/v1/images/generations`,
method: "POST",
headers: {
"Authorization": `Bearer ${driverCtx.secrets.OPENAI_API_KEY}`,
"Content-Type": "application/json",
...(input.idempotency_key && { "Idempotency-Key": input.idempotency_key }),
},
body: {
model: input.model ?? "dall-e-3",
prompt: input.prompt,
...(input.size && { size: input.size }),
...(input.style && { style: input.style }),
},
}
}
throw new Error(`No buildRequest for ${toolId}`)
},
parseResponse: ({ toolId, status, body, headers }) => {
if (status === 200 && toolId === "image.create") {
return { ok: true, value: { url: body.data[0].url, width: 1024, height: 1024 } }
}
if (status === 401) return { ok: false, error: { code: "auth_required" } }
return { ok: false, error: { code: "upstream_error", message: body?.error?.message ?? "unknown" } }
},
})The host's HTTP runtime uses buildRequest / parseResponse when
declared, falling back to body_template / response_extract when
not.
Streaming (v1 declarative only)
When a contract supports streaming (per AIP-14's open-question section) AND the driver implements streaming, declare:
streaming:
transport: sse
event_field: data
implements:
- tool: ./tools/chat-completion/TOOL.md
metadata:
http:
endpoint: "/v1/chat/completions"
body_template:
stream: true
model: "..."
messages: "${input.messages}"
streaming:
transport: sse
event_field: data
terminator: "[DONE]"The HTTP runtime parses SSE events, yielding each data: payload to
the caller. v1 streams output only (request is unary). Bidirectional
streaming defers to a future revision.
Stable identity
Per AIP-30: id + version form the driver's stable identity.
Bumping base_url or auth scheme is a major change. Adding a new
implements[] entry without breaking existing ones is a minor.
The defineDriver({ kind: "http", ... }) signature
HTTP drivers use defineDriver per AIP-30, with kind: "http".
The HTTP-specific behavioural functions are:
interface DriverDefinition<"http"> extends DriverDefinition {
kind: "http"
baseUrl: string
defaultHeaders?: Record<string, string>
defaultMethod?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
streaming?: { transport: "sse" | "ndjson" | "chunked"; event_field?: string }
// Optional behavioural adapters.
buildRequest?: (args: BuildRequestArgs) => HttpRequest
parseResponse?: (args: ParseResponseArgs) => DriverResult<unknown>
}
interface BuildRequestArgs {
toolId: string
input: unknown
context: Record<string, unknown>
driverCtx: DriverContext
signal: AbortSignal
}
interface HttpRequest {
url: string
method: string
headers: Record<string, string>
body?: unknown // JSON-serialised by the runtime
query?: Record<string, string>
}
interface ParseResponseArgs {
toolId: string
status: number
body: unknown
headers: Record<string, string>
}A frontmatter-only HTTP driver (no entry) is valid when body_template
response_extractcover the dispatch shape. Most third-party REST APIs fit this pattern.
A custom-entry HTTP driver extends with buildRequest / parseResponse
when conditional or computed request shapes are needed.
Conformance rules
-
Inherit AIP-30 conformance. All AIP-30
defineDriverrules apply. Identity-vs-frontmatter, signal honouring, sandbox enforcement, no module-load I/O. -
base_urlis single-host. A driver serves one host. Multi- region failover is multiple drivers, not conditional URL. -
Body templating is JSON-only.
body_templateis JSON; v1 does not support form-data, multipart, or query-only bodies. Use a custom entry for those. -
Response extraction is JSONPath-lite. No JS expressions. Custom parsing lives in
parseResponse. -
Streaming is opt-in. Tools whose contract doesn't declare streaming-eligible outputs cannot stream regardless of driver's streaming declaration.
Authoring with SKILL.md
The canonical way to generate an HTTP DRIVER.md is via the paired
author-http skill.
The skill walks the agent through:
- Pick
id(kebab +-httpsuffix). - Identify
base_url. - Identify auth scheme + map to
default_headers/auth.state.env. - For each TOOL the driver implements: identify endpoint, method,
body shape, response shape; author
metadata.http. - Declare
network.egress(the API's host). - Validate against
./resources/aip-31/draft/HTTP.schema.json.
Example
See ./resources/aip-31/draft/EXAMPLES.md
for canonical patterns: API-key auth, OAuth-bearer, query-key auth,
streaming SSE, multi-tool sharing one auth.
Compatibility
With AIP-30 DRIVER.md
This AIP is a strict specialisation. Every HTTP driver IS a DRIVER. The AIP-30 schema validates the universal fields; the AIP-31 schema validates the HTTP-specific fields. Hosts MUST run both schemas in sequence at registration time.
With AIP-14 TOOL.md
HTTP drivers reference TOOL.md contracts via implements[].tool.
The contract's inputSchema is the input the runtime validates
against; the body template references those validated input values.
The contract's outputSchema is what response_extract (or
parseResponse) produces.
With AIP-19 SECRETS.md
The driver's auth.ref points at a SECRETS.md listing the
HTTP API's required env-var bindings (API keys, OAuth tokens). The
runtime resolves secrets per call and injects them into headers/body
via ${secrets.X} substitution.
Security considerations
- Headers leak secrets in logs. Hosts MUST redact
Authorization,X-API-Key, and any header name listed inauth.state.envfrom request/response logs. The runtime emits audit rows withheader_keys(not values). - Body templates leak secrets when authors carelessly substitute
${secrets.X}into body fields visible in audit logs. Hosts SHOULD flag anybody_templatefield whose value derives from${secrets.…}and redact during audit. - Idempotency keys prevent retry-induced double-charges. When
the contract is
mutates: ["external:*"], drivers SHOULD declareidempotency_key_headerand rely on the runtime's per-call UUID. - Open redirects in
base_urlare an attack surface for user-supplied URLs. Hosts MUST refuse abase_urlwhose hostname is in the call'sargs.input—base_urlis a static config, not user-controlled. - TLS verification is non-negotiable. Hosts MUST validate certificates; drivers MUST NOT request TLS-skip-verify.
Open questions
- Multipart / form-data bodies. v1 is JSON-only. APIs that
require multipart (file uploads) need either a custom entry or a
future
body_format: multipartextension. Defer to v2. - Bidirectional streaming. v1 streams response only. Request-streaming (uploading a transcribed audio chunk-by-chunk to a transcription API) needs a different mechanism. v2 candidate.
- GraphQL. A GraphQL endpoint is one URL with a body containing
the query — fits HTTP.md technically but feels under-served. v2
may add a
kind: graphqlsibling specialisation. - WebSocket transports. Some APIs (Anthropic's streaming endpoint, OpenAI's realtime API) speak WebSocket. v1 doesn't cover this; defer to AIP-32 MCP-over-WebSocket or a future AIP-XX WebSocket driver.
See also
- AIP-14 — TOOL.md — abstract contracts HTTP drivers implement
- AIP-19 — SECRETS.md — auth-surface inventory
- AIP-29 — CLI.md — sibling specialisation,
kind: cli - AIP-30 — DRIVER.md — abstract supertype
- AIP-32 — MCP.md — sibling specialisation,
kind: mcp - AIP-33 — SDK.md — sibling specialisation,
kind: sdk - Driver family — index of related AIPs
./HTTP.schema.json— JSON Schema validator./ADAPTER.md— implementer's guide./EXAMPLES.md— additional patterns
Resources
Supporting artifacts for AIP-31. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
AIP-30: DRIVER.md — agentdriver/v1 (abstract driver supertype)
A markdown + frontmatter format for declaring a concrete implementation of one or more agent tools — its identity, kind (cli / http / mcp / sdk / builtin), install lifecycle, auth surface, sandbox profile, and per-tool dispatch bindings. The supertype every concrete driver AIP (CLI, HTTP, MCP, SDK) specialises, with the standard `defineDriver` entry-point signature.
AIP-32: MCP.md — agentmcp/v1 (MCP driver specialisation)
A markdown + frontmatter format for declaring a Model Context Protocol driver — its server reference, transport (stdio / SSE / HTTP), per-tool MCP tool-name binding, prompts/resources composition, and lifecycle. Specialises AIP-30 DRIVER for the `kind: mcp` case. Wraps Anthropic-spec MCP servers (filesystem, github, postgres, …) as agent tools.