Skip to content

Protocol Adapters Pack

The Protocol Adapters Pack (@lumenflow/packs-protocol-adapters, WU-3521) ingests third-party API descriptions and turns them into immutable, content-addressed tool manifests that can be invoked with schema-validated typed arguments under the same governance discipline as every other LumenFlow tool.

  • ingestOpenApiDocument(input) — parses an OpenAPI 2.x/3.x document (a parsed object, or a JSON/YAML string), validates required structure (openapi/swagger version, paths, info.title + info.version, at least one servers[] entry, a unique operationId per operation), resolves local #/components/schemas/... $refs in request-body schemas, and returns a frozen, content-addressed OpenApiToolManifest. The manifest’s digest is a stable sha256: hash, so re-ingesting the same document — as an object or an equivalent JSON/YAML string — always reproduces the same digest.
  • invokeOpenApiOperation({ manifest, operationId, args, transport, allowedHosts }) — looks up the operation, validates args against its per-bucket (path/query/headers/body) input schema, and fails closed with an invalid_arguments result — never calling transport — on the first validation failure. Only once arguments are valid does it resolve the target host and check it against the caller-supplied allowedHosts; an unlisted host fails closed with egress_denied, again before transport is ever called.
  • runOpenApiAdapterConformanceSuite({ transport }) — a fixed conformance harness (stable digest, re-ingest idempotence, fail-closed invalid args, fail-closed disallowed host, successful invoke) exercised against a published, realistic “Petstore”-style golden fixture.
  • createProtocolAdaptersCapabilityFactory — derives a network/allowlist scope entry from protocol_adapters.allowed_hosts in pack config, using the same host/CIDR grammar the kernel sandbox’s own network allowlist uses.
import {
  ingestOpenApiDocument,
  invokeOpenApiOperation,
  runOpenApiAdapterConformanceSuite,
  createInMemoryOpenApiConformanceTransport,
} from '@lumenflow/packs-protocol-adapters';

const manifest = ingestOpenApiDocument(petstoreOpenApiDocument);

const result = await invokeOpenApiOperation({
  manifest,
  operationId: 'getPetById',
  args: { path: { petId: '1' } },
  transport: realTransport,
  allowedHosts: ['petstore.example.com'],
});

ingest.ts intentionally accepts a bounded OpenAPI/JSON-Schema subset: it rejects oneOf/anyOf/allOf/not and external $refs, and carries only type/properties/required/items/enum/format/minLength/ maxLength/minimum/maximum/additionalProperties through to the manifest. requestBody.content is read from the application/json key only. Local #/components/schemas/... $refs in request-body schemas resolve up to a depth of 20 with circular-$ref detection; external/remote $refs and #/components/parameters/... refs are rejected. Cookie-location parameters, multiple servers, security schemes, and non-JSON request bodies are out of scope today.

This is a deliberate design choice, not a limitation to work around: a general-purpose JSON Schema validator (ajv or similar) would accept a strictly larger grammar than ingest.ts can ever produce, would add a new runtime dependency to this pack’s SBOM, and would buy no additional correctness for the subset actually in play. invoke.ts instead validates against that exact bounded subset with a small hand-rolled walker.

invoke.ts never bypasses the kernel sandbox’s network allowlist and does not implement a second egress-control path: before calling the caller-supplied transport, it resolves the target host from the manifest’s recorded servers[0] and rejects the call with egress_denied unless the host matches one of the caller-supplied allowedHosts entries — using the exact same allowlist grammar (hostname, host:port, or IPv4 CIDR) the kernel sandbox itself uses, imported rather than reimplemented. Actual OS-level egress enforcement (bwrap/iptables on Linux, sandbox-exec/SBPL on macOS) remains the kernel sandbox’s job; this check is fail-closed defense-in-depth.

manifest.yaml declares:

tools: []

This is by design, not an oversight: an OpenAPI document’s operations cannot be statically predeclared as kernel tools at pack-authoring time, because they depend on whatever document gets ingested at runtime.

A consequence follows directly from that: the kernel only invokes a pack’s capability_factory once per tool declared in that pack’s own manifest tools: list. Because that list is empty today, createProtocolAdaptersCapabilityFactory is never invoked by the kernel — it is exercised only when a caller imports and calls it directly, which is exactly what this pack’s own tests do. It activates automatically, with no code change required in this pack, the moment a future WU registers an OpenAPI-derived tool in this manifest (tracked as a dynamic-tool-registration concern, independently shippable from this pack).

In short: an agent cannot call openapi:<operation_id> today. What ships now is the ingest/invoke/conformance library surface that a future tool-registration layer will sit on top of, proven correct against a golden fixture in isolation from that future wiring.