Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tungsten

tungsten compiles an OpenAPI 3.0 or 3.1 description into TypeScript, Python and Rust SDKs, a command-line client, an MCP server, browser WebMCP tools and LLM-oriented docs. Every client it emits is built for an autonomous caller: failures are instructions, side effects are never duplicated, and irreversible calls need an explicit confirmation.

This book is the user documentation. Start with Getting started, then read:

The source code is at github.com/pboachie/tungsten. The compiler is licensed under AGPL-3.0-only and the runtime libraries under Apache-2.0; output generated by tungsten carries no licence from tungsten.

Getting started

tungsten compiles an OpenAPI 3.0 or 3.1 description into TypeScript, Python and Rust SDKs, a command-line client, an MCP server and LLM-oriented docs, all built for autonomous callers.

Why tungsten

Most SDK generators optimize for a person reading autocomplete. An agent has a finite context window, no memory between retries, and no sense of what is irreversible. tungsten changes the design goal of each stage:

  • Diagnostic error envelopes. Every failure, from argument validation to a transport error to an HTTP status, is one closed JSON shape with a category, the failing parameter, the received value, what was expected, a remediation sentence and an explicit retryable verdict. The same 14 members in every runtime, ready to paste into a model's context.
  • Safety tiers. Each operation is read_only, mutating, destructive or irreversible, defaulted from the HTTP method and refined in agent.yml. Tiers drive retries, MCP tool annotations and confirmation rules.
  • Previews and confirmation tokens. A destructive or irreversible call must be previewed first. The preview returns the request that would be sent and its effects, plus a confirmation token that is valid only for identical arguments. The generated CLI replaces tokens with --yes (and --i-understand for irreversible operations).
  • Idempotency and unknown outcomes. Mutations are retried only with an idempotency key (or an identity body), always with the same key. A timeout or reset after a write is reported as OUTCOME_UNKNOWN with the call to verify it, not as a failure to retry blindly.
  • Verification. An operation can name a read to run after success and the state to expect, attached to the result.
  • MCP progressive disclosure. Past a tool-count threshold the server lists a few meta-tools (search_tools, describe_tool, invoke, preview, list_clusters) instead of every operation. In the maintainers' measurement on the ZROtext pilot (October 2026, BPE token counts), the progressive listing of its 41 tools costs 765 tokens, against 9,473 for the 14 tools of that project's hand-written MCP server.
  • Deterministic output. The same inputs produce byte-identical files. Generated files record the generator version and the digest of their inputs, and tungsten generate --check fails when output is stale.

Targets

TargetEmitsRuntime it depends on
typescriptTyped SDK, Result values, Zod validation@elysra/tungsten-runtime
pythonSync and async SDK, strict Pydantic v2 modelstungsten-runtime (PyPI)
rustAsync SDK (serde, reqwest) and, optionally, a generated CLItungsten-runtime and tungsten-cli-kit (crates)
mcpMCP server over stdio or Streamable HTTP, discrete or progressive@elysra/tungsten-mcp
webmcpBrowser ESM package with a tool manifest and explicit registration helper@elysra/tungsten-webmcp and the generated TypeScript SDK
docsllms.txt, llms-full.txt, tools.json, READMEnone
go (external)Go SDK package (models, resources, typed client, pagination and stream helpers), generated by tungsten-emit-go from emitters/gogithub.com/pboachie/tungsten/runtimes/go

Targets and the mock server (tungsten mock) share one IR, so they agree on operations, types and rules. The three SDK runtimes implement identical semantics: as of this writing a shared contract suite of 58 scenarios (kept in the maintainers' repository) passes unchanged through the TypeScript, Python and Rust drivers.

Install

Prebuilt binaries for Linux, macOS and Windows are attached to each GitHub release. The installer checks the archive against the release's SHA256SUMS before it installs anything:

curl -fsSL https://raw.githubusercontent.com/pboachie/tungsten/main/install.sh | sh   # Linux and macOS, into ~/.local/bin
irm https://raw.githubusercontent.com/pboachie/tungsten/main/install.ps1 | iex        # Windows

Pass --version v0.1.0 to install.sh (or -Version v0.1.0 to install.ps1) to pin a release, and --dir (-InstallDir) to choose the target directory.

Or build from source, with Rust 1.95 or newer (the repository pins 1.97.0 in rust-toolchain.toml for development):

git clone https://github.com/pboachie/tungsten
cd tungsten
cargo install --path crates/tungsten-cli   # installs the `tungsten` binary
tungsten doctor                            # reports optional tools (node, python3, uv, formatters)

Node, Python and the formatters are optional: a missing tool only disables the target or --format option that needs it.

Quickstart

tungsten init --from openapi.json   # writes tungsten.yml and agent.yml

init writes no targets, so generate has nothing to do until you add some. Edit tungsten.yml to add targets, and agent.yml to review every operation with side effects (init lists them as TODO):

# tungsten.yml
targets:
  typescript: { package: "@acme/petstore", out: generated/typescript }
  python:     { package: acme-petstore, module: acme_petstore, out: generated/python }
  docs:       { out: generated/docs }
tungsten check              # validate specs and manifests
tungsten generate           # write every configured target
tungsten mock . --port 8099 # serve the API from the IR, no backend needed

Then call the generated SDK. Python (the TypeScript client is symmetrical: const res = await client.pets.list({ limit: 10 })):

from acme_petstore import SwaggerPetstoreClient, Ok

client = SwaggerPetstoreClient(base_url="http://127.0.0.1:8099")
res = client.pets.list(limit=10)
if isinstance(res, Ok):
    print(res.value)
else:
    print(res.error["category"], res.error["remediation"])

Calls return a Result and do not raise for API or transport errors. Arguments are validated before anything is sent, so a wrong type produces this envelope (real output of the snippet above with limit="ten", no request made):

{
  "status": "error",
  "category": "VALIDATION_FAILED",
  "operation": "petstore.listPets",
  "http_status": null,
  "code": null,
  "failed_parameter": "args.limit",
  "received_value": "ten",
  "expected": "Input should be a valid integer",
  "remediation": "Fix args.limit (Input should be a valid integer) and call again.",
  "retryable": "never",
  "retry_after_ms": null,
  "next_action": null,
  "request_id": null,
  "trace": { "attempts": 0 }
}

Destructive operations use the preview flow. From the TypeScript SDK generated for the ZROtext pilot:

const preview = await client.public.webhooks.disable.preview({ endpointId });
if (preview.ok && preview.value.confirmation_token) {
  console.log(preview.value.effects);
  await client.public.webhooks.disable({ endpointId }, { confirm: preview.value.confirmation_token });
}

Architecture

  1. The frontend reads OpenAPI 3.0 and 3.1, resolves references and applies overlays.
  2. The IR is a versioned JSON document (tungsten ir dump, schema via tungsten schema ir).
  3. The agent compiler merges agent.yml into it: safety tiers, idempotency, confirmation, remediation text, verification hooks, macros and the disclosure mode for MCP.
  4. Emitters write each target from the IR. They are pure functions of their inputs.
  5. Runtimes hold the behaviour shared by generated code: validation, auth, retries, idempotency stores, the envelope, previews, pagination and middleware.

Repository layout: crates/ is the compiler (AGPL-3.0-only); the compiler, the compiler, the compiler, the compiler and the compiler are the libraries generated code depends on (Apache-2.0).

Licensing

  • The compiler is licensed under AGPL-3.0-only.
  • The runtime libraries that generated code depends on and the external Go emitter (emitters/go) are licensed under Apache-2.0.
  • Output generated by tungsten is not a work covered by the AGPL and carries no licence from tungsten. The owner of the API description chooses the licence of the generated code.

Supported platforms

The CLI release matrix builds these archives. The Windows ARM64 and Linux GNU archives will become available in the first release containing the expanded matrix; existing releases keep their original assets.

SystemCPURust targetArchive
Linux, static muslx86-64x86_64-unknown-linux-musl.tar.gz
Linux, static muslARM64aarch64-unknown-linux-musl.tar.gz
Linux, glibcx86-64x86_64-unknown-linux-gnu.tar.gz
Linux, glibcARM64aarch64-unknown-linux-gnu.tar.gz
macOSIntelx86_64-apple-darwin.tar.gz
macOSApple Siliconaarch64-apple-darwin.tar.gz
Windowsx86-64x86_64-pc-windows-msvc.zip
WindowsARM64aarch64-pc-windows-msvc.zip

Each archive is named tungsten-<version>-<target> with the extension above. Release assets include checksums and build provenance.

The Unix installer automatically selects the CPU and system. On Linux it uses the static musl archive by default, including on Alpine. To select glibc:

TUNGSTEN_LIBC=gnu sh install.sh --version vX.Y.Z

The glibc builds are tested on Ubuntu 22.04 for x86-64 (glibc 2.35) and Ubuntu 24.04 for ARM64 (glibc 2.39). Use musl for older glibc systems. Leave TUNGSTEN_LIBC unset on macOS. The Windows PowerShell installer selects x86-64 or ARM64 automatically.

ARMv7, RISC-V, FreeBSD and Android/Termux do not yet have validated CLI release archives. The CLI platform list is separate from the runtimes supported by SDKs it generates.

Start a project

Use an OpenAPI document to write the two manifests without interactive prompts:

tungsten init --from openapi.yaml --dir my-api --targets webmcp,py
tungsten check my-api
tungsten doctor my-api --strict
tungsten generate my-api

--targets accepts typescript, python, rust, docs, mcp, webmcp, and mock. Use ts, py, or rs as aliases. Separate targets with commas or repeat --targets. Duplicates are removed. Unknown names fail before files are written. The mock target is reserved; use tungsten mock to run the built-in mock server. Without --targets, init keeps the existing TypeScript, Python, and docs defaults.

Selecting webmcp also configures the TypeScript SDK and connects the browser adapter to it using sdk_path: ../typescript. Selecting both mcp and typescript also connects the server to that local SDK. Edit the package names, versions, output locations, and other target settings in tungsten.yml as needed.

Init derives the API name, namespace, and default server from the specification. When no document is found, it writes a placeholder spec path with a TODO. Review the safety rules in agent.yml, especially operations with side effects. Existing manifests are preserved unless --force is given. Symlinks and other nonregular manifest entries are refused even with --force.

Check local prerequisites

Bare tungsten doctor remains an informational list of optional tools and always succeeds. Give it a project directory or a tungsten manifest to identify the tools required by its configured targets:

tungsten doctor my-api
tungsten doctor my-api --strict

TypeScript, MCP, and WebMCP require Node.js 22 or newer and npm to build and install the generated packages. Python requires Python 3.12 or newer; Rust requires Cargo and rustc 1.88 or newer. Other tools remain optional. --strict fails when a required tool is missing, its version is unsupported, or a required minimum version cannot be determined. With --strict and no path, doctor checks the current project. A docs-only project has no required external tools. Invalid project manifests fail in either project mode.

Explicit sdk_path, runtime_path, and sdk_runtime_path settings are inspected for local package.json files relative to the generated target's output directory. These checks are informational: generation or dependency installation may still be pending. Doctor performs no registry requests, package installation, login, or credential prompts; success does not verify package publication or registry access.

Build generated packages

Generate first, then build the SDK before its agent adapter:

(cd my-api/generated/typescript && npm install && npm run build)
(cd my-api/generated/webmcp && npm install && npm run build)

The browser adapter requires the @elysra/tungsten-webmcp and @elysra/tungsten-runtime packages. Their versions and any local development paths belong in the target options. See the Browser WebMCP guide for frontend registration and disposal.

Specification compatibility

Tungsten loads OpenAPI 3.0.x and 3.1.x as JSON or YAML. Compatibility is a feature-level contract: accepting a document does not establish that every JSON Schema validation keyword or service behavior is implemented. Diagnostics identify rejected input and approximations. The checks named below run offline against focused interoperability fragments. Public source distributions do not include the private test corpus.

FeatureStatusDiagnostic / exact checkConstraint
OpenAPI 3.0 nullable and exclusive numeric boundsSupportednormalization checks, normalized_30_equals_its_31_form3.0 schemas normalize to the internal 3.1 representation; requiredness remains separate from nullability.
External complete OpenAPI documents with a different 3.x dialectSupportedcompatibility checks, external_openapi_30_document_keeps_its_dialect_from_a_31_entry, external_openapi_31_document_is_not_normalized_by_a_30_entry, nested_external_documents_propagate_their_own_dialects_to_fragmentsA referenced complete document keeps its declared dialect. A dialectless fragment inherits the referring document's dialect; fragments shared with 3.0 are normalized as documented by the normalization checks. Referenced documents do not become new input namespaces.
3.1 nullable type arrays and non-null scalar type unionsSupportedcompatibility checks, nullable_type_arrays_and_allof_record_composition_keep_presence; nullable checksnull becomes presence/nullability metadata; named types keep their non-null shape. Scalar unions retain their variants.
oneOf / anyOf with string discriminatorsSupportedunion checks; sanitized error-envelope fixtureExplicit mappings or distinct required constant string tags select variants. Invalid mappings produce TG0306.
Ambiguous untagged schema unionsLimitedTG0301; union checksOrdered variants approximate ambiguous unions. Acceptance by a generated validator does not guarantee JSON Schema oneOf exclusivity.
allOf record compositionLimitedTG0302 when flattening is unsafe; composition checks, nullable_type_arrays_and_allof_record_composition_keep_presenceCompatible record fields and constraints flatten. Other intersections remain explicit; no claim of arbitrary JSON Schema intersection solving.
Local and relative-file JSON Pointer $refSupportedreference checks, mixed-dialect Petstore fixtureReferences stay under an input directory. Dangling refs produce TG0201; traversal produces TG0204.
Schemas inside Path Item $ref siblingsLimitedcompatibility checks, path_item_ref_siblings_have_their_schemas_normalized_and_checked, path_item_siblings_follow_external_files_without_following_example_payload_refsThe frontend resolves and normalizes sibling schemas. Operation generation follows the referenced Path Item; sibling operations/parameters are not merged into the generated surface. OpenAPI leaves overlapping Path Item fields undefined.
Request media: JSON / +json, form, multipart, text, bytesSupported in the IR; target behavior is limited by runtime capabilitiesmedia-type corpus assertions, multipart fixtureWire encoding is selected per media type. This does not establish arbitrary per-property OpenAPI Encoding Object support or service interoperability for every emitted language.
patternProperties, conditional/dependent schemas, unevaluated*, tuple prefixItems, $dynamicRefLimitedTG0303; pattern-properties fixtureThese keywords are reported and ignored. Remaining schema shape still builds; generated validation can be broader than the source.
Anchor $ref fragments, remote $ref fetchingRejectedTG0201 (anchor), TG0202 (remote); reference checksVendor remote descriptions locally and use JSON Pointer file references. $id URI scope resolution and dynamic anchors are not implemented.
Swagger 2.0 / OpenAPI 3.2RejectedTG0103; version checks, unsupported_external_document_versions_are_diagnosed_at_the_document_and_referenceConvert unsupported inputs to supported OpenAPI versions first.

The Petstore interoperability fragment adapts the versioned OAI Petstore example from the Apache-2.0 licensed specification repository. The fixture deliberately splits a 3.1 operation document and a 3.0 model document and adds nullable tag metadata; it does not claim the upstream example uses mixed dialects. This catches the loader defect where a 3.1 entry previously lost 3.0 model nullability.

The Claude error-envelope fragment is an authored reduction of two discriminator variants from the pilot's pinned specification, associated with official SDK release 0.123.0 of the official TypeScript SDK. It uses a synthetic endpoint. The check covers that error shape, not the entire Claude API; no full vendor specification or credentials are redistributed.

Validation includes exact diagnostic and IR shape assertions, deterministic serialization, IR JSON Schema validation and generated TypeScript packages checked with tsc --noEmit --strict. These checks establish the tested fragment's compiler and generated type behavior. Live API authentication, server behavior, pagination and endpoint coverage need separate integration checks.

Runtime distributions

Generated clients need their runtime packages installed as well as their own generated code. The supported distribution units are:

TargetRuntime packagesPackage source
TypeScript SDK@elysra/tungsten-runtimenpm
MCP server@elysra/tungsten-runtime, @elysra/tungsten-mcpnpm
Browser WebMCP@elysra/tungsten-runtime, @elysra/tungsten-webmcp, generated TypeScript SDKnpm and your generated package
Python SDKtungsten-runtimePyPI
Rust SDKtungsten-runtimecrates.io
Rust CLItungsten-runtime, tungsten-cli-kitcrates.io

These names describe the intended registry packages. A successful local build does not mean a version exists in a registry. npm and PyPI publication remain disabled until maintainers configure trusted publishers and enable the release switches. Rust manifests retain publish = false; Rust publication requires a separate reviewed change. Before relying on registry installation, check that the version required by your generated package is available.

All runtime distributions use Apache-2.0 and include the actual license file. The generator's license is separate. npm packages ship compiled ESM, TypeScript declarations and explicit exports. @elysra/tungsten-runtime/node supplies Node adapters; the core and WebMCP import graphs stay free of Node builtins. The MCP runtime runs on Node and can import Node functionality. Browser WebMCP API availability depends on the host browser and is checked separately from package distribution.

For local development before registry publication, build the runtimes and set the generator's supported runtime path options to your local checkout. A source path is a development dependency; package releases use registry version ranges. The release checks also install all npm tarballs together into an isolated project, so the core peer is resolved from the packed artifact instead of a source link or an unpublished registry version.

Maintainers can reproduce the npm distribution check with make runtime-packages in the development repository. It checks version agreement, registry dependency declarations, exported JS and declaration files, Apache licenses, isolated package imports and the distributed browser import graph. The validated tarballs and checksums are retained for inspection. To check all runtime archives locally, run python release/runtime-artifacts.py; Python requires the pinned build and twine tools, and Rust verification compiles temporary packaged crates. The script builds a wheel and sdist, checks metadata, licenses and typing files, installs the wheel in an isolated environment, and packages the Rust workspace in a temporary copy without changing its committed publication switch.

The release workflow always checks npm packages, builds Python archives and rehearses Rust packaging. npm publishing uses the already validated archives, checks their checksums, and publishes the core runtime before its adapters. Only a real release with NPM_PUBLISH=true can publish npm packages, using GitHub's OIDC trusted publisher in environment npm. Missing registry publisher configuration fails the job; there is no stored-token fallback. Prereleases use the next npm dist-tag, and stable releases use latest. All archive versions must match the full release tag before the first upload; mismatched prerelease and stable package versions fail the publish step.

See Downloads for CLI installation and Browser integration for browser setup and capability checks.

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project will adhere to Semantic Versioning from its first stable release. Until 1.0, minor versions may contain breaking changes.

Unreleased

Added

  • Project setup with init --targets and a project-aware doctor --strict; browser targets automatically include their sibling TypeScript SDK.

  • Browser registration lifecycle controller for route and framework cleanup, cancellation of stale dynamic imports, and scoped loading.

  • Opt-in bounded streaming and binary WebMCP results; response byte limits and stream reader cancellation in the TypeScript runtime.

  • diff --migration upgrade actions, deprecation reporting and recorded runtime requirements. API surface snapshots use format 3 and read format 2.

  • A transport-neutral, versioned tool catalogue with JSON Schema, scoped groups and external emitter conformance helpers (schema tool-catalog).

  • Runtime tarball installation checks and gated npm trusted publishing for the TypeScript, MCP and WebMCP packages.

  • Built-in webmcp target: a browser ESM package with a shared tool manifest and an explicit registration helper backed by the generated TypeScript SDK and the new Apache-2.0 @elysra/tungsten-webmcp runtime. Registration defaults to read-only operations, supports scoped opt-in writes and macros, confirmation previews, cancellation and disposal. The runtime adapts the experimental document.modelContext API and the legacy navigator.modelContext shape; generated-package browser smoke passed in Chromium 154.0.8037.57 with WebMCPTesting enabled. Full cross-browser conformance remains a follow-up.

  • The language-neutral foundation of the coming Java, C#, Kotlin, Swift, PHP, Ruby and Dart SDKs in tungsten-emit (sdk): one SDK plan per API (types, operations, resource tree, macros), naming profiles for the seven languages in tungsten-ir, and the SDK descriptor document their runtimes read (tungsten schema sdk-descriptors). The repository guard knows the seven runtime directories. No target is added yet and existing output is unchanged.

  • Resumable event streams in the TypeScript, Python and Rust runtimes and the MCP server: a dropped stream reconnects with Last-Event-ID, honouring the server's retry under a cap (maxReconnects, default 3), without repeating delivered events; an idle timeout ends or reconnects a silent stream (STREAM_IDLE); on, collect, reduce, first and cancel helpers. tungsten mock continues a stream after Last-Event-ID and can drop or stall one for testing.

  • tungsten lsp: a Language Server Protocol server over stdio for tungsten.yml, agent.yml and overlay files. It reports the same diagnostics as tungsten check on the YAML lines they belong to, completes keys, values and operation ids, documents fields and operations on hover, jumps from an operation id to its place in the OpenAPI document and outlines the manifests. See the README for editor setup.

  • External emitters: a target of tungsten.yml with external: true (or a command) is generated by a separate tungsten-emit-<name> executable that speaks a small JSON protocol (the IR on standard input, files and diagnostics on standard output). The compiler writes the files, so generate --check, the output manifest and the API surface diff work unchanged. New: tungsten emitters, tungsten schema external-emitter, diagnostics TG0801 to TG0806 and the example examples/external-emitter-markdown.

  • Optional embedding index for the MCP server's search_tools, off by default and with no model shipped. targets.mcp.search.embeddings names a provider (a command speaking a small JSON protocol, or a plain http:// /embeddings endpoint); tungsten generate writes index.embeddings.bin and index.embeddings.json next to manifest.json, caching vectors by content hash. @elysra/tungsten-mcp ranks hybrid (BM25 and cosine, reciprocal rank fusion) when search.embeddings is set, and falls back to BM25 with the reason in the result when the query embedder fails. Diagnostics TG0570 to TG0574.

Changed

  • Rust runtime: StreamEvent has a new public field reconnects; code that builds or destructures StreamEvent with a struct literal must add it (or use ..).

  • Go SDKs through the external emitter interface: emitters/go builds tungsten-emit-go (enable it with go: { out: ..., external: true, module: ..., package: ... }), which generates a Go package with models, resources, a typed client, operation descriptors, pagination and stream helpers, macros and a README. The SDKs use the new Go runtime github.com/pboachie/tungsten/runtimes/go (standard library only, Go 1.22 or later), with the same error envelope, retries, idempotency, previews, confirmation tokens, auth handling and resumable event streams as the other runtimes.

0.1.0 - Unreleased

First release. The compiler, the three SDK runtimes (@elysra/tungsten-runtime, tungsten-runtime on PyPI and tungsten-runtime on crates.io), the MCP runtime (@elysra/tungsten-mcp) and the installers ship together at 0.1.0.

Added

  • Compiler: OpenAPI 3.0 and 3.1 frontend, tungsten.yml and agent.yml manifests, a versioned intermediate representation (tungsten ir dump, tungsten schema) and diagnostics with stable codes (tungsten check, tungsten explain).
  • Commands: init, check, generate (with --check for stale output), diff, report (text, JSON and a self-contained HTML report), doctor, ir dump, schema, explain and mock.
  • TypeScript SDK emitter and the @elysra/tungsten-runtime client runtime (Apache-2.0): Result values, Zod validation, the diagnostic error envelope, safety tiers, previews with confirmation tokens, idempotency keys, unknown-outcome reporting and retries.
  • Python SDK emitter (sync and async clients, strict Pydantic v2 models or plain dataclasses) and the tungsten-runtime Python package (Apache-2.0).
  • Rust SDK emitter (async client, a blocking client and typed macro outputs) and the tungsten-runtime crate (Apache-2.0).
  • Generated command-line interface for the Rust SDK, built on tungsten-cli-kit, with a stable output contract, exit codes, confirmation for destructive operations (--yes, --i-understand), configuration and pagination.
  • MCP server emitter and the @elysra/tungsten-mcp runtime: discrete or token-budgeted progressive tool surfaces, an automatic mode chosen from the tool count, stdio and Streamable HTTP transports, a sandboxed script tool and confirmation of destructive operations.
  • Streaming: server-sent events and JSON Lines responses in all three SDK runtimes (stream() and <method>_stream methods) and in the MCP server, which returns the collected events.
  • Pagination: opaque string cursors and after_id / has_more styles are inferred from the OpenAPI description.
  • Documentation emitter: llms.txt, llms-full.txt, tools.json and a README for each generated SDK.
  • tungsten mock: a mock server generated from the OpenAPI description, for trying a generated SDK, CLI or MCP server without a live API.
  • Behaviour that is identical across the TypeScript, Python and Rust runtimes, checked by a shared contract suite.
  • Release pipeline: prebuilt archives for Linux (x86_64, aarch64), macOS (x86_64, aarch64) and Windows (x86_64) with a checksum file, a CycloneDX SBOM and build provenance attestations, plus install.sh and install.ps1 installers that verify checksums.
  • OAuth2 authorization-code helpers in the TypeScript, Python and Rust runtimes: PKCE (S256), the authorization URL, the code exchange, refresh with a single flight and a pluggable token store. A call sends the stored token, refreshes it before it expires and once after a 401. Generated clients expose the helpers only when the specification declares an authorizationCode flow.
  • Repository guard and community files (contributing guide, security policy, code of conduct, issue forms and pull request template).

Changed

  • tungsten init writes a default targets block.
  • Documents that omit info.version, use query-style path keys, or leave out an API title now compile, with a warning where the result is inferred.
  • Types no operation reaches are pruned from the generated models.
  • Required constant headers and API-key environment variables are carried into the generated clients.
  • Error codes are found through tagged unions, and unknown-outcome messages describe what the caller should verify.
  • The compiler warns when a gates entry in agent.yml matches no operation.

Manifest reference

A tungsten project has two manifests next to each other: tungsten.yml (the shape and naming of the SDKs) and agent.yml (the agent semantics). Both are YAML with JSON Schemas shipped in the binary: tungsten schema tungsten and tungsten schema agent print them, and both are validated before compilation (schema errors are TG06xx diagnostics). For any agent-facing property the precedence is agent.yml, then a spec x-agent-* extension, then the inferred default.

tungsten.yml

Shape and naming of the SDK, independent of agent semantics.

tungsten: 1
api:
  name: acme
  title: Acme
  docs_url: https://docs.example.test
inputs:                                   # several documents, each a namespace
  - namespace: public
    spec: ../acme/protocol/v1/openapi/public-v1.json
    include: { extension: x-implemented, equals: true }
    planned_from: { extension: x-implemented, equals: false }
  - namespace: sealed
    spec: ../acme/protocol/v1/openapi/sealed-v1.json
  - namespace: workflow
    spec: ../acme/protocol/v1/openapi/workflow-tools-v1.json
    rpc_unflatten:
      path: /v1/workflow/tools
      method: post
      discriminator: method
      params: params
      name_strip_prefix: "workflow."
overlays: []                                # OpenAPI Overlay 1.0 files, applied in order
servers:
  default: https://api.example.test     # placeholder
  allow_override: true
resources:                                  # optional overrides of the inferred tree
  public:
    webhooks:
      path: /v1/webhooks
      children:
        deliveries: { path: "/v1/webhooks/{endpoint_id}/deliveries" }
    devices: { path: /v1/enrollment/devices }
    owner_messages: { path: /v1/owner/messages }
    alpha_messages: { path: /v1/alpha/messages }
naming:
  operations:
    public.listWebhookEndpoints: list
    public.createWebhookEndpoint: create
    public.replayWebhookDelivery: replay
  fields: preserve_wire_names_in_ts: false
auth_profiles:
  ownerSession:                             # Composite scheme
    composite:
      - cookie: __Host-acme_session
      - cookie: __Host-acme_csrf
      - header: { name: x-acme-csrf, equals_cookie: __Host-acme_csrf }
      - header: { name: Origin, from_config: origin, when: mutation }
    satisfies: [ownerSession, ownerCsrf]    # the spec's requirement pair
  bearerAuth: { bearer: { env: ACME_API_KEY } }
  workflowCredential: { bearer: { env: ACME_WORKFLOW_TOKEN, prefix: "ztw_" } }
pagination:                                 # explicit overrides; otherwise inferred
  public.listOwnerMessages: { cursor: { param: before, field: next_cursor, items: messages } }
targets:
  typescript: { package: "@acme/sdk", out: generated/typescript, runtime: "^0.1" }
  python:     { package: acme_sdk, out: generated/python, models: pydantic }
  rust:       { crate: acme-sdk, out: generated/rust, cli: { bin: acme } }
  mcp:        { package: "@acme/mcp", out: generated/mcp }
  docs:       { out: generated/docs }
  mock:       { out: generated/mock }

Resource tree inference

When resources is absent: strip the first path segment if it is a version (/v1), split remaining segments, drop {param} segments, and nest. Tags override segment names when a tag covers all operations under a prefix. Method names: list for GET collection, get for GET item, create for POST collection, update for PATCH/PUT item, delete for DELETE, and the last path verb segment for POST actions (enable, rotate, replay, cancel). The operationId camel-cased is the fallback.

:

  • Placement. A configured resource of the namespace whose path is the longest segment prefix of the operation's path wins ({a} matches any {b}; ties go to the first declared). Otherwise the tree is inferred: a leading version segment (v1, v2, v1beta, v2alpha1) is dropped, a trailing action is set aside, and the remaining literal segments nest (/v1/webhooks/{id}/deliveries → webhooks › deliveries). A literal segment that only leads to children becomes a resource with no operations (/v1/owner/messages → owner › messages). An operation with no literal segment goes to a resource named root. A segment that mixes text and placeholders ({date}.csv) is a Template segment in the IR and counts as an item, like {param}; it never names a resource.
  • Custom methods. A :verb suffix on the last segment (/v1/users/{id}:archive, /v1/users:search, any HTTP method) is an action: the operation is placed by the path without the suffix and named after it (users.archive, users.search).
  • Action. The last segment of a POST path is an action when it is a literal right after a {param}, every operation on that exact path is a POST, no path continues below it, and it reads as a verb: one of a fixed verb list (enable, rotate, replay, cancel, revoke, ...) or a last word not ending in s. So /{id}/enable is the action enable, while /{id}/sealed-events is a sub-resource whose POST is create.
  • Method names. naming.operations wins; then, relative to the resource's path: list for a GET of a collection, get for any other GET of the resource's own path (a singleton: GET /v1/health, /users/me, /users/{id}/settings), create (POST of the resource's path), update (PATCH, PUT) and delete on a singleton path, get / update (PATCH, PUT) / delete on one item, the action for an action; anything else keeps its operation id. A GET is a collection when it is paginated, its success JSON body is an array or an object with exactly one array property, or (no JSON body says otherwise) an item path <path>/{id} exists or its last segment is a plural (deliveries). PUT, PATCH and DELETE on a collection path keep their ids. Collisions inside a resource, and between sibling resources, are renamed with a numeric suffix and TG0401 (list, list2 for /v1/pets and /v2/pets, which merge into one pets resource).
  • rpc methods (rpc_unflatten). A method goes under the configured resource of its HTTP path (or the top level), then one level per dotted part of its name except the last, and is named by the last part: workflow.action.send with name_strip_prefix: "workflow." → tools › action › send. A method without dots is placed like an HTTP operation. Child resources declared without a path are matched by name.
  • path_prefix is the longest common segment prefix of the resource's operations and the path that names it (configured path, inferred path up to its literal segment, or the HTTP path of rpc methods), without trailing parameters; a resource with neither uses its descendants.
  • Order. Resources and children appear in the order of their first operation in the document; operations keep document order. Planned operations get method names but stay out of the tree.
  • A configured path that matches no path of the input is TG0604; a naming.operations or pagination key that names no operation is TG0603.

Inputs, filters and gates

  • include and planned_from compare one operation extension with a value. Without include everything is included, and an operation that matches planned_from is planned. With include, an operation that fails it is planned when it matches planned_from and dropped otherwise. Every excluded operation is a TG0504 info naming the reason; planned ones keep that reason in OperationStatus::Planned and live in Namespace.planned, never in the resource tree.

  • rpc_unflatten needs the named path and method to exist and its JSON request body to be a oneOf whose every member has a string const (or one-value enum) under discriminator, with distinct values that leave a dotted name after name_strip_prefix. Any failure is a TG0506 error and the envelope stays one ordinary operation.

  • A single OpenAPI file checked without a manifest compiles with a default one: namespace and api.name from the file stem, no filters, no profiles. Acme's documents compiled that way show every route as callable and infer their paginations (TG0501).

  • pagination.<op> declares one style (cursor, offset, page, link_header) or none: true, which only switches inference off. Declared entries apply to planned operations too.

  • types.prune_unreferenced (default true) drops the named types no operation reaches, after the agent manifest has been applied: roots are the parameters, request and response bodies and headers of every callable and planned operation and the error envelopes (including the one agent.yml names); everything they reach through arrays, maps, records, unions (discriminator mappings included) and intersections stays. The warnings and notes about the schemas of dropped types go with them (errors never do), and one TG0760 note counts the dropped types and names the first eight. A document without operations is a library of schemas and keeps every type. false generates every schema.

Auth profiles

  • A composite profile becomes one AuthScheme::Composite with the profile's name, its parts in order (cookie, header with equals_cookie / from_config / when: mutation → mutation_only, bearer with env / prefix) and satisfies resolved to IR scheme names. A satisfies entry naming no scheme of any namespace is TG0503; a spec name that was namespace-qualified expands to every qualified variant.
  • The profile may share its name with a spec scheme it satisfies. Both stay in Ir.auth, the composite sorted after the spec scheme; operation requirements keep naming the spec schemes.
  • Cookie and header names supplied by a composite give matching parameters the Auth role, except Origin, which keeps Origin.
  • config entries of other profiles are runtime configuration and do not change the IR.
  • a non-composite profile with bearer configures the HTTP bearer scheme of the same name (spec or IR name): HttpBearer.prefix and HttpBearer.env are additive IR fields. The spec's bearerFormat stays format and is only documentation; the prefix is enforced by the runtime before sending.

MCP embedding index (targets.mcp.search.embeddings)

targets:
  mcp:
    package: "@acme/mcp"
    out: generated/mcp
    search:
      embeddings:
        provider: command           # command | http
        command: "node tools/embed.mjs"   # provider: command (words split on spaces; quotes group)
        # url: http://127.0.0.1:8080/v1/embeddings   # provider: http (plain http at generation)
        model: my-embedding-model   # required: recorded in the header, keys the cache
        dimensions: 384             # optional: checked on every answer, sent to http endpoints
        # api_key_env: EMBED_API_KEY  # http: Bearer token read from this variable, never written
        required: false             # true: a provider failure fails generation
        batch_size: 32              # documents per call, 1 to 512
        timeout_ms: 60000           # per call, 1 to 3600000
KeyMeaningDefault
providercommand (JSON on standard input and output) or http (the common /embeddings request shape)required
commandthe command line of a command provider, run from the directory of tungsten.ymlrequired for command
urlthe endpoint of an http providerrequired for http
modelmodel name sent to the provider and recorded in index.embeddings.jsonrequired
dimensionsvector length; a different answer is TG0572taken from the first answer
api_key_envenvironment variable with the endpoint's API key (http only)none
requireda provider failure is an error instead of a warning and BM25 onlyfalse
batch_sizedocuments per provider call32
timeout_mswall-clock limit of one provider call60000

Command protocol (version 1), request on standard input and answer on standard output (exit status 0; standard error is free text):

{ "protocol": 1, "kind": "document", "model": "m", "dimensions": 384, "inputs": ["text", "..."] }
{ "embeddings": [[0.12, -0.03, "..."], ["..."]] }

agent.yml

Agent semantics. Every key compiles into AgentModel / OperationAgentMeta in the IR.

agent: 1
defaults:
  safety:
    get: read_only
    post: mutating
    put: mutating
    patch: mutating
    delete: destructive
  idempotency: none
  preview: local
  unknown_outcome:
    on_timeout: unknown            # never "retry"
    on_connection_reset: unknown
    on_ambiguous_status: unknown   # statuses marked ambiguous in `errors`
  retries:
    read_only: { max: 3, backoff: { base_ms: 200, max_ms: 5000, jitter: full } }
    mutating:  { max: 0 }          # retries only with an idempotency key, see idempotency
    honor_retry_after: true
  disclosure:
    mode: auto                     # auto | discrete | progressive
    threshold: 24
    description_budget_tokens: 60
    schema_budget_tokens: 600

tools:
  - operation: public.submitAlphaMessage
    safety: irreversible
    idempotency: { policy: caller_owned, header: Idempotency-Key, format: uuid_v4, persist: required }
    confirmation:
      summary_fields: [recipient_e164, device_id, test_case_id, expires_at_ms]
      message: "This queues an SMS to {recipient_e164} from device {device_id}. Carrier charges may apply."
    verify:
      operation: public.getAlphaMessageStatus
      args: { message_id: $response.message_id }
      expect: { state: { in: [accepted, queued, claimed, submitting, submitted, delivered] } }
      terminal: { state: { in: [delivered, delivery_unknown, unknown, failed, cancelled, expired] } }
      poll: { interval_ms: 1000, budget_ms: 60000 }
    remediation:
      queue_full: { text: "The device queue is full. Wait and retry with the SAME Idempotency-Key.", retryable: same_key_only }
      recipient_suppressed: { text: "Recipient opted out. Do not retry, do not pick another number. Stop and report.", retryable: never }
      billing_pending: { text: "Account billing is not active. Stop; the owner must act.", retryable: never }
      payment_hold: { text: "Payment hold on the account. Stop; the owner must act.", retryable: never }
  - operation: public.revokeOwnerDevice
    safety: destructive
    confirmation: { summary_fields: [device_id] }
  - operation: public.rotateWebhookEndpoint
    safety: destructive
    response: { sensitive_fields: [secret], shown_once: true }
    remediation_note: "Rotation disables the endpoint and retires pending deliveries. Re-enable after storing the new secret."
  - operation: public.createWebhookEndpoint
    response: { sensitive_fields: [secret], shown_once: true }
  - operation: public.disableWebhookEndpoint
    safety: destructive
  - operation: public.replayWebhookDelivery
    idempotency: { policy: caller_owned, header: Idempotency-Key, format: uuid_v4, persist: required }
    remediation:
      replay_not_eligible: { text: "Only exhausted failed deliveries on an enabled endpoint can be replayed. Check the delivery's terminal_reason and the endpoint's enabled flag; do not retry.", retryable: never }
      replay_limit: { text: "All three replay generations were used. Stop.", retryable: never }
  - operation: sealed.submitSealedOutbound
    safety: irreversible
    idempotency: { policy: content_identity, note: "Retries must resend the identical envelope bytes; the unsigned digest is the identity. Never attach an Idempotency-Key header." }

errors:
  envelope: { schema: Error, code_field: code }
  ambiguous_statuses: [408, 503]                  # on mutations → OUTCOME_UNKNOWN
  non_json:
    - { status: 413, media: text/plain, category: REQUEST_TOO_LARGE, retryable: never }
    - { status: 400, media: text/plain, category: MALFORMED_REQUEST, retryable: never, text: "A path or query parameter failed framework parsing before authentication. Fix the parameter format (UUIDs must be canonical)." }
  codes:
    unauthorized: { category: AUTH_FAILED, retryable: never, text: "Credential missing, expired or revoked. Do not retry; obtain a new credential." }
    forbidden:    { category: AUTH_FAILED, retryable: never, text: "CSRF or Origin check failed, or scope insufficient." }
    rate_limited: { category: RATE_LIMITED, retryable: after_delay }
    unavailable:  { category: UPSTREAM_UNAVAILABLE, retryable: after_delay }
    not_found:    { category: NOT_FOUND, retryable: never }
    conflict:     { category: CONFLICT, retryable: never }
    invalid_request: { category: VALIDATION_FAILED, retryable: never }

gates:                                              # x-runtime-gate rendering
  SEALED_WEBHOOK_DELIVERY_ENABLED:
    text: "This deployment returns 404 for sealed-event selection because SEALED_WEBHOOK_DELIVERY_ENABLED is off. This is a deployment setting, not a missing resource."

macros:
  - name: public.submitAlphaMessageAndAwait
    summary: "Submit a synthetic-alpha message and wait for a terminal delivery state."
    safety: irreversible                             # inherits the strictest step
    steps:
      - call: public.submitAlphaMessage
        args: $input
        as: accepted
      - poll: public.getAlphaMessageStatus
        args: { message_id: $accepted.message_id }
        until: { state: { in: [delivered, delivery_unknown, unknown, failed, cancelled, expired] } }
        interval_ms: 1000
        budget_ms: $input.wait_budget_ms
        as: status
    output: { message_id: $accepted.message_id, created: $accepted.created, state: $status.state, requires_reconciliation: { expr: "$status.state in ['unknown','delivery_unknown']" } }
  - name: public.bootstrapWebhookEndpoint
    summary: "Create a webhook endpoint, enable it, return the one-time secret."
    steps:
      - call: public.createWebhookEndpoint
        args: $input
        as: created
      - call: public.enableWebhookEndpoint
        args: { endpoint_id: $created.endpoint_id }
    output: { endpoint_id: $created.endpoint_id, secret: $created.secret }
    response: { sensitive_fields: [secret], shown_once: true }
  - name: public.listAllOwnerDevices
    summary: "All enrolled devices across pages."
    steps:
      - paginate: public.listOwnerDevices
        max_pages: 20
        as: devices
    output: $devices

disclosure:
  clusters:                                          # semantic grouping for search_tools
    - { name: messaging, operations: [public.alpha_messages.*, public.owner_messages.*, sealed.sealed_messages.*] }
    - { name: webhooks,  operations: [public.webhooks.*, sealed.sealed_webhooks.*] }
    - { name: devices,   operations: [public.devices.*, sealed.sealed_devices.*] }
    - { name: workflow,  operations: [workflow.*] }
  prune:
    descriptions: { max_sentences: 2, drop_phrases: ["Implemented today (", "crates/server/src/"] }
    drop_fields: []                                  # fields hidden from agent schemas
    keep_examples: false

Safety tiers

TierMeaningSDKCLIMCP
read_onlyNo side effectsretries allowedrunsreadOnlyHint: true
mutatingSide effect, reversible or idempotentno blind retry; preview availablerunsdestructiveHint: false, idempotentHint from policy
destructiveDeletes or disables something; recoverable only by re-creationrequires {confirm: true} option or preview firstrequires --yes or TTY confirmationdestructiveHint: true; needs confirmation_token from a prior preview call
irreversibleCannot be undone (money, messages sent to people, permanent deletion)requires {confirm: {token}} obtained from preview()requires --yes --i-understand and prints the summarydestructiveHint: true; needs confirmation_token; the MCP server refuses unless the client supplies the token returned by preview in the same session

Idempotency policies

PolicyWho generates the keyPersistenceRetry rule
nonenobody–mutation never auto-retried
autoruntime, UUIDv4 per logical callruntime IdempotencyStore (memory by default; file or caller-provided)retried with the same key within retries.mutating
caller_ownedthe caller, validated against formatcaller must persist; persist: required makes the runtime refuse a call without a keyretried with the same key only
content_hashruntime, hash of the canonical bodystore keyed by hashsame
content_identitythe body itself is the identity (binary envelopes)n/aresend identical bytes only

The blueprint's "auto-generate a key per call" is wrong for APIs like Acme where a key must survive the agent's own crash. persist: required exists for exactly that case and the generated docs say why.

Preview modes

ModeBehavior
localValidate arguments, resolve the auth profile, render the request (method, URL, headers with secrets redacted, body) and the manifest's side-effect summary. No network. Returns a confirmation_token bound to the argument digest.
headerAs local, then send with preview.header (for example X-Dry-Run: true) and return the server's response as the preview.
endpointCall preview.operation with mapped args.
none.preview() is not emitted.

Remediation table resolution

Runtime lookup order for an HTTP error: operation-specific tools[].remediation[code] → global errors.codes[code] → errors.non_json match on status and media type → generic by status class. The matched entry supplies category, remediation, retryable and, when present, a next_action referencing a verification hook or macro.

Server rules: what the server enforces and the spec leaves out

tools[].server_rules declares rules of the real server that the API description does not state, so that tungsten mock answers a request that breaks one the way the server does (the Acme live pilot found two: reserved callback hosts and an expiry window, pilots/acme/REPORT.md findings 1 and 2). The key is additive: a manifest without it, and the IR of an operation without rules, are unchanged. It is data, never read by the SDKs, the MCP server or the docs.

tools:
  - operation: public.createWebhookEndpoint
    server_rules:
      - { field: callback_url, reject_host_suffixes: [".test", ".example", ".invalid", ".local", ".localhost"], status: 400, code: invalid_webhook_endpoint }
  - operation: public.submitAlphaMessage
    server_rules:
      - { field: expires_at_ms, max_ahead_minutes: 15, status: 400, code: invalid_request }
KeyMeaning
fieldThe request member read: a dotted path into the JSON body (options.expires_at_ms), else a query parameter of that name. A member that is absent, or whose value is not of the kind the check reads, is not this rule's business.
reject_host_suffixesThe value is a URL; the rule is broken when its host (without userinfo, port, case or a trailing dot) is a suffix or the bare name of one (.test matches a.test and test, not latest.com).
max_ahead_minutesThe value is epoch milliseconds (a number or numeric text); the rule is broken when it is later than now plus that many minutes. A time in the past passes.
statusThe refusal's status, 400 to 599; default 400.
codeThe API error code written at the error envelope's code field (text/plain APIs lead the text with it).
messageThe reason (X-Tungsten-Reason); default names the field and the rule.

Exactly one of reject_host_suffixes and max_ahead_minutes per rule (TG0602 otherwise). Compiled form: OperationAgentMeta.server_rules: Vec<ServerRule> (field, check tagged host_suffixes { suffixes } or max_ahead_minutes { minutes }, status, code?, message?), written only when non-empty. The mock checks them after the description's own checks (authentication, parameters, body schema) and before idempotent replay, so a refused request is never stored; rules apply to their operation only, the first broken one answers. Tests: unit_mock_rules, unit_agent_*, corpus agent-server-rules, contract scenarios mock-refuses-reserved-callback-hosts and mock-refuses-an-expiry-beyond-the-window.

Vendor extensions accepted from the spec

x-agent-safety, x-agent-idempotency, x-agent-preview, x-agent-confirmation, x-agent-remediation, x-agent-verify, x-agent-sensitive (on schema fields), x-agent-cluster. Same shapes as the manifest entries. Unknown x-agent-* keys raise TG0610.

Details

tungsten-agent loads agent.yml (next to tungsten.yml; schema specs/agent-manifest.schema.json, printed by tungsten schema agent) and applies it to the IR after the build. Refinements of the plan above:

  • Precedence: agent.yml > x-agent-* > inferred (a required Idempotency-Key header makes a side-effecting operation caller_owned with persist_required) > the manifest's defaults > the method defaults. A rule that cannot apply is dropped with a warning (TG0603–TG0611) and the rest of the manifest still applies.
  • AgentModel is flat: macros, clusters, gates, error_codes, ambiguous_statuses, non_json, retries, unknown_outcome, disclosure. OperationAgentMeta also has remediation_note and hidden. A gates entry may declare disabled_status.
  • compact_doc is the summary plus the description, sentences with a drop_phrases entry removed, the first max_sentences kept, cut at a word boundary to description_budget_tokens (a token counted as four characters).
  • Clusters: each operation and macro belongs to at most one cluster, the one its tools entry or x-agent-cluster names, else the first declared cluster whose list (operation references, macro names, <ns>.* and <ns>.<resource path>.* globs) covers it.
  • Macros are checked against the compiled API and normalized to the canonical form on tungsten_ir::Macro (steps {kind, operation, args, as, until, interval_ms, budget_ms, max_pages}, input: {extends, add}); a macro carries cluster, sensitive_response_fields and shown_once. Its safety is the strictest step's (a poll counts as read_only); a weaker declared tier is raised (TG0609).
  • Macro confirmation is macro-level. A macro whose tier is destructive or irreversible (or that has such a step) needs one confirmation for the whole run: confirm: true for destructive, otherwise a token from the macro's own preview, bound to the macro name and its exact input (after defaults). That confirmation authorizes every step; steps never ask for tokens of their own, since their arguments can depend on earlier results. The macro preview validates and renders the first step and lists every step's effects (arguments only known later read <from an earlier step>), without sending anything.
  • At run time an added input field's JSON Schema default is applied when the input omits it, and a poll step whose budget runs out binds its as name to null (so {expr: "$status == null"} reports the timeout; the timed_out output). The first failing step's envelope is returned with the completed steps named in its remediation.
  • disclosure.prune.drop_fields and keep_examples are accepted but not applied in this release: agent tool schemas list exactly the SDK's arguments, and a non-empty drop_fields (or keep_examples: true) is a TG0613 warning instead of a silent no-op. Their semantics (which fields may be hidden, how they are named, what the MCP server does with a hidden field) are decided with the MCP emitter in this release.
  • tungsten.yml targets must have distinct, non-nested out directories (TG0612): each target's .tungsten/manifest.json removes the files it no longer generates, so targets sharing a directory deleted each other's output on every run.

Overlays

overlays: in tungsten.yml lists OpenAPI Overlay 1.0 files, applied in order before the document is compiled. Use them to repair a third-party description without editing it. tungsten overlay suggest writes an overlay whose actions fix a missing or repeated operationId:

tungsten overlay suggest . --write fixes.overlay.yaml

Safety and runtime behaviour

The three runtimes (@elysra/tungsten-runtime for TypeScript, tungsten-runtime for Python and tungsten-runtime for Rust) implement identical semantics, checked by a shared contract suite. This page describes that behaviour.

Diagnostic error envelope

Every failure the SDK returns, from pre-flight validation to transport failure to an HTTP error, is one shape:

{
  "status": "error",
  "category": "VALIDATION_FAILED",
  "operation": "public.webhooks.deliveries.replay",
  "http_status": null,
  "code": null,
  "failed_parameter": "idempotencyKey",
  "received_value": "replay-1",
  "expected": "UUIDv4 string",
  "remediation": "Generate a random UUIDv4, persist it with the replay intent, and pass it as idempotencyKey. Reuse the exact same key on every retry of this replay.",
  "retryable": "never",
  "retry_after_ms": null,
  "next_action": null,
  "request_id": null,
  "trace": { "attempts": 0 }
}

Categories (closed set, shared by all runtimes):

CategorySourceDefault retryable
VALIDATION_FAILEDpre-flight or 400 with envelopenever
MALFORMED_REQUESTnon-JSON 400never
REQUEST_TOO_LARGE413never
AUTH_FAILED401, 403never
NOT_FOUND404 (or gated route)never
CONFLICT409never
PRECONDITION_FAILED402, 412, 422 by manifestnever
RATE_LIMITED429after_delay
UPSTREAM_UNAVAILABLE502, 503, 504 on a readafter_delay
OUTCOME_UNKNOWNtimeout, reset, ambiguous status or unexplained 5xx on a mutationsame_key_only (after_remediation without replay protection)
TRANSPORT_FAILEDDNS, TLS, connection refused before sendafter_delay
CONFIRMATION_REQUIREDdestructive/irreversible without tokennever (call preview)
GATE_DISABLEDstatus matches an x-runtime-gate disabled statusnever
UNEXPECTED_RESPONSEresponse failed strict validationnever

retryable values: never, after_delay, same_key_only, after_remediation. The envelope is designed to be pasted verbatim into a model's context: short, imperative, no stack traces.

received_value is redacted when the parameter is marked sensitive and truncated to 200 characters.

Pre-flight validation

Before any network call the runtime validates: required params present, types and formats (uuid, date-time, e164 via pattern), enum membership, string bounds, closed records (unknown keys rejected when the schema is additionalProperties: false), idempotency key format when the policy is caller_owned, confirmation token when the tier requires it. The first failure is returned as VALIDATION_FAILED with failed_parameter as a JSON path (body.recipient_e164).

Transport pipeline

call → preflight → auth profile → idempotency → [preview?] → send → classify → verify? → result
                                     ▲                           │
                                     └──── retry policy ◀────────┘

Middleware hooks: onRequest, onResponse, onDiagnostic, onRetry. The OpenTelemetry adapter is a middleware. Default headers: User-Agent: <api>-sdk/<ver> tungsten/<ver> (<target>), X-Tungsten-Operation.

Retry policy

  • Read-only: up to max attempts on RATE_LIMITED, UPSTREAM_UNAVAILABLE, TRANSPORT_FAILED, with full-jitter exponential backoff, honoring Retry-After (seconds or HTTP-date) when present, capped by max_ms.
  • Mutating with an idempotency key (auto, caller_owned, content_hash): same as read-only, always with the identical key and identical body.
  • Mutating without a key: never retried; a transport failure before the request was sent is TRANSPORT_FAILED; anything after is OUTCOME_UNKNOWN.
  • Redirects are never followed on mutations. Reads follow same-origin redirects only (as built: at most five, auth query parameters re-applied); fetch is never asked to follow, since it would forward custom credential headers to another origin.

Unknown outcome

Returned when the runtime cannot know whether the server applied a mutation: client timeout after the request was written, connection reset without a response, or a status listed in errors.ambiguous_statuses. The envelope carries next_action from the verification hook when one exists ("Call public.getAlphaMessageStatus with message_id if you have it, or list recent messages filtered by client_message_id"), and retryable: same_key_only. SDK docs state that a new key for the same intent is the one way to duplicate a side effect.

Idempotency store

Interface in each runtime: get(scope, logical_id) → key?, put(scope, logical_id, key). Default memory store; file store adapters for Node and Python; caller_owned + persist: required bypasses the store and refuses calls without a key. Keys are never logged.

Preview and confirmation

preview() returns:

{
  "operation": "public.webhooks.rotate",
  "safety": "destructive",
  "request": { "method": "POST", "url": "https://.../v1/webhooks/0b2e.../rotate", "headers": { "Origin": "https://app.example.test", "x-acme-csrf": "<redacted>" } },
  "effects": [
    "Disables webhook endpoint 0b2e... immediately.",
    "Permanently retires its pending deliveries.",
    "Returns a new signing secret exactly once."
  ],
  "confirmation_token": "tgc1.…",
  "expires_in_ms": 300000
}

, macros follow the same rule at macro level: ClientCore.previewMacro returns a token bound to the macro name and its exact input, and runMacro requires it (or confirm: true for a destructive macro) when the strictest of the macro's declared tier and its steps' tiers is destructive or irreversible; the run's steps then need no tokens of their own.

effects comes from the manifest's confirmation.message, remediation_note and the operation's compacted description. The token is HMAC(session key, operation, args digest) and is only valid for identical arguments. In the SDK the session key is per client instance; in the MCP server per connection; in the CLI it is replaced by --yes because the CLI has no session.

Verification hooks

Compiled from verify entries: after a successful call, optionally run the referenced read operation with mapped arguments and compare with expect; poll up to budget_ms when terminal is given. Results are attached to the success result as verification: { checked: true, state: "queued" }. The hook never changes the primary result and is off by default in the SDK ({verify: true} option), on by default in the MCP invoke for irreversible operations, and exposed as --verify in the CLI.

Sensitive data

Fields marked sensitive (manifest or x-agent-sensitive) are: excluded from received_value, excluded from traces and logs, redacted in previews, and flagged shown_once in MCP text renderings. The runtime never persists them.

Auth profiles

Profile kindRuntime behavior
bearerAuthorization: Bearer <token> from env, config or callback; optional required prefix check (ztw_, ApiDescriptor http_bearer prefix) before every request: a mismatch is AUTH_FAILED and nothing is sent
api_keyheader, query or cookie
basicheader
oauth2_client_credentialstoken fetch and refresh with single-flight
oauth2_authorization_codehelpers on the client (oauth): PKCE S256 (RFC 7636), authorizationUrl, exchangeCode, refresh; the tokens live in ClientOptions.tokenStore (memory by default) and are sent as bearer, refreshed single-flight when they expire within 30 s, and refreshed once, then the call is sent once more, after a 401; see "OAuth2 authorization code"
compositethe ordered parts: cookies added to the jar, derived headers computed (equals_cookie), conditional headers (when: mutation) added for non-GET; missing parts fail pre-flight with AUTH_FAILED and a remediation naming the missing piece

OAuth2 authorization code

An authorizationCode flow of an oauth2 scheme reaches the API descriptor as authorizationCode: {authorizationUrl, tokenUrl, refreshUrl, scopes} (Python authorization_code, Rust AuthSchemeDescriptor::Oauth2.authorization_code) and only then; generated clients expose the helpers as client.oauth[scheme] (Rust: client.oauth(scheme)). The credential of the scheme is {flow: "authorizationCode", clientId, clientSecret?, redirectUri?} (client_id, ... in Python and Rust); a bare string is still an access token. There is no interactive flow in the runtime: the application sends the user to authorizationUrl(...), receives the redirect, and calls exchangeCode(...).

  • pkce(): a 43 character URL-safe verifier from 32 random bytes and its S256 challenge, base64url(SHA-256(verifier)) without padding.
  • authorizationUrl: response_type=code, client_id, redirect_uri, scope (space separated; the flow's scopes by default), state, code_challenge with code_challenge_method=S256, then extra parameters in key order that never override these. RFC 3986 percent-encoding.
  • exchangeCode and refresh POST a form to the flow's tokenUrl (refreshUrl for a refresh, tokenUrl when absent): grant_type=authorization_code, code, redirect_uri, code_verifier; grant_type=refresh_token, refresh_token. A client with a secret authenticates with HTTP Basic (client_id:client_secret, percent-encoded); a public client adds client_id to the form. Both return metadata without secrets (expiresAt, scope, refreshable); the tokens go to the store. A refresh keeps the old refresh token when the answer has none.
  • A call reads the store: no token is AUTH_FAILED before anything is sent; a token that expires within 30 s is refreshed first, concurrent callers sharing one refresh request (a token without a refresh token is AUTH_FAILED asking to authorize again); a token endpoint answer of HTTP 4xx to a refresh deletes the stored tokens. A 401 from the API refreshes the token once (when it has a refresh token and was not replaced meanwhile) and sends the same request once more; a second 401 is the envelope. Streams and previews do not retry.
  • Client secret, code, verifier and tokens are scrubbed from envelopes and never appear in middleware contexts; the token endpoint's error text is not repeated (only an error code such as invalid_grant).

Results without a body

A success response without a body (204, or a success status declared without content) is {ok: true, value: undefined}; generated SDKs type such operations as void, or T | undefined when the body is optional.

Rules fixed by the contract suite

  • Retry tiers: defaults.retries reaches the SDK as ApiDescriptor.retries (readOnly, mutating). A call's policy is the runtime default, then its operation's tier, then the client's own retries option, field by field. A mutation is still retried only with a key or an identity body.
  • Unknown outcome: next_action comes from the verification hook (when it can be called without the lost response, see the review rules below); without one, a keyed mutation's names the repeat with the same key (and identical arguments), a content_identity mutation's the repeat with the identical body; otherwise it is null. An ambiguous status keeps the manifest's text for it (errors.non_json, errors.codes) in the remediation.
  • A read never reports OUTCOME_UNKNOWN: a manifest entry mapping a status to it reads as UPSTREAM_UNAVAILABLE, after_delay, with the entry's text.
  • Verification hooks and confirmation messages are written against the API: argument keys, $args.<name> references and {name} placeholders use wire names, resolved to the generated argument names. $response and $args references in expect and terminal are resolved before polling; one that does not resolve never matches.

Rules fixed by review

  • Replay protection decides retryable for an unknown outcome: with a key or an identity body it is same_key_only; without either (an unkeyed create, workflow.action.send, cancel, enable, rotate) it is after_remediation, so an envelope-only policy checks instead of resending.
  • On a mutation, any 5xx (a gateway's 502/504, an intermediary's bare or HTML 500, a JSON 500 with an unknown code) is OUTCOME_UNKNOWN, not "wait, then call again". A manifest entry for the answer's code or non-JSON shape that is a definite rejection (retryable: never / after_remediation, or a category such as PRECONDITION_FAILED, NOT_FOUND, RATE_LIMITED) wins over this and over ambiguous_statuses; one that maps to UPSTREAM_UNAVAILABLE (Acme's unavailable, which can report a failed commit) does not.
  • next_action uses the verification hook only when its arguments do not need $response: after a lost submit the caller has no message_id, so the same-key repeat is the next action.
  • A gate's disabled status is recognized only without the API's error code (the route is not mounted; the mock answers it bare). The mounted route's own JSON 404 is classified by its code.
  • Partial results: a failed result may carry partial (never inside the envelope): the decoded body of a mutation whose success response failed strict validation (the effect happened and the body can hold a one-time secret; strict-validation-keeps-a-created-secret), or a macro's completed step results by as name. A macro's envelope names the macro; when a completed mutating step has no replay protection the envelope says not to rerun the macro, downgrades after_delay / same_key_only to after_remediation and names the failed step as the next call.
  • A confirmation token authorizes one intent. The first call that sends spends it and binds it to that call's replay protection (the key sent, or the identity body); it is accepted again only for a retry under the same protection, and a call without a key can use it once. Macro tokens likewise.
  • A path parameter that would make an empty or dot segment ("", ., ..) is VALIDATION_FAILED before anything is rendered, confirmed or sent: URL parsing removes such segments, so DELETE /items/.. would reach the collection.
  • Sensitive request fields (AgentMeta.sensitiveRequestFields) are redacted in received_value (also below a parent value), in previews, in the body middleware sees (RequestContext.body is the redacted rendering whenever redaction changed a JSON or form body) and are scrubbed from diagnostics.
  • An optional argument set to undefined is absent (TypeScript callers without exactOptionalPropertyTypes pass {cursor: maybe}); the request schemas stay exact.

Logging and telemetry

Structured events through onDiagnostic and onRequest/onResponse, no default stdout logging. The OTel middleware maps operations to spans with http.* attributes and tungsten.safety, tungsten.idempotency.policy, tungsten.outcome.

Event streams (SSE)

ClientCore.stream (TypeScript), stream on ClientCore/AsyncClientCore (Python) and ClientCore::stream (Rust, with the stream's own descriptor) iterate the server-sent events of an operation. Generated SDKs get <method>Stream / <method>_stream; MCP tools whose operation only streams (or whose flag is true) return {events}.

  • The request is Accept: text/event-stream with the stream's request flag set to true. A caller's Accept (the call's, else the client's; any casing of the name) is merged with text/event-stream into one header, never duplicated: its media ranges in order without repeats, text/event-stream first when they do not name it; a q-value the caller wrote stays. Everything before the first byte of a 2xx event stream (validation, auth, status errors, retries by the usual tier rules) is the normal envelope and the only item. A 2xx answer of another type is UNEXPECTED_RESPONSE.
  • The body is parsed per WHATWG HTML 9.2 (LF/CRLF/CR, split chunks, comments, multi-line data, event, id and retry persisting across events, a leading BOM dropped, an event still open at the end discarded). data is JSON; the declared done value ends the stream without an item.
  • Each item carries value, event (default message), id, retry and the response meta. Events are checked against the event type per validateResponses: strict ends the stream with UNEXPECTED_RESPONSE (events[N].path), warn reports through onDiagnostic and delivers, off does not look. The Rust SDK decodes into generated types, so an event that does not decode ends the stream in every mode.
  • A started stream is not retried by the tier rules, but it can be resumed (below). A lost connection or a stream idle for the attempt timeout ends it with one final failure item: TRANSPORT_FAILED or UPSTREAM_UNAVAILABLE for a read, OUTCOME_UNKNOWN (with the usual check-before-repeating rule) for a mutation. Leaving the loop closes the connection.
  • Resuming: a connection that drops after the stream started (a transport error, or a clean end before the done event the operation declares; a stream that declares none ends cleanly) is reconnected with Last-Event-ID set to the last event id delivered, up to maxReconnects / max_reconnects times (default 3; 0 turns it off, and a clean end is then a clean end as before). Only a call that is safe to repeat is reconnected: a read, or a mutation with replay protection (the same request goes out again, idempotency key included). It needs an event id to resume from, or no event delivered yet (then the request is simply repeated). The wait before a reconnect is the server's last retry value (250 ms when it sent none) capped at reconnectMaxMs / reconnect_max_ms / reconnect_max (default 30 s) and shortened by up to 25 % at random (so it never exceeds the cap). A server that ignores Last-Event-ID and replays events is harmless: after a reconnect, events whose id was already delivered are skipped until the first new one. The reconnect is a new request through the usual pipeline (its failure, an error status or a body that is not an event stream, is the final item as an ordinary envelope); when the reconnects are used up the final item is the failure of the last drop and its remediation says how many reconnects were made. Items report reconnects in their meta (the stream so far), id and retry carry over a reconnect. The caller's abort signal / cancel / drop ends the stream for good, never reconnects.
  • Idle timeout: idleTimeoutMs / idle_timeout_ms / idle_timeout (default none) is the silence, bytes of comments included, after which a stream is idle. An idle stream is reconnected like a dropped one; when it cannot be, it ends with UPSTREAM_UNAVAILABLE (read) or OUTCOME_UNKNOWN (mutation) whose code is STREAM_IDLE. Without it the attempt timeout ends a silent stream as before, without a reconnect and without the code. The category set is unchanged: STREAM_IDLE is a code value, the one the runtime itself sets (envelope schema, code).
  • Helpers, in every runtime: the stream can be consumed with on (TypeScript and Python: a handler per event name, * for the others; Rust: EventHandlers), collect (TypeScript and Python; Rust has collect_values), reduce (fold the events, e.g. concatenate text deltas), first(predicate) (then close the stream) and cancellation (TypeScript cancel() or the AbortSignal; Python cancel(), also from another thread or task, which ends a blocked read; Rust: drop the stream). They return the result and reconnects, or the failure with what was gathered before it (partial, TypeScript and Python; Rust Folded). cancel() ends the stream without a failure item.
  • Size limit: one event holds at most maxEventBytes / max_event_bytes (client option, default 1 MiB): the UTF-8 bytes of its field lines, each counted with a one-byte terminator, plus the line being read; a finished comment line is not kept and does not count (an unfinished one counts like any line). Past it the parser stops, the events before it are delivered and the stream ends with UNEXPECTED_RESPONSE (failed_parameter events[N], expected an event of at most N bytes), never buffering more than the limit plus one chunk. The same in TypeScript, Python (sync and async) and Rust.
  • Collection budgets: collecting a stream into a list is bounded. The MCP tool of a streamed operation stops after 1000 events (truncated: true), and fails with UNEXPECTED_RESPONSE (failed_parameter events, the events so far as partial) after maxStreamBytes of event JSON (default 4 MiB) or maxStreamMs (default 60 s, whatever the idle timeout; the connection is closed); both are ServerOptions. The Rust dynamic Dispatch::stream (generated collect_events, EventStream::collect_values) ends the same way, last item an error, after ClientOptions::max_collect_bytes (4 MiB) or max_collect_time (60 s). Python's collect() is not bounded; its callers bound it themselves.
  • MCP: a streamed tool call reads the stream through ClientCore.stream, so it resumes like any other (the server's client options apply). When the tools/call request carries a progressToken, each event collected is a notifications/progress (progress is the number of events so far, message says how many were collected and how often the stream reconnected), so progress keeps flowing across a reconnect.
  • Mock: tungsten mock serves a generated stream from the event after the numeric Last-Event-ID of the request (the done sentinel has no id and stays), and the injections drop-after=<n>[;retry=<ms>] (the first n events, then the connection breaks) and stall-after=<n>[;ms=<ms>][;retry=<ms>] (then silence for ms, default 30 s) make a resumable and an idle stream; retry starts the stream with a retry: line. Programs can apply them (inject); a scripted body still replays whatever it says, which is how the contract tests a server that ignores Last-Event-ID.
  • Detection (build): the stream is carried by an exact success status, else a 2XX range, else default (a default that declares text/event-stream and has no sibling success response becomes the success response; next to a success response it stays an error response and carries no stream).

MCP server guide

tungsten generate with an mcp target writes an MCP server that calls the API through the generated TypeScript SDK, so it inherits the same safety rules. This page covers how the tool surface is chosen, what the meta-tools do, and what a generated server's README says.

How the tool surface is chosen

Below disclosure.threshold tools (default 24) the MCP server exposes discrete tools. Above it, it exposes search_tools, describe_tool, invoke and the per-cluster index. The index is a keyword index built at compile time (BM25 over names, summaries, parameter names, tags). Embedding indexes are an optional plugin because they would make the MCP server depend on a model download.

As built: embedding index plugin. Off by default, no model shipped, nothing in the manifest changes. targets.mcp.search.embeddings in tungsten.yml names a provider; when tungsten generate writes the target it sends one document per tool (the terms of its BM25 document, in order, joined by spaces: name words, summary, parameter names, resources, tags, cluster) to that provider in batches and writes two files next to manifest.json: index.embeddings.bin (count x dimensions little-endian f32, row-major, tool order, each vector L2-normalized, so cosine is a dot product) and index.embeddings.json (a header: format 1, model, dimensions, count, encoding, normalized, the BLAKE3 checksum of the .bin file and docs, the tool name and content hash of each document). Providers: command (a command line run from the directory of tungsten.yml: one JSON request on standard input, {"embeddings": [[...], ...]} on standard output) and http (the common /embeddings shape: POST of {model, input, dimensions?, encoding_format}, answer {data: [{index, embedding}]}, Authorization: Bearer from the environment variable named by api_key_env; the key is never written anywhere). The compiler links no TLS stack, so its http provider is plain http:// only (a local gateway, or a command that runs curl); the server's own query embedder has no such limit. Vectors are cached by content hash in the previous run's files, so unchanged tools are not embedded again and a model or dimension change invalidates the cache; the provider is called only by a write (generate), never by --check, --dry-run, diff or report, which build the files from the cache alone (and leave them out, so --check reports them stale, when it does not cover every tool). A provider failure never breaks generation: it is a warning (TG0571 cannot run or too slow, TG0572 invalid answer, TG0573 refused, no key or https), the stale index is removed and the server searches with BM25; required: true makes the same problems errors. A bad search.embeddings is TG0570; TG0574 reports what was written.

Tools and behaviour

Stdio transport by default, Streamable HTTP optional. Protocol versions negotiated as the SDK of the day supports.

Discrete mode (operation count ≤ threshold): one tool per operation and per macro, name <namespace>_<resource>_<op> (Acme: public_webhooks_list), compact description, input schema from the IR with pruning, outputSchema, annotations from the safety tier.

outputSchema is bounded (fix/mcp-output-closure). A response type's transitive $defs closure is the whole API on specs with cyclic, densely connected response graphs (Stripe: every expandable field is anyOf [string, Resource]; one tool's closure was ~860 definitions and 1 MB, the discrete listing ~29M tokens). The rule, in tungsten-emit-mcp manifest::bound_closure: a $ref more than OUTPUT_HOPS (1) reference hops from the response body is replaced by {} (any value, so the real structuredContent still validates), then the unreachable $defs are dropped; hoisted shared_* scalars never count as a hop. When the result is still over OUTPUT_BUDGET_FACTOR (4) times defaults.disclosure. schema_budget_tokens, every reference is {} (0 hops). A tool still over that budget is TG0727. TG0721 keeps counting description plus input only (schemaTokens is that figure in the manifest). The runtime needs no change: advertisedOutputSchema passes $defs and the body through.

Progressive mode:

ToolPurpose
describe_tool(name)Full input/output schema, remediation table, example call
invoke(name, args, options?)Executes through the SDK; returns body or envelope; options.confirmation_token for destructive/irreversible
preview(name, args)Runs the preview mode; returns summary and confirmation_token
list_clusters()The manifest's clusters with counts
run_script(code)Optional sandbox

Behaviors:

  • Results carry structuredContent and a text rendering; errors are the envelope, never thrown as protocol errors, so the model sees remediation.
  • Sensitive fields (sensitive_response_fields) are returned once and replaced with "<redacted after first read>" on repeated identical calls within the session.
  • Confirmation tokens are HMAC over (session, tool, args digest) with a short TTL; the server never accepts a token for different arguments.
  • shown_once responses are flagged in the text rendering: "Store this secret now; it will not be shown again."

Details

The emitter is tungsten-emit-mcp (target mcp); it writes an McpManifest (manifest.json, the contract in the compiler) and a small server package on top of the generated TypeScript SDK; all behaviour lives in @elysra/tungsten-mcp.

  • src/server.ts reads the manifest with loadManifest(new URL("../manifest.json", import.meta.url)), builds one ClientCore per MCP session from the environment (one variable per credential, the base URL, <API>_MCP_MODE) and starts with serve(server, parseServeArgs(argv)): stdio by default, Streamable HTTP with --http <port> [--host <host>] (the URL is written to stderr; SIGINT and SIGTERM close every session). The command line selects the transport only; credentials never come from arguments.
  • invoke and preview take {name, arguments} (discrete preview takes {tool, arguments}); the reserved input fields idempotency_key and confirmation_token (prefixed _ when an argument has that name) are taken out before the SDK call.
  • The manifest carries instructionsByMode (additive): the runtime sends the instructions of the mode in effect, so a server whose mode is overridden does not describe the other mode's tools.
  • Descriptions name the fields a tool returns once (returns signing_secret_b64url once), so a client that has no output schema (a discrete macro, a search result) knows which value to store.
  • Macro previews: the text rendering lists every step (N. <kind> <operation> (<tier>, result as <as>): <method> <url> with body ..., then its effects) with the <from step NAME: path> placeholders; structuredContent is the PreviewResult with its steps.
  • Envelopes stay the runtime's; when their remediation or next_action names SDK operations, the text adds how this server calls them (Here public.listWebhookEndpoints is called as invoke({"name": "public_webhooks_list", ...})).
  • Token budgets count what the runtime serves: tungsten_emit_mcp:: tools_list renders tools/list of either mode exactly as @elysra/tungsten-mcp does (the advertised output schemas with the envelope branch, the redaction markers and the verification result; the preview meta tool; the cluster enum), and listing_tokens adds that mode's instructions. TG0722, the README table and tungsten report use it; the MCP gate (mcp-gate/listing.mjs) checks that the built runtime serves the rendering byte for byte for the gate cases in both modes.

The script sandbox

run_script executes agent-written TypeScript against the generated SDK. node:vm is not a security boundary, so the default implementation spawns a Deno subprocess with --allow-net=<api host> only, a wall-clock limit and a memory limit, and is disabled unless deno is on PATH and mcp.sandbox.enabled: true. A WASM-based isolate is a later option.

A generated server's README

This is the README that tungsten writes next to a generated server (here for a three-operation example API).

openapi-mcp

MCP server for the openapi API, generated by tungsten. It serves 3 tools (3 operations, 0 macros) over stdio through @elysra/tungsten-mcp, calling the API with the generated SDK openapi-sdk.

Run

npm install
npm run build
node dist/server.js    # or `openapi-mcp` once installed

The server speaks MCP over stdio by default. node dist/server.js --http 3000 serves Streamable HTTP at http://127.0.0.1:3000/mcp instead (one session, with its own confirmation tokens, per Mcp-Session-Id; --host <host> binds another interface, --help lists the flags).

Client configuration (the mcpServers entry of Claude Desktop, Claude Code and other MCP clients):

{
  "mcpServers": {
    "openapi": {
      "command": "npx",
      "args": [
        "-y",
        "openapi-mcp"
      ],
      "env": {
        "OPENAPI_BASE_URL": "..."
      }
    }
  }
}

For a local build use "command": "node" with the absolute path of dist/server.js as the argument.

Environment

Credentials are read from the environment only, never from arguments; .env.example lists every variable. Set only those of the schemes your operations need.

VariableMeaning
OPENAPI_BASE_URLBase URL of the API (default http://petstore.swagger.io/v1).
OPENAPI_MCP_MODEdiscrete or progressive: overrides the generated disclosure mode.

Modes

This server starts in discrete mode (3 tools, list budget 10000 tokens; agent.yml defaults.disclosure: auto is progressive when the discrete tool list is over the budget).

  • discrete: one MCP tool per operation and macro, with input and output schemas and annotations from the safety tier.
  • progressive: a few tools instead of the whole list. search_tools(query, cluster?, limit?) searches a BM25 index built at generation time, describe_tool(name) returns one tool's schemas and rules, invoke(name, arguments) calls it (invoke_read(name, arguments) calls read-only tools and is annotated readOnlyHint, so a host can allow it without confirmation; search_tools and describe_tool results carry each tool's safety tier and annotations, and invoke, invoke_read and preview results carry them in _meta["tungsten/tool"]), preview(name, arguments) shows a call without sending it, list_clusters() lists the groups of tools.

Set OPENAPI_MCP_MODE to discrete or progressive to override the mode.

Safety rules

  • Read-only tools are annotated readOnlyHint; destructive and irreversible ones destructiveHint.
  • Failures are returned as the diagnostic envelope (category, retryable, remediation, next_action), never as protocol errors. After OUTCOME_UNKNOWN, check the outcome before any retry.
  • Planned operations are not tools.

Token budget

What a client receives before its first call (the tools/list answer of each mode and its initialize instructions) and each tool's description and input schema, measured at generation time with the estimate counter of tungsten-tokens.

MeasureTokensBudget
Progressive tools/list and instructions (with the cluster index)8332000
Discrete tools/list and instructions (3 tools)529
Largest tool schema (openapi_pets_list)51600
Median tool schema48600

Browser WebMCP guide

The built-in webmcp target generates an ESM npm package that exposes your API as browser tools through the generated TypeScript SDK. Your frontend explicitly registers the tools when a page is ready and removes them when that page leaves. No MCP server or stdio process is needed.

WebMCP is experimental. Its current API is document.modelContext, with async registerTool(tool, {signal}) and abort-based removal. The runtime can also adapt the legacy navigator.modelContext shape. Browser support varies: detect the capability and let your application continue when it is unavailable. The checks include structural adapters and a generated-package native smoke passed in Chromium 154.0.8037.57 with WebMCPTesting enabled. That smoke covers document backend detection, page-scoped registration, real HTTP execution, argument validation before requests, duplicate rejection, abort/disposal preserving unrelated host tools, and re-registration. Full cross-browser conformance is future work.

Generate and build

Add a browser target alongside the TypeScript SDK in tungsten.yml:

targets:
  typescript:
    package: "@acme/petstore"
    version: "1.0.0"
    out: generated/typescript
  webmcp:
    package: "@acme/petstore-webmcp"
    out: generated/webmcp

Run tungsten check and tungsten generate. Install and build the generated TypeScript SDK first, then install and build the browser package:

(cd generated/typescript && npm install && npm run build)
(cd generated/webmcp && npm install && npm run build)

The target accepts package, version, sdk, sdk_path, runtime, runtime_path, sdk_runtime and sdk_runtime_path, plus out. Without overrides, the sibling TypeScript target supplies the SDK package and version. runtime selects the @elysra/tungsten-webmcp version range; sdk_runtime selects the browser package's direct @elysra/tungsten-runtime dependency (default ^0.1.0). Published dependency defaults use version ranges.

For development from a compiler source checkout, build runtimes/ts and runtimes/webmcp first (npm ci && npm run build in each, in that order). Then configure all local dependencies explicitly, replacing /path/to/tungsten with your compiler checkout:

targets:
  typescript:
    package: "@acme/petstore"
    out: generated/typescript
    runtime_path: /path/to/tungsten/runtimes/ts
  webmcp:
    package: "@acme/petstore-webmcp"
    out: generated/webmcp
    sdk_path: ../typescript
    runtime_path: /path/to/tungsten/runtimes/webmcp
    sdk_runtime_path: /path/to/tungsten/runtimes/ts

Relative dependency paths are relative to the generated package directory. The two browser runtime paths select separate packages; both are needed for a fully local install. Regenerate, then install/build the SDK before the browser package using the commands above. Bundle the browser package through your existing frontend toolchain. The output contains manifest.json, src/index.ts, a preserved src/custom/index.ts, build configuration and a README. The main module exports manifest and register.

Register for a page

import { register, manifest } from "@acme/petstore-webmcp";
import { detectWebMcpBackend } from "@elysra/tungsten-webmcp";

const backend = detectWebMcpBackend();
if (backend) {
  const lifetime = new AbortController();
  const readNames = manifest.tools
    .filter(tool => tool.kind === "operation" && tool.safety === "read_only")
    .map(tool => tool.name);
  const registration = await register({
    backend,
    client: { baseUrl: new URL("/api", window.location.origin).href },
    allowlist: readNames, // narrow this to the reads needed on this page
    signal: lifetime.signal,
  });
  console.log(registration.registeredNames, registration.backend);
  // In your route/component cleanup:
  // lifetime.abort();
  // await registration.dispose();
}

Importing the package has no registration side effects. Without an allowlist, only read-only operations register. An explicit allowlist selects exact catalog names, including writes or macros when requested; an empty allowlist exposes nothing. Use the manifest to select tools for the current page. Dispose an old registration before replacing its scope. Disposal removes only tools owned by that registration and cancels its active executions.

The helper constructs a ClientCore using the SDK's descriptors and your ClientOptions. Supply the API base URL and browser session from your application. Generated browser code does not read environment variables or bundle service credentials. Use an authenticated same-origin API proxy or appropriate short-lived user credentials. The API must enforce authorization, CORS and CSRF protections. WebMCP requires the browser's supported secure context.

Safety, results and confirmation

Browser input schemas are not enforced by the browser. SDK argument validation, idempotency, diagnostic envelopes and confirmation rules still apply. Browser tool annotations describe intent; they are hints, not an authorization boundary.

When an explicitly selected operation requires a confirmation token, registration also adds <tool-name>__preview. Preview prepares the request and effects without executing the mutation. Pass its token in the original tool's reserved confirmation field with identical arguments. Keep caller-owned idempotency keys stable across retries. After OUTCOME_UNKNOWN, follow the diagnostic's verification action.

Tool execution receives the browser's cancellation signal and returns a JSON-serializable result. The runtime limits UTF-8 JSON output to 1 MiB by default; set maxResultBytes when registering to adjust the limit. Oversize output fails explicitly. Streaming and binary output require an explicit bounded result policy; see tool results. Tools with sensitive or one-time response fields are rejected before an API call. Await dispose() or abort the lifetime signal on route teardown.

For custom integration, the runtime also exports registerTungstenWebMcp({core, manifest, macros?, allowlist?, backend?, signal?, maxResultBytes?}). It returns a promise for {registeredNames, backend, dispose(): Promise<void>}. The generated src/custom/index.ts hook is preserved across regeneration.

Scope and API references

The adapter registers discrete tools with explicit page scope. The lifecycle controller connects scoped dynamic loading to route changes and React or Vue cleanup. The tool catalogue describes transport-neutral groups. Full cross-browser conformance and progressive browser meta-tools remain follow-ups. Server sandbox execution and run_script are not part of this browser package.

Track the WebMCP specification, implementation status and Chromium IDL for changes to this experimental API.

Browser lifecycle and scoped loading

Use createWebMcpRegistrationController from @elysra/tungsten-webmcp to connect page or route lifetimes to browser tools. It has no React or Vue dependency. Imports and controller construction are safe during SSR; without a supported backend, replace returns null before invoking the loader.

import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";

const tools = createWebMcpRegistrationController();
await tools.replace(async ({ signal }) => {
  const page = await import("./tool-scopes/pets.js");
  signal.throwIfAborted();
  return page.createScope();
});
// Before changing pages, replace with the next page's loader.
// For a page with no tools:
await tools.clear();
// When permanently retiring this controller:
await tools.dispose();

A scope factory returns { core, manifest, macros?, allowlist? }, with the same options as registerTungstenWebMcp except backend and signal. Optional result limits and streaming/binary policies can also be returned. Put imports of scoped manifests/descriptors and core construction inside the dynamically imported module and its factory. Importing a large catalog at application startup and filtering it inside the loader does not defer its download or construction. Avoid a module that re-exports every group; import the specific group's entry point. Check your bundler's chunk report to verify that scope modules are separate chunks. Dynamic import cancellation prevents registration; the browser may still finish downloading an import already in progress.

Generated WebMCP packages already provide scoped modules. Use the moduleKey listed in their catalogue's groups:

await tools.replace(async ({ signal }) => {
  const group = await import("my-api-webmcp/groups/public_pets");
  signal.throwIfAborted();
  return group.createScope({ baseUrl: new URL("/api", window.location.origin).href });
});

Each module includes its operation dependencies and reachable models. Its default registration exposes reads; add an explicit allowlist to the returned scope when the page needs a mutation or macro.

Keep one core for the entire active scope. Preview tokens and execution must use that same core/session. Return a newly constructed core on a fresh scope load; never construct a new core inside a tool's execution callback. Scope manifests should contain only the tools for that page, and their operations, macro steps, endpoint previews and verification targets must be registered with the core. Default registration exposes read-only operations. Add consequential operations or macros only through an explicit allowlist of manifest tool names; preview and confirmation requirements still apply.

replace immediately aborts the preceding scope's lifetime, cancels in-flight SDK calls, disposes owned tools, then loads the new scope. Concurrent loads do not block a newer route even if an old loader ignores its signal. Only the latest load may register. Superseded loads return null, including rejected obsolete imports; current import and registration failures reject. Browser registration and cleanup are serialized, so a delayed browser registration rolls back before the next scope registers. Cleanup never clears another owner's catalog. A failed legacy cleanup rejects and can be retried with clear() or dispose().

React

Pass a stable loader function, typically a route-to-loader map declared outside the component. This hook cancels the preceding route and supports React's additional development effect cleanup/setup cycle. clear() keeps the controller reusable; use dispose() when permanently retiring an application-owned controller.

import { useEffect, useMemo } from "react";
import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";
import type { WebMcpScopeLoader } from "@elysra/tungsten-webmcp";

export function usePageTools(loadScope: WebMcpScopeLoader) {
  const tools = useMemo(() => createWebMcpRegistrationController(), []);
  useEffect(() => {
    void tools.replace(loadScope).catch(reportToolError);
    return () => { void tools.clear().catch(reportToolError); };
  }, [tools, loadScope]);
}

function reportToolError(error: unknown) {
  console.error("Page tools could not be updated", error);
}

Call usePageTools(routeLoaders[routeName]) from the component that owns the active page. Do not register during render. An unsupported browser leaves the page's normal UI available without constructing its SDK core.

Vue

Watch the route after mount, and clear the same controller on unmount. Keep loaders in a stable map so a watch update represents a route change.

import { onBeforeUnmount, onMounted, watch } from "vue";
import { createWebMcpRegistrationController } from "@elysra/tungsten-webmcp";
import type { WebMcpScopeLoader } from "@elysra/tungsten-webmcp";

export function usePageTools(
  activeRoute: () => string,
  loaders: Record<string, WebMcpScopeLoader>,
) {
  const tools = createWebMcpRegistrationController();
  let stop: (() => void) | undefined;
  const report = (error: unknown) => console.error("Page tools failed", error);
  onMounted(() => {
    stop = watch(activeRoute, route => {
      const loader = loaders[route];
      if (loader) void tools.replace(loader).catch(report);
      else void tools.clear().catch(report);
    }, { immediate: true });
  });
  onBeforeUnmount(() => {
    stop?.();
    void tools.dispose().catch(report);
  });
}

Authenticated same-origin transport

Use the application's authenticated same-origin API or backend-for-frontend. The browser session cookie remains managed by the browser. Do not embed service API keys or client secrets in a bundle, manifest, tool schema or loader.

// Pass these ClientOptions into the scope's ClientCore or generated client.
const origin = window.location.origin;
const clientOptions = {
  baseUrl: new URL("/api", origin).href,
  fetch: ((input, init) => {
    const url = new URL(input instanceof Request ? input.url : String(input), origin);
    if (url.origin !== origin) throw new Error("Expected a same-origin API URL");
    return window.fetch(input, { ...init, credentials: "same-origin" });
  }) satisfies typeof fetch,
};

Create these options inside the client-only loader or scope factory. Preserve init.signal and the SDK's headers when wrapping fetch so route cancellation, request diagnostics and idempotency continue to work. Have the application add its normal CSRF header for cookie-authenticated mutations; do not expose the CSRF value as a tool parameter. Backend authorization remains authoritative for every request, and an allowlist is a page exposure decision. After sign-out or a change of authenticated principal, clear the scope and load a fresh core for the new session. Cancellation of an in-flight mutation does not prove it had no effect; inspect server state before retrying.

WebMCP tool results

The browser adapter returns the SDK's JSON result envelope, including diagnostics, confirmation previews, HTTP metadata and verification outcomes. Results have a UTF-8 JSON limit (maxResultBytes, default 1 MiB). Exceeding a limit returns a failure; output is never silently clipped. A rendering failure after an API call cannot undo the call. Inspect API state before retrying consequential operations.

Streaming and declared binary responses reject before HTTP by default. Secret response fields and values shown only once remain blocked with every result policy. Use trusted SDK application code for those outputs.

Bounded SSE collection

For an operation with an SDK stream descriptor, explicitly enable collection:

const registration = await registerTungstenWebMcp({
  core, manifest, backend,
  streamCollection: {
    maxEvents: 100,
    maxBytes: 256 * 1024,
    maxDurationMs: 10_000,
  },
});

Every limit is required and must be a positive safe integer; duration is at most 2,147,483,647 ms. maxResultBytes must exceed 1024 for collection. The adapter uses ClientCore.stream, so SDK authentication, SSE Accept headers, validation, confirmation and idempotency remain authoritative. Streaming macros are rejected.

Success is { ok: true, value: events, reconnects }. Each event retains its SDK value, event, id, retry and meta. An SDK failure retains its diagnostic and adds partial (previous events) and reconnects. Adapter count, byte and duration failures also return partial. Partial output is incomplete and is not an instruction to retry. Adapter interruption of a mutation uses OUTCOME_UNKNOWN and directs the caller to inspect API state. SDK diagnostics retain their original classification and effect guidance.

The byte budget counts UTF-8 JSON event envelopes and separators. The outer result budget reserves 1024 bytes for envelope/diagnostic overhead. The SDK separately bounds individual SSE events with ClientOptions.maxEventBytes; configure that limit for the upstream API. Collection ends only on complete stream termination; reaching a count limit while another event arrives is a failure. Time limits and caller abort cancel the SDK reader. Caller abort leaves registration available; registration disposal cancels active calls and unregisters owned tools.

JSON Lines/NDJSON responses use ClientCore.call and its array result envelope, not SSE collection. Configure SDK response validation as strict when malformed JSON Lines must fail rather than produce the SDK's default warning. With an explicit result policy, non-SSE HTTP bodies are bounded while reading by the adapter's maxResultBytes (or the declared binary policy's maxBytes).

Binary output

Small, reviewed binary outputs can be converted to JSON base64:

binaryResults: { mode: "base64", maxBytes: 64 * 1024 }

The result value is { kind: "binary", encoding: "base64", mediaType, byteLength, data }, retaining the outer SDK envelope. Base64 expansion must fit the JSON result budget. The HTTP reader checks both declared Content-Length and actual received bytes, cancels on overflow, and never retries an oversized response.

Applications can instead render an expiring download handle:

binaryResults: {
  mode: "download",
  maxBytes: 256 * 1024,
  async render({ bytes, mediaType, operation, signal }) {
    return applicationDownloads.create({ bytes, mediaType, operation, signal });
    // { url: "https://downloads.example.test/opaque-handle", expiresAt: epochMs }
  },
}

The adapter accepts HTTPS URLs without embedded credentials and future integer expiry timestamps. The JSON value is { kind: "download", mediaType, byteLength, url, expiresAt }. The application must enforce expiry, access control and its content policy; a timestamp alone does not expire a URL. The renderer receives an AbortSignal and should cancel its own work. No browser Blob URLs are generated, and secret-bearing outputs never reach the renderer. Invalid handles and binary limits return explicit failures with guidance about possible API effects.

Regeneration and migration reports

Generate an SDK once, change the input specification or target options, then review the proposed regeneration before writing it:

tungsten diff --migration
tungsten diff --migration --json > migration.json

--migration implies --semver. It compares the stored .tungsten/surface.json with the newly compiled API and emitted package manifests. Each target's semver.migration.actions contains a stable rule, subject and recommended action. Human output includes the same guidance for renamed or removed methods, required inputs, changed types, agent tool names, deprecations and runtime requirements. Changes remain sorted deterministically.

diff runs emitters in memory and writes no generated files. Files beneath a directory named custom are excluded from regeneration diffs and preserved by generate; reports expose preserves_custom_files: true. Review changes and run consumer tests before running tungsten generate.

Snapshot format 3 records the generated package name, version and dependency requirements from npm, Python and Rust SDK manifests. A package rename can break imports and is classified major. A package version change is release metadata and does not imply a breaking API change. Dependency requirement changes carry runtime_dependency_changed and an action to inspect runtime compatibility, update installation declarations and lockfiles, and run integration tests. They do not invent an API major bump or claim that the installed runtime is compatible.

Format 2 snapshots remain readable. Their missing package/dependency metadata is unknown, rather than an empty dependency list: runtime_requirements_known is false, and migration guidance requests manual inspection of the previous package manifest. Existing API signature classification still works. A missing or unreadable snapshot leaves the semantic level null with a diagnostic; a future unsupported snapshot format is not silently interpreted.

Tool catalogue

The version 1 tool catalogue is the public discovery contract shared by MCP, WebMCP and external emitters. tungsten schema tool-catalog prints its JSON Schema. Rust integrations use tungsten_emit::tool_catalog::build(&ir) and validate with tool_catalog::validate. Its schema identifier is https://tungsten.dev/schemas/tool-catalog-v1.json.

A catalogue contains catalogVersion, API and generator versions, tools and named groups. Every visible callable operation and supported macro has a deterministic name, its operation ID or macro name, a descriptor reference, arguments and optional response JSON Schema, safety tier, idempotency policy, collision-safe reserved fields, cluster, and sensitive response metadata. Planned and hidden operations are excluded. Invalid macros produce diagnostics and are excluded. Names use ASCII snake case, at most 48 characters; collisions receive stable ID digests.

Arguments use the SDK's compact argument layout. Reserved idempotency and confirmation fields receive leading underscores when they conflict with API arguments. Caller-owned persisted keys are required. Destructive and irreversible tools require preview confirmation. Output schemas describe the first JSON object success response; streams and non-object responses have no output schema. Response reference closures are bounded to keep discovery small; schema references are local and resolvable within each schema. A catalogue contains no credential values, environment values, output paths, search indexes or server instructions.

descriptor: { kind, id } identifies an operation or macro in the generated SDK's descriptor document. It is an identity reference, not an import path. Adapters resolve it using their runtime's descriptor registry. MCP adds annotations, measured token budgets, BM25 disclosure and initialization instructions. WebMCP adds manifestVersion and browser registration. Both reuse the same shared name, argument schema, safety and reserved-field compiler.

Browser resource groups

Generated WebMCP packages export neutral discovery data at <package>/catalog. They also export <package>/groups/<group-key>: each module contains only that group's tool manifest, operation dependencies and reachable model schemas. Group keys are normalized from cluster names or namespace/resource paths using the same stable collision-aware naming rule. The catalogue's groups list gives each group's moduleKey and tool names. Importing a group does not import the eager SDK client or full catalogue and does not register tools.

await controller.replace(async () => {
  const group = await import("my-api-webmcp/groups/public_pets");
  return group.createScope({ baseUrl: "/api" });
});

createScope(client) returns a core, manifest and macro registry when needed. Registration defaults to read-only tools. Explicitly opt into mutations or macros by adding allowlist: ["exact_tool_name"] to the returned scope. The host chooses the backend and lifecycle signal. Preview, verification and macro step dependencies remain available to the scoped core without exposing extra tools. Shared model dependencies can occur in multiple groups; actual bundling and chunk boundaries remain the application's choice.

External emitter conformance

External emitters keep JSON protocol version 1. They can compile a catalogue from the request's existing IR without a new protocol field. Validate the catalogue against the published schema and with tool_catalog::validate, resolve descriptor IDs in the generated SDK document, and preserve safety and reserved fields.

tool_catalog::external_conformance(&catalog, &first, &second) accepts two independently produced protocol responses. It rejects unsupported versions, dangling or external schema references, invalid safety metadata, reassigned catalogue tool identities, unsafe/duplicate output paths, and differing output bytes or ordering. Conformance fixtures use UTF-8 text files. The helper performs no process, network or filesystem reads and receives no credentials. It supplements the external runner's normal timeout, output size and file guards. Run the emitter twice on the same request and compare the responses; timestamps, absolute machine paths and nondeterministic iteration fail the check.

SDK guides

Each SDK target is a typed client plus a README that tungsten generates next to it. The three SDKs behave the same way (see safety and runtime behaviour): calls return a result value instead of throwing for API or transport errors, arguments are validated before anything is sent, and destructive operations go through a preview.

TargetGuideRuntime it depends on
typescriptTypeScript SDK@elysra/tungsten-runtime
pythonPython SDKtungsten-runtime (PyPI)
rustRust SDK and CLItungsten-runtime and tungsten-cli-kit (crates)
docsDocs targetnone

The pages in this section are the READMEs generated for a small example API (Swagger's Petstore), so you can see exactly what a generated project tells its users.

TypeScript SDK

This is the README that tungsten generates for the TypeScript target, shown for a small example API (Swagger's Petstore). Names change with your API; the structure does not.

TypeScript SDK for the openapi API (1.0.0), generated by tungsten.

Every call returns a Result and never throws for API or transport errors. Arguments are validated before any request is sent.

Install

npm install openapi-sdk

Create a client

import { OpenapiClient } from "openapi-sdk";

const client = new OpenapiClient({
  baseUrl: "http://petstore.swagger.io/v1",
});

Read

const res = await client.pets.list();
if (res.ok) console.log(res.value);
else console.error(res.error.category, res.error.remediation);

Change something, safely

Mutations are never retried blindly. When the operation takes an idempotency key, generate it once, persist it with your intent, and reuse it on every retry.

const idempotencyKey = crypto.randomUUID();
const res = await client.pets.create({}, { idempotencyKey });

Errors

A failed call returns { ok: false, error }, where error is the diagnostic envelope:

{
  "status": "error",
  "category": "VALIDATION_FAILED",
  "operation": "<operation id>",
  "http_status": null,
  "code": null,
  "failed_parameter": "<args path>",
  "received_value": null,
  "expected": "<what was expected>",
  "remediation": "<what to do next>",
  "retryable": "never",
  "retry_after_ms": null,
  "next_action": null,
  "request_id": null,
  "trace": { "attempts": 0 }
}

Branch on category and retryable: same_key_only means retry only with the same idempotency key, after_remediation means act first (fix the input, or check whether an unknown outcome took effect: without a key a repeat can apply it twice), and OUTCOME_UNKNOWN means the request may have been applied. When a mutation or macro already took effect, the failed result's partial holds what it produced (store one-time secrets from it). A success response without a body has value: undefined.

Extending

src/custom/index.ts is yours: it is created once, never overwritten, and exported as custom.

Python SDK

This is the README that tungsten generates for the Python target, shown for a small example API (Swagger's Petstore). Names change with your API; the structure does not.

Python SDK for the openapi API (1.0.0), generated by tungsten.

Every call returns a Result and never raises for API or transport errors. Arguments are validated before any request is sent. Python 3.12 or newer; models are Pydantic v2.

Install

pip install openapi-sdk

Create a client

from openapi_sdk import OpenapiClient

client = OpenapiClient(
    base_url="http://petstore.swagger.io/v1",
)

Read

from openapi_sdk import Ok

res = client.pets.list()
if isinstance(res, Ok):
    print(res.value)
else:
    print(res.error["category"], res.error["remediation"])

The async client has the same methods, awaited:

import asyncio

from openapi_sdk import AsyncOpenapiClient

async def main() -> None:
    async with AsyncOpenapiClient(base_url="http://petstore.swagger.io/v1") as client:
        res = await client.pets.list()
        print(res.ok)

asyncio.run(main())

unwrap(res) (from openapi_sdk import unwrap) returns the value or raises TungstenError with the envelope, for code that prefers exceptions.

Change something, safely

Mutations are never retried blindly. When the operation takes an idempotency key, generate it once, persist it with your intent, and reuse it on every retry.

import uuid

key = str(uuid.uuid4())
res = client.pets.create(opts={"idempotency_key": key})

Models and optional fields

Request and response bodies are Pydantic models (from openapi_sdk.models import ...). A field that may be left out defaults to UNSET (exported by this package), which is not sent; None sends JSON null. model_dump(mode="json", by_alias=True) gives the wire form. Fields whose wire name is not a Python name are renamed: a keyword or a name the model needs gets _ appended (type_, json_) and a leading digit gets field_ in front (field_2fa); the wire name is used on the wire.

Errors

A failed call returns Err(error), where error is the diagnostic envelope:

{
  "status": "error",
  "category": "VALIDATION_FAILED",
  "operation": "<operation id>",
  "http_status": null,
  "code": null,
  "failed_parameter": "<args path>",
  "received_value": null,
  "expected": "<what was expected>",
  "remediation": "<what to do next>",
  "retryable": "never",
  "retry_after_ms": null,
  "next_action": null,
  "request_id": null,
  "trace": { "attempts": 0 }
}

Branch on category and retryable: same_key_only means retry only with the same idempotency key, after_remediation means act first (fix the input, or check whether an unknown outcome took effect: without a key a repeat can apply it twice), and OUTCOME_UNKNOWN means the request may have been applied. A success response without a body has value=None.

Extending

openapi_sdk/custom/__init__.py is yours: it is created once, never overwritten, and imported as openapi_sdk.custom.

Rust SDK and CLI

This is the README that tungsten generates for the Rust target, shown for a small example API (Swagger's Petstore). Names change with your API; the structure does not.

Rust SDK for the openapi API (1.0.0), generated by tungsten.

Every call returns tungsten_runtime::Result<T>: the value with its response metadata, or the diagnostic envelope. API and transport errors are values, never panics, and arguments are validated before any request is sent. The SDK is async (tokio), with a blocking client for programs without an async runtime (), and builds with Rust 1.88 or newer; models are serde types.

Install

cargo add openapi-sdk
cargo add tokio --features rt-multi-thread,macros

Create a client

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;

use tungsten_runtime::Credential;
use openapi_sdk::{CallOptions, ClientOptions, OpenapiClient};

let client = OpenapiClient::new(ClientOptions {
    base_url: Some("http://petstore.swagger.io/v1".to_string()),
    ..Default::default()
})?;
}

Read

use openapi_sdk::resources::pets::PetsListRequest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // let client = ...;
    match client.pets().list(PetsListRequest::default(), &CallOptions::default()).await {
        Ok(response) => println!("{:?}", response.value),
        Err(error) => println!("{:?} {}", error.category, error.remediation),
    }
    Ok(())
}

response.meta has the status, headers, request id and the number of attempts. A success without a body is () (or None when only some success statuses have one).

Change something, safely

Mutations are never retried blindly. When the operation takes an idempotency key, generate it once, persist it with your intent, and reuse it on every retry.

#![allow(unused)]
fn main() {
use openapi_sdk::resources::pets::PetsCreateRequest;

let opts = CallOptions {
    idempotency_key: Some(key), // a UUIDv4 you persisted
    ..Default::default()
};
let result = client.pets().create(PetsCreateRequest::default(), &opts).await;
}

Models and optional fields

Request and response bodies are serde structs under models. A field that may be left out is Option<T> and is not sent when None; a required field that may be null is Option<T> and is sent as null; a field that may be left out or null is Patch<T> (Patch::Undefined, Patch::Null, Patch::Value). Closed records reject unknown members; open records keep them in extra. String and integer enums are closed: an unknown value does not decode. Integer fields are i64 (i32 for int32, u64 for non-negative ones above i64::MAX) and do not accept 1.0. Constraints (bounds, lengths, patterns, formats, unique items) are checked by the validators the SDK registers with the runtime, before a request is sent and, per ClientOptions::validate_responses, on responses.

Errors

A failed call is Err(error); error.diagnostic is the envelope:

{
  "status": "error",
  "category": "VALIDATION_FAILED",
  "operation": "<operation id>",
  "http_status": null,
  "code": null,
  "failed_parameter": "<args path>",
  "received_value": null,
  "expected": "<what was expected>",
  "remediation": "<what to do next>",
  "retryable": "never",
  "retry_after_ms": null,
  "next_action": null,
  "request_id": null,
  "trace": { "attempts": 0 }
}

Branch on category and retryable: same_key_only means retry only with the same idempotency key, after_remediation means act first (fix the input, or check whether an unknown outcome took effect: without a key a repeat can apply it twice), and OUTCOME_UNKNOWN means the request may have been applied. error.partial holds what a failed call or macro already produced (a one-time secret, completed step results): store it before acting on the error.

Blocking client

OpenapiBlockingClient has the same resources, methods, arguments and results as OpenapiClient, but each call returns when the request is done: it owns a thread that runs a current-thread tokio runtime, so it needs no async runtime of its own and never panics inside one (it blocks the calling thread while it waits, so from async code use OpenapiClient). Page iterators are blocking Iterators of pages.

#![allow(unused)]
fn main() {
use openapi_sdk::{ClientOptions, OpenapiBlockingClient};

let client = OpenapiBlockingClient::new(ClientOptions::default())?;
// client.<resource>().<method>(request, &CallOptions::default())
}

Dynamic calls

The client implements tungsten_runtime::Dispatch: invoke, preview, pages, run_macro and preview_macro take an operation id (or macro name) and a JSON arguments object keyed by argument name, run the same typed methods and return JSON. Arguments that do not decode go to the runtime as given, which answers with the pre-flight envelope.

Command line

The openapi binary (crate openapi-cli) is built on this SDK: one command per operation, --json output, --dry-run previews and the same safety rules. Build it with cargo build -p openapi-cli --release.

Extending

src/custom/mod.rs is yours: it is created once, never overwritten, and exported as custom.

Docs target

The docs target writes llms.txt, llms-full.txt, tools.json and a README. This is the README tungsten generates for the example API.

Swagger Petstore API, version 1.0.0.

API version 1.0.0 · base URL http://petstore.swagger.io/v1 · 1 namespace · 3 callable operations · 0 planned

Files

FilePurpose
llms.txtIndex for agents: one line per callable operation with its safety tier and idempotency rule.
llms-full.txtFull reference for agents: arguments, responses, errors, remediation and types.
tools.jsonFunction-calling manifest: one tool per callable operation, parameters as JSON Schema.

Authentication

Auth: none declared. Auth profiles: none; each operation names the schemes it needs (a + b all of, a | b any of).

openapi operations

Swagger Petstore (version 1.0.0).

OperationEndpointSummarySafetyIdempotency
openapi.pets.listGET /petsList all petsread_onlynone
openapi.pets.createPOST /petsCreate a petmutatingnone
openapi.pets.getGET /pets/{petId}Info for a specific petread_onlynone

Errors

SDK calls never throw for API or transport errors: they return a result whose error is one envelope with category, retryable and remediation. llms-full.txt lists the API's error codes with their remediation.

CategoryWhenRetryable by default
VALIDATION_FAILEDarguments failed pre-flight validation, or a 400 with an error bodynever
MALFORMED_REQUESTa 400 without an error bodynever
REQUEST_TOO_LARGE413never
AUTH_FAILED401 or 403never
NOT_FOUND404never
CONFLICT409never
PRECONDITION_FAILED402, 412 or 422 as the manifest saysnever
RATE_LIMITED429after_delay
UPSTREAM_UNAVAILABLE502, 503 or 504 on a readafter_delay
OUTCOME_UNKNOWNa timeout, reset, ambiguous status or unexplained 5xx on a mutationsame_key_only (after_remediation without an idempotency key)
TRANSPORT_FAILEDDNS, TLS or connection refused before sendingafter_delay
CONFIRMATION_REQUIREDa destructive or irreversible call without confirmationnever
GATE_DISABLEDa gated route answered with its disabled statusnever
UNEXPECTED_RESPONSEa response that failed strict validationnever

Regenerating

These files are generated from the API's OpenAPI documents and its tungsten manifests. Run tungsten generate after changing them; tungsten check --ci fails while these files are stale.

CLI reference

This page is generated from tungsten --help, command by command. Run tungsten <command> --help for the same text in your terminal.

tungsten

Agent-native SDK generator and MCP/WebMCP compiler

Usage: tungsten [OPTIONS] <COMMAND>

Commands:
  check     Validate the manifest and specs and report diagnostics
  ir        Intermediate representation commands
  explain   Explain a diagnostic code, an operation or a type
  schema    Print a JSON Schema published by tungsten
  init      Write tungsten.yml and agent.yml for a new project
  doctor    Report installed tools and optionally check project prerequisites
  emitters  List the external emitters (`tungsten-emit-<name>`) that can be run, with the protocol and version each reports
  generate  Generate every configured target (SDKs, docs) from the project
  mock      Serve a mock of the API from the compiled IR
  report    Coverage, safety matrix, token budgets, diagnostics and changes since the last generation, as text, one static HTML file or JSON
  diff      Show what regeneration would add, change or remove in each target, and with --semver how the API surface changed
  overlay   OpenAPI Overlay authoring helpers
  lsp       Serve a Language Server Protocol server on stdin and stdout for tungsten.yml, agent.yml and overlays: diagnostics, completion, hover, go-to-definition and symbols. `--json` has no effect
  help      Print this message or the help of the given subcommand(s)

Options:
      --json
          Print exactly one JSON document on stdout

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

Examples:
  tungsten init --from openapi.yaml    Write tungsten.yml and agent.yml
  tungsten check .                     Validate the project and print diagnostics
  tungsten generate --target ts,py     Generate the TypeScript and Python SDKs
  tungsten generate --target webmcp    Generate a browser WebMCP adapter
  tungsten mock --port 4010            Serve a mock of the API from the IR
  tungsten report --html report.html   Write a static coverage and safety report
  tungsten diff --semver               Show how the API surface changed
  tungsten overlay suggest --write fixes.overlay.yaml
                                       Suggest an overlay that fixes diagnostics
  tungsten lsp                         Serve diagnostics, completion and hover for
                                       manifests to an editor over stdio

Repository: https://github.com/pboachie/tungsten

Exit codes:
  0  success (also `diff` when regeneration would change files, and
     `report` with warnings)
  1  the input has errors (or warnings with --strict), generated output is
     stale (generate --check, check --ci), or the item asked for does not
     exist (explain target not found, unknown target)
  2  usage error: the command line could not be parsed
  3  I/O or internal failure (for example --out is not writable, or the
     mock server cannot start)
  4  the command refused to act without confirmation (init over existing
     files, generate into a directory tungsten did not write: pass --force)

Output:
  Without --json, results go to stdout and diagnostics to stderr. With
  --json, stdout carries exactly one JSON document described by
  `tungsten schema cli-output`, help and version included. Color is used
  only on a terminal and never when NO_COLOR is set.

tungsten check

Validate the manifest and specs and report diagnostics

Usage: tungsten check [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --ci      CI mode: never prompt, never color, and fail (TG0901) when the generated output of a target differs from what `generate` would write
      --json    Print exactly one JSON document on stdout
      --strict  Treat warnings as errors
  -h, --help    Print help

tungsten ir

Intermediate representation commands

Usage: tungsten ir [OPTIONS] <COMMAND>

Commands:
  dump  Write the IR as JSON
  help  Print this message or the help of the given subcommand(s)

Options:
      --json  Print exactly one JSON document on stdout
  -h, --help  Print help

tungsten ir dump

Write the IR as JSON

Usage: tungsten ir dump [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --json        Print exactly one JSON document on stdout
      --out <FILE>  Write the IR to this file instead of stdout
      --compact     One line instead of indented JSON
      --pretty      Indented JSON (the default)
  -h, --help        Print help

tungsten explain

Explain a diagnostic code, an operation or a type

Usage: tungsten explain [OPTIONS] <TARGET>

Arguments:
  <TARGET>  A diagnostic code (TG0201), an operation id (public.listPets) or a type id (public.Pet). A bare operationId or type name is accepted when it is unambiguous

Options:
      --json            Print exactly one JSON document on stdout
      --project <PATH>  Project used to resolve operation and type ids [default: .]
  -h, --help            Print help

tungsten schema

Print a JSON Schema published by tungsten

Usage: tungsten schema [OPTIONS] <NAME>

Arguments:
  <NAME>
          Which schema to print

          Possible values:
          - tungsten:         The tungsten.yml manifest
          - agent:            The agent.yml manifest
          - ir:               The intermediate representation written by `ir dump`
          - cli-output:       The `--json` output of this CLI
          - external-emitter: The documents of the external emitter protocol
          - sdk-descriptors:  The SDK descriptor document (tungsten_emit::sdk::descriptors)
          - tool-catalog:     Transport-neutral tools and scoped groups

Options:
      --json
          Print exactly one JSON document on stdout

  -h, --help
          Print help (see a summary with '-h')

tungsten init

Write tungsten.yml and agent.yml for a new project

Usage: tungsten init [OPTIONS]

Options:
      --from <SPEC>        OpenAPI document to start from. Default: openapi.json, openapi.yaml or openapi.yml in the target directory
      --json               Print exactly one JSON document on stdout
      --dir <DIR>          Directory to write the manifests into (created if missing) [default: .]
      --name <NAME>        API machine name: lowercase letters, digits and underscores, starting with a letter. Default: derived from the spec title
      --targets <TARGETS>  Targets to configure, comma-separated or repeated (ts, py, rs aliases). Default: typescript,python,docs. WebMCP also configures TypeScript
      --force              Overwrite existing tungsten.yml and agent.yml
  -h, --help               Print help

tungsten doctor

Report installed tools and optionally check project prerequisites

Usage: tungsten doctor [OPTIONS] [PATH]

Arguments:
  [PATH]  Project directory or tungsten manifest to inspect without network access

Options:
      --json    Print exactly one JSON document on stdout
      --strict  Fail when the project's required tools are missing or unsupported. Uses the current directory when PATH is omitted
  -h, --help    Print help

tungsten emitters

List the external emitters (`tungsten-emit-<name>`) that can be run, with the protocol and version each reports

Usage: tungsten emitters [OPTIONS] [PATH]

Arguments:
  [PATH]  A project (as for `check`) whose `external` targets are listed too

Options:
      --json  Print exactly one JSON document on stdout
  -h, --help  Print help

tungsten generate

Generate every configured target (SDKs, docs) from the project

Usage: tungsten generate [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --json              Print exactly one JSON document on stdout
      --target <TARGETS>  Only these targets, comma-separated (`typescript,docs`; `ts`, `py` and `rs` are accepted). Default: every target in tungsten.yml
      --dry-run           Report what would be written or removed without touching the disk
      --check             Compare the output directories with what would be generated; write nothing and exit 1 when they differ
      --force             Write into a non-empty output directory that tungsten did not generate (one without .tungsten/manifest.json)
      --strict            Treat warnings (the compiler's and the emitters') as errors: nothing is written when there are any, in any target
  -h, --help              Print help

tungsten mock

Serve a mock of the API from the compiled IR

Usage: tungsten mock [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --json                          Print exactly one JSON document on stdout
      --port <PORT>                   Port on 127.0.0.1 to listen on; 0 picks a free port [default: 0]
      --seed <SEED>                   Seed for generated response values; the same seed gives the same responses [default: 0]
      --gate <ENV>                    Treat this runtime gate (its environment variable, e.g. SEALED_WEBHOOK_DELIVERY_ENABLED) as on; repeat for several. Gates not listed keep their default
      --max-recorded-calls <N>        Recorded calls kept for `/__tungsten/calls`; past it the oldest is dropped. 0 records nothing [default: 10000]
      --max-idempotent-responses <N>  Stored idempotent responses kept for replays; past it the oldest is dropped and a later request with its key is served as new. 0 stores nothing [default: 10000]
  -h, --help                          Print help

tungsten report

Coverage, safety matrix, token budgets, diagnostics and changes since the last generation, as text, one static HTML file or JSON

Usage: tungsten report [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --html <FILE>  Write the report as one static HTML file (no external assets, no scripts)
      --json         Print exactly one JSON document on stdout
  -h, --help         Print help

tungsten diff

Show what regeneration would add, change or remove in each target, and with --semver how the API surface changed

Usage: tungsten diff [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --json              Print exactly one JSON document on stdout
      --target <TARGETS>  Only these targets, comma-separated. Default: every target in tungsten.yml
      --semver            Classify the API surface change since the last generation (from .tungsten/surface.json) as major, minor, patch or none
      --migration         Include actionable migration guidance (implies --semver)
  -h, --help              Print help

tungsten overlay

OpenAPI Overlay authoring helpers

Usage: tungsten overlay [OPTIONS] <COMMAND>

Commands:
  suggest  Suggest an Overlay 1.0 document whose actions fix diagnostics (a missing or repeated operationId) without changing the IR
  help     Print this message or the help of the given subcommand(s)

Options:
      --json  Print exactly one JSON document on stdout
  -h, --help  Print help

tungsten overlay suggest

Suggest an Overlay 1.0 document whose actions fix diagnostics (a missing or repeated operationId) without changing the IR

Usage: tungsten overlay suggest [OPTIONS] [PATH]

Arguments:
  [PATH]  A directory with a tungsten.yml, a tungsten*.yml manifest, or a single OpenAPI document [default: .]

Options:
      --json              Print exactly one JSON document on stdout
      --write <FILE>      Write the overlay to this file instead of stdout
      --force             Overwrite the --write file if it exists
      --only <CODES>      Only suggest fixes for these diagnostic codes, comma-separated
      --namespace <NAME>  The input (namespace) to suggest for. Needed when more than one input has suggestions: an overlay's targets only fit its own spec
  -h, --help              Print help

tungsten lsp

Serve a Language Server Protocol server on stdin and stdout for tungsten.yml, agent.yml and overlays: diagnostics, completion, hover, go-to-definition and symbols. `--json` has no effect

Usage: tungsten lsp [OPTIONS]

Options:
      --config <PATH>  The manifest to serve: a tungsten.yml, or a directory holding one. Default: the tungsten.yml of the editor's workspace folder
      --json           Print exactly one JSON document on stdout
  -h, --help           Print help

Diagnostic codes

Every diagnostic tungsten reports has a stable code, TGnnnn. The first two digits after TG name the area. tungsten explain TGnnnn prints the text below for one code. This page is generated from the compiler's registry.

AreaCodes
Parsing and input limits (TG01xx)TG0101, TG0102, TG0103, TG0104, TG0105, TG0110, TG0111, TG0112
References and cycles (TG02xx)TG0201, TG0202, TG0203, TG0204, TG0205, TG0206, TG0207
Type normalization (TG03xx)TG0301, TG0302, TG0303, TG0304, TG0305, TG0306, TG0307, TG0308
Naming (TG04xx)TG0401, TG0402, TG0403
Operations, pagination and auth inference (TG05xx)TG0501, TG0502, TG0503, TG0504, TG0505, TG0506, TG0507, TG0508, TG0509, TG0530, TG0531, TG0570, TG0571, TG0572, TG0573, TG0574
Manifests (TG06xx)TG0601, TG0602, TG0603, TG0604, TG0605, TG0606, TG0607, TG0608, TG0609, TG0610, TG0611, TG0612, TG0613, TG0614
Emitter limits (TG07xx)TG0701, TG0702, TG0703, TG0704, TG0710, TG0711, TG0712, TG0713, TG0720, TG0721, TG0722, TG0723, TG0724, TG0725, TG0726, TG0727, TG0730, TG0731, TG0732, TG0733, TG0740, TG0741, TG0742, TG0743, TG0744, TG0745, TG0746, TG0750, TG0751, TG0752, TG0753, TG0760, TG0790, TG0791, TG0792, TG0793
External emitters (TG08xx)TG0801, TG0802, TG0803, TG0804, TG0805, TG0806
Staleness and CI (TG09xx)TG0901, TG0902, TG0920, TG0921

Parsing and input limits (TG01xx)

TG0101

input file could not be read

Meaning. An input named in tungsten.yml or on the command line could not be read: the file does not exist, is a directory, or is not readable by the current user.

Fix. Check the path. Paths in tungsten.yml are relative to the directory of tungsten.yml, not to the working directory.

TG0102

input is not valid JSON or YAML

Meaning. The input is neither valid JSON nor valid YAML, so no OpenAPI document could be read from it. The message names the first syntax error.

Fix. Fix the syntax error at the reported position. Run the file through a JSON or YAML linter if the position is not obvious.

TG0103

unsupported OpenAPI version (convert Swagger 2.0 to OpenAPI 3 first)

Meaning. The document's openapi field is missing or names a version tungsten does not compile. tungsten reads OpenAPI 3.0.x and 3.1.x; Swagger 2.0 documents declare swagger: "2.0" instead.

Fix. Set openapi to the version the document follows, or convert a Swagger 2.0 document to OpenAPI 3 (for example with swagger2openapi) before compiling.

TG0104

document is missing a required OpenAPI field

Meaning. A field the OpenAPI specification requires is absent (for example info, info.title, info.version, or a parameter's name or in).

Fix. Add the field at the reported pointer.

TG0105

input exceeds a size or nesting limit

Meaning. The input is larger than the configured byte limit or nests deeper than the depth limit. The limits protect the compiler from pathological or hostile inputs.

Fix. Split very large documents into files joined by $ref, or flatten deeply nested inline schemas into named components.

TG0110

document has no info.version; version 0.0.0 is used

Meaning. The document has no info.version. OpenAPI requires it, but published specs sometimes omit it. tungsten records version 0.0.0 and continues; the diagnostic is a warning, and the document is still refused when something else required is missing.

Fix. Add info.version to the document, or supply it with an overlay.

TG0111

path key carries a query string; its pairs are sent on every call

Meaning. A key of paths carries a query string, such as /v1/messages?beta=true. OpenAPI forbids it, but some APIs use it to select a variant of an operation. tungsten names the operation and places it in resources from the path alone, keeps the query pairs as constant query parameters of the path template (path.query in the IR), sends them on every call (they are not arguments), and the mock routes a request only to the operation whose constants it carries.

Fix. No change is needed. To make the document valid OpenAPI, declare the pair as a query parameter with a const or enum schema.

TG0112

apiKey header scheme inferred from a credential header parameter

Meaning. The document defines no security scheme and no security, but declares a header that carries an API key (x-api-key, api-key or apikey) as a parameter. tungsten inferred an apiKey header scheme named apiKey and required it by every operation, so the credential is configured once on the client and the header is no longer an argument of each call.

Fix. Define the scheme under components/securitySchemes and list it in security (an overlay can add both) to make the intent explicit and silence the diagnostic.

References and cycles (TG02xx)

TG0201

unresolvable $ref

Meaning. A $ref points at a location that does not exist: the JSON Pointer names a missing member, or the referenced file has no such path.

Fix. Correct the pointer (pointers are case-sensitive and escape / as ~1 and ~ as ~0), or add the missing definition.

TG0202

remote $ref blocked (remote fetching is not available)

Meaning. A $ref names a remote URL. This version of tungsten does not fetch remote references, so that compiling never depends on, or leaks to, other hosts.

Fix. Vendor the remote document next to the spec and reference it by relative path.

TG0203

circular $ref through named types (recursion preserved)

Meaning. Named schemas reference each other in a cycle. This is legal; the cycle is kept and the participating types are marked recursive so emitters box or lazily evaluate the recursive edges.

Fix. Nothing to fix. The note exists so recursive types are visible in review.

TG0204

$ref to an external file outside the input root

Meaning. A $ref points at a file outside the directory tree of the input, which tungsten refuses to read.

Fix. Move the referenced file under the input root, or point the manifest at a common parent directory.

TG0205

circular $ref without a named type to break the cycle

Meaning. A reference cycle runs through inline schemas only, so there is no named type at which generated code could break the recursion.

Fix. Move the recursive schema under components/schemas and reference it by name, or list the edge in tungsten.yml types.break_cycles (for example Node.children).

TG0206

overlay target matched nothing

Meaning. An overlay action's target JSONPath matched no node in the document, so the action had no effect.

Fix. Fix the JSONPath expression, or remove the action if the spec no longer contains the node it was written for.

TG0207

overlay document or action is invalid

Meaning. An overlay file is not an OpenAPI Overlay 1.x document, has no actions array, or contains an action without a valid target JSONPath, so it could not be applied.

Fix. Start the file with overlay: 1.0.0 and an info block, list the changes under actions, and give each action a target JSONPath plus update or remove.

Type normalization (TG03xx)

TG0301

untagged union: runtime will sniff candidates in order

Meaning. A oneOf or anyOf has no discriminator, so a value's variant can only be found by trying the candidates in order at runtime. The order is deterministic (most constrained first), but sniffing is slower and can pick the wrong variant when candidates overlap.

Fix. Add a discriminator with a property every variant declares as a const or single enum value.

TG0302

allOf members conflict and could not be flattened

Meaning. The members of an allOf declare the same property with incompatible types, or mix records with non-records, so they cannot be merged into one record. The type is kept as an intersection, which some targets express poorly.

Fix. Make the overlapping property types agree, or restructure the schema so allOf only combines compatible records.

TG0303

unsupported or unknown schema keyword ignored

Meaning. A schema uses a keyword tungsten does not model (for example not, if/then/else or patternProperties). The keyword is ignored, so generated validation is looser than the spec.

Fix. Nothing is required. If the constraint matters to clients, express it with supported keywords or document it in the description.

TG0304

unknown string format treated as plain string

Meaning. A string schema uses a format tungsten does not recognize. The value is treated as a plain string and the format name is preserved in the IR.

Fix. Use a standard format (uuid, date-time, email, ...) if one fits; otherwise the warning can be accepted.

TG0305

schema type could not be determined; treated as any

Meaning. A schema has no type and nothing else (enum, const, properties, items, composition) from which a type could be inferred, so it accepts any value.

Fix. Add a type, or an explicit empty schema {} if any value really is allowed.

TG0306

discriminator could not be applied as written

Meaning. A discriminator next to oneOf/anyOf could not be used as written: it has no propertyName, a variant is not an object schema, a mapping entry does not resolve or names a schema that is not one of the variants, or a variant has no tag value. Unusable mapping entries are dropped; an unusable discriminator is ignored and the union is classified as if it had none.

Fix. Point every mapping value at one of the union's variants (#/components/schemas/X or the bare name X), and make every variant an object schema with the property.

TG0307

schema admits no value (empty enum or union)

Meaning. A schema admits no value at all: an empty enum, or a oneOf/anyOf with no members. Its type is never, so no generated client can send or accept it.

Fix. List the allowed values, or remove the schema if it is unused.

TG0308

types.break_cycles entry names no component property

Meaning. An entry of tungsten.yml types.break_cycles does not name a property of a schema under components/schemas (entries have the form Type.field), so it has no effect.

Fix. Fix the entry to Type.field with the component key and property name as written in the spec, or remove it.

Naming (TG04xx)

TG0401

identifier collision disambiguated

Meaning. Two names in one scope (fields of a record, operations of a resource, types of a namespace) map to the same identifier in some target language. The later one in sorted order was renamed deterministically.

Fix. Rename one of them in the spec, or give an explicit name in tungsten.yml naming, so the generated name is chosen rather than derived.

TG0402

identifier escaped (reserved keyword or invalid start)

Meaning. A name is a reserved word in a target language or does not start with a letter, so it was escaped (for example type becomes type_ in Python and Rust). The wire name is unchanged.

Fix. Nothing is required. Rename in tungsten.yml naming if the escaped form reads badly.

TG0403

operation has no operationId; name synthesized

Meaning. An operation has no operationId, so its id and method name were synthesized from the HTTP method and path. Synthesized names change when the path changes.

Fix. Add an operationId to the operation so its id stays stable.

Operations, pagination and auth inference (TG05xx)

TG0501

pagination inferred by heuristic

Meaning. No explicit pagination was configured and the operation matched a heuristic: cursor (a closed response record with one array field and one nullable next_* field, and a query parameter of the same name), or offset/page (offset and limit, or page and a page size, with an array of items). Generated SDKs will iterate pages.

Fix. Confirm the inference, or declare the operation under tungsten.yml pagination (use none: true to switch inference off for it).

TG0502

security scheme referenced but not defined

Meaning. An operation's security requirement names a scheme that is not defined under components/securitySchemes, so tungsten cannot tell how to authenticate it.

Fix. Define the scheme under components/securitySchemes, or fix the name in the requirement.

TG0503

composite auth profile satisfies an undefined security scheme

Meaning. A composite auth profile in tungsten.yml lists under satisfies a name that is not a security scheme of any input, so the profile cannot stand in for it.

Fix. List the scheme names the operations' security requirements use (from components/securitySchemes), and add parts that supply them.

TG0504

operation excluded by include predicate (planned)

Meaning. The operation does not match the input's include predicate, so it is not callable. When it matches planned_from it is recorded as planned for documentation.

Fix. Nothing is required. Change the predicate or the operation's extension value if the operation should be callable.

TG0505

runtime gate recorded from x-runtime-gate

Meaning. The operation carries x-runtime-gate, so it only works when the server enables the named gate. Generated clients report the gate instead of a bare 404.

Fix. Nothing is required. Describe the gate under agent.yml gates so agents get a useful explanation.

TG0506

rpc_unflatten target not found or not a const-discriminated oneOf

Meaning. The input's rpc_unflatten names a path and method that do not exist, or whose request body is not a oneOf discriminated by a const method field, so it cannot be split into one operation per method.

Fix. Check rpc_unflatten.path, method, discriminator and params against the spec.

TG0507

path template parameter without a definition; assumed a required string

Meaning. A path template names a {param} that no path-level or operation-level parameter declares. The SDK treats it as a required string so the operation stays callable.

Fix. Declare the parameter with in: path, required: true and a schema.

TG0508

malformed operation element ignored

Meaning. Part of an operation does not have the shape OpenAPI requires and was skipped: a parameter without name or in, a path parameter missing from the template, a response key that is not a status code, NXX range or default, or a member that must be an object or array but is not.

Fix. Fix the element at the reported pointer; the message names what is wrong.

TG0509

unsupported security scheme ignored

Meaning. A security scheme uses a type tungsten cannot generate authentication for (for example mutualTLS or an HTTP scheme other than bearer and basic), or lacks a field its type requires. Requirements naming it stay in the IR without a scheme.

Fix. Describe the credential with a supported scheme, or authenticate those operations through a custom transport.

TG0530

event stream declares no schema; events decoded as any JSON value

Meaning. A success response declares the text/event-stream media type without a schema, so the SDK cannot type or check the events of the stream. Each event's data is still decoded as JSON and handed to the caller as an untyped value.

Fix. Declare the schema of one event under the media type (usually a oneOf tagged by a type property). Name a data value that ends the stream, if there is one, with x-tungsten-stream-done.

TG0531

event stream declared for more than one success status; the first is streamed

Meaning. More than one success status of an operation declares text/event-stream. The SDK streams the lowest status; the others are read as plain responses.

Fix. Declare the stream under one status, normally 200.

TG0570

invalid MCP embedding index configuration (search.embeddings); the index is off

Meaning. targets.mcp.search.embeddings in tungsten.yml is not valid: an unknown key, a provider that is not command or http, a missing command, url or model, a dimensions, batch_size or timeout_ms out of range, or an api_key_env that is not an environment variable name. The embedding index is off and the MCP server searches with BM25 only; with required: true this is an error instead.

Fix. Fix the option: search: { embeddings: { provider: command, command: "node embed.mjs", model: my-model } } or provider: http with url. See the MCP server guide for embedding provider examples.

TG0571

embedding provider could not be run or did not answer in time; BM25 only

Meaning. The embedding provider of the MCP target could not be used: the command was not found, could not be started, exited with a failure, or neither it nor the HTTP endpoint answered within timeout_ms. Generation went on without index.embeddings.bin and index.embeddings.json, so the server searches with BM25 only. With required: true this is an error and nothing is written.

Fix. Run the command by hand with a request on standard input ({"protocol":1,"kind":"document","model":"m","dimensions":null,"inputs":["x"]}), raise timeout_ms, or lower batch_size.

TG0572

embedding provider returned an invalid response; BM25 only

Meaning. The embedding provider answered, but not with what the protocol says: not JSON, no embeddings array (command) or data array (HTTP), a different number of vectors than inputs, vectors of different or unexpected lengths, or values that are not finite numbers. Generation went on with BM25 only (an error with required: true).

Fix. Return one vector per input, in order, all of the length the model produces; set dimensions to that length so a mismatch is caught.

TG0573

embedding endpoint refused the request, needs a missing API key or uses https; BM25 only

Meaning. The HTTP embedding provider could not be called: the endpoint answered with an error status, the environment variable named by api_key_env is not set, or the URL is https://, which the compiler cannot call (it links no TLS stack). Generation went on with BM25 only (an error with required: true). The key is only ever read from the environment and never written to the output.

Fix. Export the API key, check the URL and model, or use provider: command with a program that calls the endpoint (for example curl) or a local http:// gateway.

TG0574

embedding index written next to the MCP manifest

Meaning. The MCP target wrote the embedding index: index.embeddings.json (model, dimensions, tool names and content hashes, checksum) and index.embeddings.bin (normalized little-endian f32 vectors). Vectors of tools whose text did not change since the previous run are reused, so only new or edited tools reach the provider.

Fix. Nothing to fix. Serve the index with search.embeddings in the server options (the generated server.ts sets it from tungsten.yml).

Manifests (TG06xx)

TG0601

manifest is not valid YAML

Meaning. A manifest (tungsten.yml or agent.yml) is not valid YAML.

Fix. Fix the YAML syntax at the reported position. Indentation must use spaces.

TG0602

manifest does not match its schema

Meaning. A manifest is valid YAML but does not match its schema: a key is unknown, a required key is missing, or a value has the wrong type. The pointer names the offending node.

Fix. Correct the value at the pointer. Editors with the YAML language server validate against the schema named in the file's first-line comment (tungsten schema tungsten prints it).

TG0603

manifest references an unknown operation

Meaning. A manifest refers to an operation id that does not exist in the compiled API.

Fix. Use namespace.operationId; tungsten explain <id> shows whether an id exists.

TG0604

manifest references an unknown namespace or path

Meaning. A manifest refers to a namespace, resource or path that does not exist in the inputs.

Fix. Check the name against inputs[].namespace in tungsten.yml and the paths of the spec.

TG0605

agent rule targets an operation that cannot serve it

Meaning. An agent rule points at an operation that cannot do what the rule needs: a verification hook or a macro poll step names an operation that is not read_only (the runtime would repeat its side effect), a paginate step names an operation without pagination, or an endpoint preview names the operation itself. The rule (or the macro) is dropped.

Fix. Point the rule at a read_only operation (for a poll or a verification hook), at a paginated one (for paginate), or declare the operation's safety or pagination if the inferred one is wrong.

TG0606

remediation names an unknown error code

Meaning. A remediation entry in agent.yml (errors.codes or a tool's remediation) or in x-agent-remediation is keyed by an error code that the error model of the operation's namespace (or of any namespace, for errors.codes) does not list. The entry is kept but may never match a response.

Fix. Use a code from the error schema's code enum (tungsten ir dump shows namespaces[].errors.codes), or fix the spelling.

TG0607

agent rule names a field the operation does not have

Meaning. An agent rule names a field that does not exist: a confirmation summary field or message placeholder that is not a request parameter or body field, a sensitive field that is not in the success response, a verification argument or predicate path, a macro reference such as $accepted.message_id into a step's response, or the code field of errors.envelope.

Fix. Use the wire names of the spec (dotted for nested fields, for example key.action_id); tungsten explain <operation> lists an operation's parameters and body.

TG0608

gate names no runtime gate or conflicts with the spec

Meaning. A tool's gate names neither an x-runtime-gate environment variable of the spec nor a gates entry, or the operation is already gated by a different variable, or a gates entry's disabled_status disagrees with the spec's gate. The spec wins.

Fix. Declare the gate under gates (with text and disabled_status) or use the spec's variable name.

TG0609

macro dropped or its safety tier raised

Meaning. A macro could not be compiled as written: its name is also an operation id (the macro is dropped), or it declares a safety tier weaker than its strictest step (the macro gets the step's tier, because a macro can never be safer than what it calls).

Fix. Rename the macro, or declare a tier at least as strict as every call step.

TG0610

unknown x-agent- extension ignored*

Meaning. The spec uses an x-agent-* extension tungsten does not know, often a misspelling. It is ignored.

Fix. Fix the extension name (for example x-agent-safety), or rename it outside the x-agent- prefix if it is meant for another tool.

TG0611

x-agent- extension value is malformed; ignored*

Meaning. An x-agent-* extension of an operation has the wrong shape (for example x-agent-safety: dangerous or an x-agent-idempotency object without policy). The extension is ignored and the operation keeps its defaults or agent.yml rules.

Fix. Give the extension the shape of the matching agent.yml tools key (specs/agent-manifest.schema.json, ToolConfig).

TG0612

targets share or nest their output directories

Meaning. Two targets in tungsten.yml write to the same output directory, or one target's out is inside another's. Each target records its files in its own .tungsten/manifest.json and removes files it no longer generates, so targets sharing a directory delete each other's output on every run.

Fix. Give every target its own directory that is not inside another target's, for example typescript: { out: generated/typescript } and docs: { out: generated/docs }.

TG0613

agent.yml option not applied by this version; ignored

Meaning. agent.yml sets an option the schema accepts but this version of tungsten does not apply yet (disclosure.prune.drop_fields, disclosure.prune.keep_examples). Agent tool schemas describe exactly the arguments the SDK takes, so nothing is hidden and no examples are added; the rest of the manifest applies.

Fix. Remove the option, or keep it for a later version that applies it. To make a tool smaller now, shorten descriptions or raise defaults.disclosure.schema_budget_tokens.

TG0614

gates entry no operation is gated by

Meaning. A gates entry of agent.yml is used by no operation: the spec carries no x-runtime-gate for that environment variable and no tools entry names it in gate. The entry's text is never shown and a 404 from the routes it was written for is reported as NOT_FOUND ("check the identifiers") instead of GATE_DISABLED. Live runs against ZROtext showed it: SEALED_ADMISSION_ENABLED was declared but none of the sealed operations named it.

Fix. Add gate: <ENV_VAR> to the tools entry of every operation the deployment setting switches off (one entry per operation), or delete the gates entry.

Emitter limits (TG07xx)

TG0701

generated output could not be written

Meaning. A generated file could not be written to the target's output directory, for example because the directory is read-only or a path component is a file.

Fix. Check the target's out path and its permissions, then run tungsten generate again.

TG0702

target has no emitter in this version; skipped

Meaning. tungsten.yml configures a target (such as mock) for which this version of tungsten has no emitter yet. tungsten generate skips it and check --ci does not compare its output.

Fix. Nothing to fix. Remove the target from tungsten.yml to silence the notice, or generate it once a tungsten release ships its emitter.

TG0703

output directory was not generated by tungsten; refusing to write

Meaning. The target's output directory is not empty and has no .tungsten/manifest.json (or an unreadable one), so tungsten cannot tell its own files from yours. Writing would mix generated files into a directory it does not own, so nothing was written.

Fix. Point the target's out at a new or empty directory, or pass --force to write into it anyway; files tungsten did not generate are then left alone.

TG0704

output path leaves the output directory or is reserved

Meaning. A generated file would be written outside the target's output directory: a directory on its path is a symlink leading elsewhere, the file itself is a symlink, or the path is inside the reserved .tungsten/ directory. Nothing was written.

Fix. Remove the symlink from the output directory. If the path is under .tungsten/, the emitter is at fault: report it.

TG0710

macro not emitted by the TypeScript SDK (not in the canonical form)

Meaning. A macro in the IR does not fit the canonical form the TypeScript SDK compiles: a step names an operation that is not callable, a reference names a later or unknown step, or an {expr} is not <ref> in [..], <ref> == x or <ref> != x. The SDK is generated without that macro.

Fix. Fix the macro in agent.yml so every step calls a callable operation and every reference names $input or an earlier step's as.

TG0711

invalid TypeScript target option; default used

Meaning. An option of the typescript target is not valid (for example a package that is not an npm package name or a version that is not a semantic version), so its default is used.

Fix. Fix the option under targets.typescript in tungsten.yml.

TG0712

OpenID Connect scheme sent as a bearer token by the TypeScript SDK

Meaning. The API declares an OpenID Connect security scheme. The TypeScript SDK sends the configured credential as a bearer token and does not run discovery or obtain tokens itself.

Fix. Obtain the token with your OpenID Connect client and pass it in auth under the scheme's name.

TG0713

tool schema over the agent schema token budget

Meaning. A tool of tools.json (its name, description and parameters, measured as characters / 4 after repeated sub-schemas were moved to $defs) is larger than the per-tool schema budget of agent.yml (defaults.disclosure.schema_budget_tokens, 600 by default). Agents pay this cost every time the tool is listed.

Fix. Shorten the operation's and its fields' descriptions (disclosure.prune for the operation, the spec for fields), split the operation, or raise the budget.

TG0720

invalid MCP target option; default used

Meaning. An option of the mcp target is not valid (a package that is not an npm package name, a version that is not a semantic version, an sdk that is not <package> or <package>@<range>, an empty path or range, a sandbox that is not a boolean), so its default is used.

Fix. Fix the option under targets.mcp in tungsten.yml.

TG0721

MCP tool schema over the agent schema token budget

Meaning. An MCP tool (its description and input schema, measured with the tungsten-tokens estimate after repeated sub-schemas were moved to $defs) is larger than the per-tool schema budget of agent.yml (defaults.disclosure.schema_budget_tokens, 600 by default). Agents pay this cost whenever the tool is listed or described.

Fix. Shorten the operation's and its fields' descriptions (disclosure.prune for the operation, the spec for fields), split the operation, or raise the budget.

TG0722

progressive MCP tool list and index summary over the 2,000-token budget

Meaning. In progressive mode an MCP client first receives the meta tools (search_tools, describe_tool, invoke, preview, list_clusters) and the instructions with the cluster index. Together they exceed the 2,000-token budget of the progressive index, usually because of many clusters or long cluster summaries.

Fix. Shorten the cluster summaries in agent.yml (only their first sentence is used) or merge clusters.

TG0723

macro not exposed as an MCP tool (not emitted by the TypeScript SDK)

Meaning. A macro of the IR is not served by the MCP server because the generated TypeScript SDK, which runs it, does not emit it (see TG0710 of the typescript target).

Fix. Fix the macro in agent.yml so every step calls a callable operation and every reference names $input or an earlier step's as.

TG0724

MCP tool names collide; each is named with a digest of its operation

Meaning. Two or more operations or macros give the same MCP tool name once their namespace, resource path and method are reduced to [a-z0-9_] (/a-b/c and /a/b-c both become ..._a_b_c_get). Numbering them in IR order would hand a name to another operation when one of them is added or removed, so each is named with the first eight hex digits of the digest of its operation id or macro name instead (..._a_b_c_get_1f3e9a0c): stable, but not descriptive.

Fix. Give the operations distinct method names with naming.operations in tungsten.yml.

TG0725

disclosure.mode auto chose the MCP disclosure mode from the measured token size of the discrete tool list

Meaning. defaults.disclosure.mode: auto in agent.yml chooses between the discrete MCP tool list (every tool with its schema) and progressive disclosure (search_tools, describe_tool, invoke) by measuring the discrete tools/list plus its instructions with the tungsten token estimate. Progressive is chosen when that is above defaults.disclosure.list_budget_tokens (default 10000), whatever the number of tools. This note states the measured tokens, the budget and the decision.

Fix. Set defaults.disclosure.mode to discrete or progressive to decide yourself, or change defaults.disclosure.list_budget_tokens.

TG0726

the MCP server's SDK dependency defaults to the typescript target's package

Meaning. The mcp target has no sdk option, so the server depends on the package the typescript target of the same tungsten.yml generates (its package and version) instead of the default <api>-sdk.

Fix. Set targets.mcp.sdk (<package> or <package>@<range>) or targets.mcp.sdk_path to depend on something else.

TG0727

MCP tool output schema over the output token budget even without the types it references

Meaning. The output schema of an MCP tool is larger than four times the per-tool schema budget even though every reference to another type was already replaced by {} (the emitter keeps only the types one reference away from the response body, and none when that is still over the limit). The response type itself is large: many fields or long descriptions.

Fix. Shorten the descriptions in the spec, or raise defaults.disclosure.schema_budget_tokens in agent.yml.

TG0730

macro not emitted by the Python SDK (not in the canonical form)

Meaning. A macro in the IR does not fit the canonical form the Python SDK compiles: a step names an operation that is not callable, a reference names a later or unknown step, an {expr} is not <ref> in [..], <ref> == x or <ref> != x, or a field the macro adds to its input is not a free Python keyword argument name (not an identifier, a keyword, or an argument the extended operation already takes). The SDK is generated without that macro.

Fix. Fix the macro in agent.yml so every step calls a callable operation, every reference names $input or an earlier step's as, and added input fields have identifier names.

TG0731

invalid Python target option; default used

Meaning. An option of the python target is not valid (for example a package that is not a PEP 508 distribution name, a module that is not a lowercase identifier, a version that is not a PEP 440 version, or a models that is neither pydantic nor dataclasses), so its default is used.

Fix. Fix the option under targets.python in tungsten.yml.

TG0732

OpenID Connect scheme sent as a bearer token by the Python SDK

Meaning. The API declares an OpenID Connect security scheme. The Python SDK sends the configured credential as a bearer token and does not run discovery or obtain tokens itself.

Fix. Obtain the token with your OpenID Connect client and pass it in auth under the scheme's name.

TG0733

inline record typed as a plain mapping by the Python SDK

Meaning. A record schema without a name (inline, not a component) has fixed fields. The Python SDK declares a model class per named record only, so this one is typed dict[str, Any] and its fields are not validated client-side.

Fix. Move the schema to components/schemas (or name it with an overlay) so it gets a model.

TG0740

invalid Rust target option; default used

Meaning. An option of the rust target is not valid (for example a crate that is not a crate name, a version that is not semver, or a cli that is neither a boolean nor an object), so its default is used.

Fix. Fix the option under targets.rust in tungsten.yml.

TG0741

type or schema typed as serde_json::Value by the Rust SDK

Meaning. A schema has no Rust type of its own: a union without variants, an allOf that could not be merged into one record, a schema no value satisfies, or an inline enum, union or record without a name. The Rust SDK types it serde_json::Value (the generated type says so in its documentation) and checks its constraints (enum membership, allOf members) when the request or response is validated. Enums of booleans, of numbers with fractions and of integers above i64 are not in this group: they are untagged enums with one variant per JSON type, and enums of several types are unions of constants.

Fix. Move the schema to components/schemas (or name it with an overlay) so it gets a Rust type; for an allOf, make the members compatible so that they merge.

TG0742

macro not emitted by the Rust SDK (not in the canonical form)

Meaning. A macro in the IR does not fit the canonical form the Rust SDK compiles: a step names an operation that is not callable, a reference names a later or unknown step, an {expr} is not <ref> in [..], <ref> == x or <ref> != x, or the input extends an operation that is not callable. The SDK is generated without that macro.

Fix. Fix the macro in agent.yml so every step calls a callable operation and every reference names $input or an earlier step's as.

TG0743

OpenID Connect scheme sent as a bearer token by the Rust SDK

Meaning. The API declares an OpenID Connect security scheme. The Rust SDK sends the configured credential as a bearer token and does not run discovery or obtain tokens itself.

Fix. Obtain the token with your OpenID Connect client and pass it in auth under the scheme's name.

TG0744

operation with different non-JSON or ranged success bodies returns serde_json::Value in the Rust SDK

Meaning. An operation's success responses have different bodies, and they are not all JSON bodies under exact statuses (a body is text, binary or a form, or a status is a range such as 2XX). The Rust SDK types an operation whose exact statuses answer with different JSON bodies as a response enum; for the shapes it cannot represent it returns serde_json::Value and does not validate the response.

Fix. Declare each success status exactly and describe its body as JSON, or call the operation through Dispatch::invoke, which returns JSON.

TG0745

tagged union emitted by the Rust SDK as an untagged one

Meaning. A union has a discriminator, but a variant has no tag value: the mapping does not name it and it is not a component with a name or a constant to take the tag from. A union whose variants all have tags is decoded by the tag, whatever the variants are (records, other unions, maps), like the TypeScript and Python SDKs; this one cannot be, so the Rust SDK tries its variants in order like an untagged union.

Fix. Name every variant under the discriminator's mapping, or give every variant a component of its own.

TG0746

macro output typed as serde_json::Value by the Rust SDK

Meaning. The output of a macro is not an object or a reference whose members the Rust SDK can type: a member is a null, a list, a nested object or a reference into a step the API description gives no single JSON response type for. The macro's method returns serde_json::Value, the output expression evaluated by the runtime.

Fix. Read the members you need from the value, or flatten the output expression to references into steps with a documented response ($step.field) and boolean expressions.

TG0750

argument given as JSON text on the command line of the generated CLI

Meaning. An argument of an operation has no plain flag form in the generated CLI: a parameter whose type is an object, a map, a union or a list of those, or a multipart body whose parts are files. An object, map or union takes JSON text (--filter '{"a":1}', --filter @filter.json or - for standard input) or its members one at a time (--filter.a 1, --filter.created.after 5; a value is typed by the operation's schema, and both forms can be mixed, the members applied on top of the JSON text). A list of those repeats the flag, one JSON value each. A multipart file cannot be given from the command line at all.

Fix. Use the JSON or dotted form, or the SDK for multipart uploads. Nested fields of a request body are JSON-text flags by design and are not reported.

TG0751

resource named like a command of the generated CLI; renamed

Meaning. A resource, or a namespace, has the name of a command the generated CLI provides itself (auth, help, operations, schema). Its commands are under <name>-resource so that both exist.

Fix. Rename the resource under resources in tungsten.yml.

TG0752

flag renamed in the generated CLI because its name is taken

Meaning. The flag derived from an argument has the name of a flag the CLI adds to every command (--json, --body, --yes, --dry-run, --idempotency-key, --timeout, ...) or of another flag of the same command. The argument keeps its name in the SDK; its flag gets the suffix -arg.

Fix. Use the renamed flag, or rename the parameter in the spec or an overlay.

TG0753

two commands of the generated CLI share a path; the later one is numbered

Meaning. Two operations or macros end up with the same command path in the generated CLI (an operation and a macro with the same name, or a resource renamed onto another). The later one gets a numeric suffix so that both stay reachable.

Fix. Give the operations distinct names with naming.operations in tungsten.yml.

TG0760

types no operation reaches were not generated (types.prune_unreferenced)

Meaning. Some components/schemas entries are not reachable from any operation (typically the schemas only the operations excluded by an include predicate use), so they were not generated: no type, no validator, and none of the warnings their schemas would have raised. The note counts them and lists the first names. Operations that are only planned (planned_from) and the error envelope still keep the types they use.

Fix. Nothing to fix. To generate every schema anyway, set types.prune_unreferenced: false in tungsten.yml.

TG0790

invalid WebMCP target option; default used

Meaning. An option of the webmcp target is invalid, such as a malformed npm package, SDK dependency, runtime dependency or version. The emitter uses its documented default for that option.

Fix. Fix the option under targets.webmcp in tungsten.yml.

TG0791

WebMCP generated file could not be added

Meaning. The WebMCP emitter could not add a generated file to its output. The diagnostic identifies the file and the reason.

Fix. Check the target configuration. If the generated path is invalid or duplicated, report an emitter bug.

TG0792

WebMCP SDK dependency defaults to the TypeScript target package

Meaning. The webmcp target has no explicit SDK dependency, so the browser adapter depends on the package and version the typescript target generates.

Fix. Generate and make that TypeScript package available, or set targets.webmcp.sdk (<package> or <package>@<range>) or targets.webmcp.sdk_path.

TG0793

WebMCP preview tool name collides with an invocation tool

Meaning. A confirmation-bearing WebMCP tool's generated __preview name is already used by another invocation tool. The browser runtime rejects a registration whose selected tools contain that collision.

Fix. Rename the colliding operation or macro so invocation and preview tool names are distinct.

External emitters (TG08xx)

TG0801

external emitter not found or cannot be started

Meaning. A target in tungsten.yml declares external, but its emitter could not be run: with external: true there is no executable tungsten-emit-<name> on PATH; with a command, its program does not exist, is not executable, or the operating system refused to start it. Nothing is generated for the target.

Fix. Install the emitter and put it on PATH, or point external at it (a path with a slash is relative to tungsten.yml, a bare name is looked up on PATH). tungsten emitters lists what tungsten finds.

TG0802

external emitter speaks a different protocol version

Meaning. The external emitter answered with a protocol version other than 1, the only version this tungsten speaks. The response is not read.

Fix. Use an emitter release built for protocol 1, or a tungsten release that speaks the emitter's protocol. tungsten emitters shows the protocol each emitter reports.

TG0803

external emitter failed: non-zero exit, timeout or output over the cap

Meaning. The external emitter ran but did not finish normally: it exited with a non-zero status or was killed by a signal (the end of its standard error is quoted), it did not finish within timeout_ms (default 60 s), or it wrote more than max_output_bytes (default 64 MiB) to standard output. Nothing is generated for the target.

Fix. Run the emitter by hand with a request on its standard input to see why it fails. Raise timeout_ms or max_output_bytes on the target if the limit is too tight for a large API.

TG0804

external emitter returned an invalid response

Meaning. The external emitter's standard output is not a valid response of protocol 1: it is not JSON, a field has the wrong type, a file has both or neither of content and content_base64, a base64 value is malformed, a path is listed twice, or there are more than 10000 files. The same code reports a --describe document that is not valid. Nothing is generated for the target.

Fix. Check the emitter against the schema printed by tungsten schema external-emitter (validate the response against #/$defs/EmitResponse).

TG0805

external emitter returned a path outside the output directory

Meaning. The external emitter named a file path that is absolute, has a .., . or empty segment, a backslash, a NUL byte or a drive prefix, or starts with .tungsten/. Generated files are written only inside the target's out directory, so the whole response is refused and nothing is written.

Fix. Make the emitter return paths relative to the output directory with / separators. It never needs the absolute output path.

TG0806

diagnostic reported by an external emitter

Meaning. A diagnostic reported by an external emitter, kept with its severity. The message reads external emitter <name>: <code>: <text>, where <code> is the emitter's own code. An error fails the target; under --strict a warning does too.

Fix. Fix the cause the emitter describes; tungsten explain knows only tungsten's own codes, so look the emitter's code up in the emitter's documentation.

Staleness and CI (TG09xx)

TG0901

generated output is stale relative to its inputs

Meaning. A target's output directory differs from what tungsten generate would write now: a generated file is missing or has other content, a file of the previous generation would be removed, or .tungsten/manifest.json is missing. Usually the spec or a manifest changed after the last generation, or a generated file was edited by hand.

Fix. Run tungsten generate and commit the result. Put hand-written code in the target's custom/ files, which generation never overwrites.

TG0902

API surface snapshot of the last generation is missing or unreadable; the change is not classified

Meaning. tungsten diff --semver and tungsten report compare the API surface recorded by the last generation (.tungsten/surface.json in the target's output directory) with the current one. The target has no snapshot (it was never generated, or generated by a tungsten that did not record one), or the snapshot cannot be read or parsed, so the change since that generation is not classified as major, minor or patch.

Fix. Run tungsten generate to record a snapshot; later runs of tungsten diff --semver classify the changes made after it. Do not edit files under .tungsten/.

TG0920

overlay suggest: a diagnostic has an overlay fix but its value cannot be derived; skipped

Meaning. tungsten overlay suggest found a diagnostic that an overlay action could fix, but the value the action needs cannot be derived without guessing: the operation has no single id in the IR (its document is shared by several namespaces, or the operation is expanded from an rpc envelope), or its position cannot be written as an Overlay target. The overlay suggestion leaves it out; it never writes placeholder text.

Fix. Fix the diagnostic by hand in the spec (add or change the operationId), or in the overlay you maintain yourself.

TG0921

overlay suggest: no overlay action can fix this diagnostic code

Meaning. tungsten overlay suggest only writes actions whose fix the compiler has already decided: the id of an operation without operationId, and the unique id of an operation whose operationId is used more than once. Warnings of any other code, such as an inferred pagination (TG0501) or a renamed field or type (TG0401), need a decision or live in tungsten.yml; the count is reported once per code.

Fix. Use tungsten explain on the code for its usual fix, for example declare the pagination under pagination in tungsten.yml.

Case study: the ZROtext pilot

ZROtext is an open-source Android SMS gateway with a Rust server. Its contract is three OpenAPI 3.1 documents (the public, sealed and workflow planes, 44 operations in all), and it ships hand-written clients, which makes it a useful baseline. It was the first real API tungsten was pointed at: the TypeScript, Python and Rust SDKs, the generated CLI and the MCP server were generated from those documents and two manifests, then every scenario was run against a live instance on loopback. Nothing in the pilot sends a real SMS. This page summarises what the run showed.

Headline

  • The generated TypeScript, Python (sync and async) and Rust SDKs pass all 26 live scenarios against the real server: 4 driver runs, 104 scenario runs.
  • The parity check finds the four records identical (26/26, three times). The generated Rust CLI passes 20/20 live checks.
  • All the live scenarios pass in every SDK and in the CLI.
  • No runtime or emitter defect was found. The live run found four defects in the agent manifest; all are fixed with offline regression tests, and one led to the new tungsten check warning TG0614.
  • ZROtext's hand-written alpha clients behave the same on the wire but give their caller only status, code and Retry-After.

Criteria

Result
Compile44 operations, 0 errors, 3 TG0301 warnings
Golden outputgoldens regenerated
Generated CLIunchanged
Contract suite64/64 on each of 4 runs, parity 64/64 ×3
Tool schema sizelargest tool schema 538 tokens, median 116, none over the budget
Webhook lifecyclewebhook lifecycle with the verification hook, the bootstrap macro, replay of a dead delivery (generation 2, created true then false, then 409 (not eligible)): pass in 4 SDK runs and the CLI
Submit and cancel a messagesubmit 202 created:true → identical resubmit 202 created:false → status 200 → cancel 204 → cancel again 409 never → status 200: pass in 4 SDK runs and the CLI
Missing idempotency keymissing key refused before sending: attempts 0, the proxy recorded 0 requests; CLI exit 2
Disabled featuregate off → bare 404 → GATE_DISABLED with the gate's text (also the sealed and workflow planes); CLI exit 3
Pagination and revoke50 + 50 + 6 devices via the list-all macro; revoke needs confirmation; CLI exit 6 without --yes
Timeout with an unknown outcomethe 600 ms client timeout fires while the proxy holds the 202 for 2.5 s → OUTCOME_UNKNOWN / same_key_only; the same-key retry gives created:false; the owner message list holds one message; CLI exit 5

Extra live scenarios: 409 for the same key with another body; the await macro reports timed_out honestly; a device queue holds 16, the 17th is 429 (queue full) with Retry-After 60 and same_key_only; a CSRF value the session does not own and a foreign origin are 403; an unknown bearer is 401; the key bound to one device is 403 for the other; an unknown endpoint is 404; limit 21 on the delivery page is refused before sending.

Findings

Each finding has a fix and an offline regression test, so the mock-based suite catches it next time.

  1. Reserved callback hosts. The server refuses callback hosts under .test, .example, .invalid, .local and similar suffixes with a 400 error; the example host and the mock both accept them. Fix: the agent.yml remediation text names the rule.
  2. Expiry window. An expiry more than 15 minutes ahead gets a bare 400 that the spec never explains. Fix: an operation-specific remediation with retryable: after_remediation.
  3. Ungated planes. The sealed plane's gate was declared but no sealed operation named it, and the workflow plane had no gate, so a default deployment's 404 came back as NOT_FOUND. Fix: a gate on all 15 sealed and 9 workflow operations, plus a gate for the workflow plane; new tungsten check warning TG0614 for a gates entry no operation uses.
  4. Rate-limit text. A rate-limit answer on submit carried the "four requests in flight" text when the live cause was the 60-per-minute budget, and had no same-key rule. Fix: an operation-specific remediation with same_key_only.
  5. Harness. Live runs were order dependent because of the server's budgets and caps (8 webhook endpoints, 16 pending messages per device, 60 submissions per minute). Fix: the proxy's reset restores server state before every scenario.
  6. Policy, unchanged. The manifest requires a UUIDv4 submit key; the server accepts any 1–128 character [A-Za-z0-9._-] key.

Comparison baseline

Hand-written (ZROtext's own clients)Generated by tungsten
Coveragealpha plane only (no owner-webhook or device client)public, sealed and workflow planes
SizeTypeScript 284 lines, Python 239; signature verification 96 / 80TS 6,461 lines, Python 8,535, Rust 13,140 + CLI 812
MCP tools/listits MCP server 9,473 tokens, its send-only MCP server 539progressive 765, discrete 14,205

Run live through the same flows:

FlowHand-writtenGenerated
submit, resubmit, status, cancelsamesame
cancel againan API error with status 409envelope CONFLICT / never with remediation
queue fullan API error with status 429RATE_LIMITED / same_key_only, retry_after_ms 60000
bad keya type or value error before sendingVALIDATION_FAILED before sending
drop or timeoutan outcome-unknown error; same-key retry gives created:falseOUTCOME_UNKNOWN / same_key_only with next_action; same-key retry gives created:false

Equal: wire behaviour and no automatic retry. Better in the generated SDKs: the envelope, preview tokens, secret handling and owner-plane coverage. Missing or worse in the generated SDKs: webhook signature verification, sealed envelope composition, and the stricter UUIDv4 key.

Downloads: llms.txt

The docs target writes two plain-text files meant to be loaded into a model's context: llms.txt, an index of the API with one line per operation, and llms-full.txt, the full reference with parameters, safety tiers, error remediation and examples. These two were generated for the ZROtext pilot.