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 manifest reference for
tungsten.ymlandagent.yml; - safety and runtime behaviour for the error envelope, retries, idempotency, previews and confirmation;
- the MCP server guide and browser WebMCP guide;
- the SDK guides for TypeScript, Python and Rust;
- the CLI reference and the diagnostic codes;
- a case study of a real API and the
llms.txtdownloads generated for it.
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
retryableverdict. The same 14 members in every runtime, ready to paste into a model's context. - Safety tiers. Each operation is
read_only,mutating,destructiveorirreversible, defaulted from the HTTP method and refined inagent.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-understandfor 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_UNKNOWNwith 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 --checkfails when output is stale.
Targets
| Target | Emits | Runtime it depends on |
|---|---|---|
typescript | Typed SDK, Result values, Zod validation | @elysra/tungsten-runtime |
python | Sync and async SDK, strict Pydantic v2 models | tungsten-runtime (PyPI) |
rust | Async SDK (serde, reqwest) and, optionally, a generated CLI | tungsten-runtime and tungsten-cli-kit (crates) |
mcp | MCP server over stdio or Streamable HTTP, discrete or progressive | @elysra/tungsten-mcp |
webmcp | Browser ESM package with a tool manifest and explicit registration helper | @elysra/tungsten-webmcp and the generated TypeScript SDK |
docs | llms.txt, llms-full.txt, tools.json, README | none |
go (external) | Go SDK package (models, resources, typed client, pagination and stream helpers), generated by tungsten-emit-go from emitters/go | github.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
- The frontend reads OpenAPI 3.0 and 3.1, resolves references and applies overlays.
- The IR is a versioned JSON document (
tungsten ir dump, schema viatungsten schema ir). - The agent compiler merges
agent.ymlinto it: safety tiers, idempotency, confirmation, remediation text, verification hooks, macros and the disclosure mode for MCP. - Emitters write each target from the IR. They are pure functions of their inputs.
- 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.
| System | CPU | Rust target | Archive |
|---|---|---|---|
| Linux, static musl | x86-64 | x86_64-unknown-linux-musl | .tar.gz |
| Linux, static musl | ARM64 | aarch64-unknown-linux-musl | .tar.gz |
| Linux, glibc | x86-64 | x86_64-unknown-linux-gnu | .tar.gz |
| Linux, glibc | ARM64 | aarch64-unknown-linux-gnu | .tar.gz |
| macOS | Intel | x86_64-apple-darwin | .tar.gz |
| macOS | Apple Silicon | aarch64-apple-darwin | .tar.gz |
| Windows | x86-64 | x86_64-pc-windows-msvc | .zip |
| Windows | ARM64 | aarch64-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.
| Feature | Status | Diagnostic / exact check | Constraint |
|---|---|---|---|
OpenAPI 3.0 nullable and exclusive numeric bounds | Supported | normalization checks, normalized_30_equals_its_31_form | 3.0 schemas normalize to the internal 3.1 representation; requiredness remains separate from nullability. |
| External complete OpenAPI documents with a different 3.x dialect | Supported | compatibility 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_fragments | A 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 unions | Supported | compatibility checks, nullable_type_arrays_and_allof_record_composition_keep_presence; nullable checks | null becomes presence/nullability metadata; named types keep their non-null shape. Scalar unions retain their variants. |
oneOf / anyOf with string discriminators | Supported | union checks; sanitized error-envelope fixture | Explicit mappings or distinct required constant string tags select variants. Invalid mappings produce TG0306. |
| Ambiguous untagged schema unions | Limited | TG0301; union checks | Ordered variants approximate ambiguous unions. Acceptance by a generated validator does not guarantee JSON Schema oneOf exclusivity. |
allOf record composition | Limited | TG0302 when flattening is unsafe; composition checks, nullable_type_arrays_and_allof_record_composition_keep_presence | Compatible record fields and constraints flatten. Other intersections remain explicit; no claim of arbitrary JSON Schema intersection solving. |
Local and relative-file JSON Pointer $ref | Supported | reference checks, mixed-dialect Petstore fixture | References stay under an input directory. Dangling refs produce TG0201; traversal produces TG0204. |
Schemas inside Path Item $ref siblings | Limited | compatibility checks, path_item_ref_siblings_have_their_schemas_normalized_and_checked, path_item_siblings_follow_external_files_without_following_example_payload_refs | The 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, bytes | Supported in the IR; target behavior is limited by runtime capabilities | media-type corpus assertions, multipart fixture | Wire 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, $dynamicRef | Limited | TG0303; pattern-properties fixture | These keywords are reported and ignored. Remaining schema shape still builds; generated validation can be broader than the source. |
Anchor $ref fragments, remote $ref fetching | Rejected | TG0201 (anchor), TG0202 (remote); reference checks | Vendor remote descriptions locally and use JSON Pointer file references. $id URI scope resolution and dynamic anchors are not implemented. |
| Swagger 2.0 / OpenAPI 3.2 | Rejected | TG0103; version checks, unsupported_external_document_versions_are_diagnosed_at_the_document_and_reference | Convert 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:
| Target | Runtime packages | Package source |
|---|---|---|
| TypeScript SDK | @elysra/tungsten-runtime | npm |
| MCP server | @elysra/tungsten-runtime, @elysra/tungsten-mcp | npm |
| Browser WebMCP | @elysra/tungsten-runtime, @elysra/tungsten-webmcp, generated TypeScript SDK | npm and your generated package |
| Python SDK | tungsten-runtime | PyPI |
| Rust SDK | tungsten-runtime | crates.io |
| Rust CLI | tungsten-runtime, tungsten-cli-kit | crates.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 --targetsand a project-awaredoctor --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 --migrationupgrade 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
webmcptarget: 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-webmcpruntime. Registration defaults to read-only operations, supports scoped opt-in writes and macros, confirmation previews, cancellation and disposal. The runtime adapts the experimentaldocument.modelContextAPI and the legacynavigator.modelContextshape; generated-package browser smoke passed in Chromium 154.0.8037.57 withWebMCPTestingenabled. 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 intungsten-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'sretryunder a cap (maxReconnects, default 3), without repeating delivered events; an idle timeout ends or reconnects a silent stream (STREAM_IDLE);on,collect,reduce,firstandcancelhelpers.tungsten mockcontinues a stream afterLast-Event-IDand can drop or stall one for testing. -
tungsten lsp: a Language Server Protocol server over stdio fortungsten.yml,agent.ymland overlay files. It reports the same diagnostics astungsten checkon 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.ymlwithexternal: true(or a command) is generated by a separatetungsten-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, sogenerate --check, the output manifest and the API surface diff work unchanged. New:tungsten emitters,tungsten schema external-emitter, diagnostics TG0801 to TG0806 and the exampleexamples/external-emitter-markdown. -
Optional embedding index for the MCP server's
search_tools, off by default and with no model shipped.targets.mcp.search.embeddingsnames a provider (a command speaking a small JSON protocol, or a plainhttp:///embeddingsendpoint);tungsten generatewritesindex.embeddings.binandindex.embeddings.jsonnext tomanifest.json, caching vectors by content hash.@elysra/tungsten-mcpranks hybrid (BM25 and cosine, reciprocal rank fusion) whensearch.embeddingsis set, and falls back to BM25 with the reason in the result when the query embedder fails. Diagnostics TG0570 to TG0574.
Changed
-
Rust runtime:
StreamEventhas a new public fieldreconnects; code that builds or destructuresStreamEventwith a struct literal must add it (or use..). -
Go SDKs through the external emitter interface:
emitters/gobuildstungsten-emit-go(enable it withgo: { 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 runtimegithub.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.ymlandagent.ymlmanifests, a versioned intermediate representation (tungsten ir dump,tungsten schema) and diagnostics with stable codes (tungsten check,tungsten explain). - Commands:
init,check,generate(with--checkfor stale output),diff,report(text, JSON and a self-contained HTML report),doctor,ir dump,schema,explainandmock. - TypeScript SDK emitter and the
@elysra/tungsten-runtimeclient runtime (Apache-2.0):Resultvalues, 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-runtimePython package (Apache-2.0). - Rust SDK emitter (async client, a blocking client and typed macro outputs)
and the
tungsten-runtimecrate (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-mcpruntime: 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>_streammethods) and in the MCP server, which returns the collected events. - Pagination: opaque string cursors and
after_id/has_morestyles are inferred from the OpenAPI description. - Documentation emitter:
llms.txt,llms-full.txt,tools.jsonand 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.shandinstall.ps1installers 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
authorizationCodeflow. - Repository guard and community files (contributing guide, security policy, code of conduct, issue forms and pull request template).
Changed
tungsten initwrites a defaulttargetsblock.- 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
gatesentry inagent.ymlmatches 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
pathis 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 namedroot. A segment that mixes text and placeholders ({date}.csv) is aTemplatesegment in the IR and counts as an item, like{param}; it never names a resource. - Custom methods. A
:verbsuffix 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 ins. So/{id}/enableis the actionenable, while/{id}/sealed-eventsis a sub-resource whose POST iscreate. - Method names.
naming.operationswins; then, relative to the resource's path:listfor a GET of a collection,getfor 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) anddeleteon a singleton path,get/update(PATCH, PUT) /deleteon 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,list2for/v1/petsand/v2/pets, which merge into onepetsresource). - 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.sendwithname_strip_prefix: "workflow."→tools›action›send. A method without dots is placed like an HTTP operation. Child resources declared without apathare matched by name. path_prefixis the longest common segment prefix of the resource's operations and the path that names it (configuredpath, 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
paththat matches no path of the input is TG0604; anaming.operationsorpaginationkey that names no operation is TG0603.
Inputs, filters and gates
-
includeandplanned_fromcompare one operation extension with a value. Withoutincludeeverything is included, and an operation that matchesplanned_fromis planned. Withinclude, an operation that fails it is planned when it matchesplanned_fromand dropped otherwise. Every excluded operation is a TG0504 info naming the reason; planned ones keep that reason inOperationStatus::Plannedand live inNamespace.planned, never in the resource tree. -
rpc_unflattenneeds the named path and method to exist and its JSON request body to be aoneOfwhose every member has a stringconst(or one-valueenum) underdiscriminator, with distinct values that leave a dotted name aftername_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.namefrom 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) ornone: true, which only switches inference off. Declared entries apply to planned operations too. -
types.prune_unreferenced(defaulttrue) 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 oneagent.ymlnames); 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.falsegenerates every schema.
Auth profiles
- A
compositeprofile becomes oneAuthScheme::Compositewith the profile's name, its parts in order (cookie,headerwithequals_cookie/from_config/when: mutation→mutation_only,bearerwithenv/prefix) andsatisfiesresolved to IR scheme names. Asatisfiesentry 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
Authrole, exceptOrigin, which keepsOrigin. configentries of other profiles are runtime configuration and do not change the IR.- a non-composite profile with
bearerconfigures the HTTP bearer scheme of the same name (spec or IR name):HttpBearer.prefixandHttpBearer.envare additive IR fields. The spec'sbearerFormatstaysformatand 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
| Key | Meaning | Default |
|---|---|---|
provider | command (JSON on standard input and output) or http (the common /embeddings request shape) | required |
command | the command line of a command provider, run from the directory of tungsten.yml | required for command |
url | the endpoint of an http provider | required for http |
model | model name sent to the provider and recorded in index.embeddings.json | required |
dimensions | vector length; a different answer is TG0572 | taken from the first answer |
api_key_env | environment variable with the endpoint's API key (http only) | none |
required | a provider failure is an error instead of a warning and BM25 only | false |
batch_size | documents per provider call | 32 |
timeout_ms | wall-clock limit of one provider call | 60000 |
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
| Tier | Meaning | SDK | CLI | MCP |
|---|---|---|---|---|
read_only | No side effects | retries allowed | runs | readOnlyHint: true |
mutating | Side effect, reversible or idempotent | no blind retry; preview available | runs | destructiveHint: false, idempotentHint from policy |
destructive | Deletes or disables something; recoverable only by re-creation | requires {confirm: true} option or preview first | requires --yes or TTY confirmation | destructiveHint: true; needs confirmation_token from a prior preview call |
irreversible | Cannot be undone (money, messages sent to people, permanent deletion) | requires {confirm: {token}} obtained from preview() | requires --yes --i-understand and prints the summary | destructiveHint: true; needs confirmation_token; the MCP server refuses unless the client supplies the token returned by preview in the same session |
Idempotency policies
| Policy | Who generates the key | Persistence | Retry rule |
|---|---|---|---|
none | nobody | – | mutation never auto-retried |
auto | runtime, UUIDv4 per logical call | runtime IdempotencyStore (memory by default; file or caller-provided) | retried with the same key within retries.mutating |
caller_owned | the caller, validated against format | caller must persist; persist: required makes the runtime refuse a call without a key | retried with the same key only |
content_hash | runtime, hash of the canonical body | store keyed by hash | same |
content_identity | the body itself is the identity (binary envelopes) | n/a | resend 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
| Mode | Behavior |
|---|---|
local | Validate 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. |
header | As local, then send with preview.header (for example X-Dry-Run: true) and return the server's response as the preview. |
endpoint | Call 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 }
| Key | Meaning |
|---|---|
field | The 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_suffixes | The 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_minutes | The 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. |
status | The refusal's status, 400 to 599; default 400. |
code | The API error code written at the error envelope's code field (text/plain APIs lead the text with it). |
message | The 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 requiredIdempotency-Keyheader makes a side-effecting operationcaller_ownedwithpersist_required) > the manifest'sdefaults> the method defaults. A rule that cannot apply is dropped with a warning (TG0603–TG0611) and the rest of the manifest still applies. AgentModelis flat:macros,clusters,gates,error_codes,ambiguous_statuses,non_json,retries,unknown_outcome,disclosure.OperationAgentMetaalso hasremediation_noteandhidden. Agatesentry may declaredisabled_status.compact_docis the summary plus the description, sentences with adrop_phrasesentry removed, the firstmax_sentenceskept, cut at a word boundary todescription_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-clusternames, 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 carriescluster,sensitive_response_fieldsandshown_once. Its safety is the strictest step's (a poll counts asread_only); a weaker declared tier is raised (TG0609). - Macro confirmation is macro-level. A macro whose tier is
destructiveorirreversible(or that has such a step) needs one confirmation for the whole run:confirm: truefordestructive, 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
defaultis applied when the input omits it, and apollstep whose budget runs out binds itsasname tonull(so{expr: "$status == null"}reports the timeout; thetimed_outoutput). The first failing step's envelope is returned with the completed steps named in its remediation. disclosure.prune.drop_fieldsandkeep_examplesare accepted but not applied in this release: agent tool schemas list exactly the SDK's arguments, and a non-emptydrop_fields(orkeep_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.ymltargets must have distinct, non-nestedoutdirectories (TG0612): each target's.tungsten/manifest.jsonremoves 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):
| Category | Source | Default retryable |
|---|---|---|
VALIDATION_FAILED | pre-flight or 400 with envelope | never |
MALFORMED_REQUEST | non-JSON 400 | never |
REQUEST_TOO_LARGE | 413 | never |
AUTH_FAILED | 401, 403 | never |
NOT_FOUND | 404 (or gated route) | never |
CONFLICT | 409 | never |
PRECONDITION_FAILED | 402, 412, 422 by manifest | never |
RATE_LIMITED | 429 | after_delay |
UPSTREAM_UNAVAILABLE | 502, 503, 504 on a read | after_delay |
OUTCOME_UNKNOWN | timeout, reset, ambiguous status or unexplained 5xx on a mutation | same_key_only (after_remediation without replay protection) |
TRANSPORT_FAILED | DNS, TLS, connection refused before send | after_delay |
CONFIRMATION_REQUIRED | destructive/irreversible without token | never (call preview) |
GATE_DISABLED | status matches an x-runtime-gate disabled status | never |
UNEXPECTED_RESPONSE | response failed strict validation | never |
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
maxattempts onRATE_LIMITED,UPSTREAM_UNAVAILABLE,TRANSPORT_FAILED, with full-jitter exponential backoff, honoringRetry-After(seconds or HTTP-date) when present, capped bymax_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 isOUTCOME_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 kind | Runtime behavior |
|---|---|
bearer | Authorization: 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_key | header, query or cookie |
basic | header |
oauth2_client_credentials | token fetch and refresh with single-flight |
oauth2_authorization_code | helpers 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" |
composite | the 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_challengewithcode_challenge_method=S256, then extra parameters in key order that never override these. RFC 3986 percent-encoding.exchangeCodeandrefreshPOST a form to the flow'stokenUrl(refreshUrlfor a refresh,tokenUrlwhen 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 addsclient_idto 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_FAILEDbefore 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 isAUTH_FAILEDasking 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
errorcode such asinvalid_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.retriesreaches the SDK asApiDescriptor.retries(readOnly,mutating). A call's policy is the runtime default, then its operation's tier, then the client's ownretriesoption, field by field. A mutation is still retried only with a key or an identity body. - Unknown outcome:
next_actioncomes 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), acontent_identitymutation'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 asUPSTREAM_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.$responseand$argsreferences inexpectandterminalare resolved before polling; one that does not resolve never matches.
Rules fixed by review
- Replay protection decides
retryablefor an unknown outcome: with a key or an identity body it issame_key_only; without either (an unkeyed create,workflow.action.send, cancel, enable, rotate) it isafter_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 asPRECONDITION_FAILED,NOT_FOUND,RATE_LIMITED) wins over this and overambiguous_statuses; one that maps toUPSTREAM_UNAVAILABLE(Acme'sunavailable, which can report a failed commit) does not. next_actionuses the verification hook only when its arguments do not need$response: after a lost submit the caller has nomessage_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 byasname. A macro's envelope names the macro; when a completed mutating step has no replay protection the envelope says not to rerun the macro, downgradesafter_delay/same_key_onlytoafter_remediationand 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 (
"",.,..) isVALIDATION_FAILEDbefore anything is rendered, confirmed or sent: URL parsing removes such segments, soDELETE /items/..would reach the collection. - Sensitive request fields (
AgentMeta.sensitiveRequestFields) are redacted inreceived_value(also below a parent value), in previews, in the body middleware sees (RequestContext.bodyis the redacted rendering whenever redaction changed a JSON or form body) and are scrubbed from diagnostics. - An optional argument set to
undefinedis absent (TypeScript callers withoutexactOptionalPropertyTypespass{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-streamwith the stream's request flag set to true. A caller'sAccept(the call's, else the client's; any casing of the name) is merged withtext/event-streaminto one header, never duplicated: its media ranges in order without repeats,text/event-streamfirst 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 isUNEXPECTED_RESPONSE. - The body is parsed per WHATWG HTML 9.2 (LF/CRLF/CR, split chunks, comments,
multi-line
data,event,idandretrypersisting across events, a leading BOM dropped, an event still open at the end discarded).datais JSON; the declareddonevalue ends the stream without an item. - Each item carries
value,event(defaultmessage),id,retryand the response meta. Events are checked against the event type pervalidateResponses:strictends the stream withUNEXPECTED_RESPONSE(events[N].path),warnreports throughonDiagnosticand delivers,offdoes 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_FAILEDorUPSTREAM_UNAVAILABLEfor 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
doneevent the operation declares; a stream that declares none ends cleanly) is reconnected withLast-Event-IDset to the last event id delivered, up tomaxReconnects/max_reconnectstimes (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 lastretryvalue (250 ms when it sent none) capped atreconnectMaxMs/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 ignoresLast-Event-IDand 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 reportreconnectsin their meta (the stream so far),idandretrycarry 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 withUPSTREAM_UNAVAILABLE(read) orOUTCOME_UNKNOWN(mutation) whosecodeisSTREAM_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_IDLEis acodevalue, 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 hascollect_values),reduce(fold the events, e.g. concatenate text deltas),first(predicate)(then close the stream) and cancellation (TypeScriptcancel()or theAbortSignal; Pythoncancel(), also from another thread or task, which ends a blocked read; Rust: drop the stream). They return the result andreconnects, or the failure with what was gathered before it (partial, TypeScript and Python; RustFolded).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 withUNEXPECTED_RESPONSE(failed_parameterevents[N],expectedan 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 withUNEXPECTED_RESPONSE(failed_parameterevents, the events so far aspartial) aftermaxStreamBytesof event JSON (default 4 MiB) ormaxStreamMs(default 60 s, whatever the idle timeout; the connection is closed); both areServerOptions. The Rust dynamicDispatch::stream(generatedcollect_events,EventStream::collect_values) ends the same way, last item an error, afterClientOptions::max_collect_bytes(4 MiB) ormax_collect_time(60 s). Python'scollect()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 thetools/callrequest carries aprogressToken, each event collected is anotifications/progress(progressis the number of events so far,messagesays how many were collected and how often the stream reconnected), so progress keeps flowing across a reconnect. - Mock:
tungsten mockserves a generated stream from the event after the numericLast-Event-IDof the request (the done sentinel has no id and stays), and the injectionsdrop-after=<n>[;retry=<ms>](the firstnevents, then the connection breaks) andstall-after=<n>[;ms=<ms>][;retry=<ms>](then silence forms, default 30 s) make a resumable and an idle stream;retrystarts the stream with aretry:line. Programs can apply them (inject); a scripted body still replays whatever it says, which is how the contract tests a server that ignoresLast-Event-ID. - Detection (build): the stream is carried by an exact success status, else a
2XXrange, elsedefault(adefaultthat declarestext/event-streamand 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:
| Tool | Purpose |
|---|---|
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
structuredContentand 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_onceresponses 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.tsreads the manifest withloadManifest(new URL("../manifest.json", import.meta.url)), builds oneClientCoreper MCP session from the environment (one variable per credential, the base URL,<API>_MCP_MODE) and starts withserve(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.invokeandpreviewtake{name, arguments}(discretepreviewtakes{tool, arguments}); the reserved input fieldsidempotency_keyandconfirmation_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;structuredContentis thePreviewResultwith itssteps. - Envelopes stay the runtime's; when their
remediationornext_actionnames 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_listrenderstools/listof either mode exactly as@elysra/tungsten-mcpdoes (the advertised output schemas with the envelope branch, the redaction markers and the verification result; thepreviewmeta tool; the cluster enum), andlisting_tokensadds that mode's instructions. TG0722, the README table andtungsten reportuse 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.
| Variable | Meaning |
|---|---|
OPENAPI_BASE_URL | Base URL of the API (default http://petstore.swagger.io/v1). |
OPENAPI_MCP_MODE | discrete 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 annotatedreadOnlyHint, so a host can allow it without confirmation;search_toolsanddescribe_toolresults carry each tool's safety tier and annotations, andinvoke,invoke_readandpreviewresults 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 onesdestructiveHint. - Failures are returned as the diagnostic envelope (
category,retryable,remediation,next_action), never as protocol errors. AfterOUTCOME_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.
| Measure | Tokens | Budget |
|---|---|---|
Progressive tools/list and instructions (with the cluster index) | 833 | 2000 |
Discrete tools/list and instructions (3 tools) | 529 | |
Largest tool schema (openapi_pets_list) | 51 | 600 |
| Median tool schema | 48 | 600 |
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.
| Target | Guide | Runtime it depends on |
|---|---|---|
typescript | TypeScript SDK | @elysra/tungsten-runtime |
python | Python SDK | tungsten-runtime (PyPI) |
rust | Rust SDK and CLI | tungsten-runtime and tungsten-cli-kit (crates) |
docs | Docs target | none |
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
| File | Purpose |
|---|---|
llms.txt | Index for agents: one line per callable operation with its safety tier and idempotency rule. |
llms-full.txt | Full reference for agents: arguments, responses, errors, remediation and types. |
tools.json | Function-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).
| Operation | Endpoint | Summary | Safety | Idempotency |
|---|---|---|---|---|
openapi.pets.list | GET /pets | List all pets | read_only | none |
openapi.pets.create | POST /pets | Create a pet | mutating | none |
openapi.pets.get | GET /pets/{petId} | Info for a specific pet | read_only | none |
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.
| Category | When | Retryable by default |
|---|---|---|
VALIDATION_FAILED | arguments failed pre-flight validation, or a 400 with an error body | never |
MALFORMED_REQUEST | a 400 without an error body | never |
REQUEST_TOO_LARGE | 413 | never |
AUTH_FAILED | 401 or 403 | never |
NOT_FOUND | 404 | never |
CONFLICT | 409 | never |
PRECONDITION_FAILED | 402, 412 or 422 as the manifest says | never |
RATE_LIMITED | 429 | after_delay |
UPSTREAM_UNAVAILABLE | 502, 503 or 504 on a read | after_delay |
OUTCOME_UNKNOWN | a timeout, reset, ambiguous status or unexplained 5xx on a mutation | same_key_only (after_remediation without an idempotency key) |
TRANSPORT_FAILED | DNS, TLS or connection refused before sending | after_delay |
CONFIRMATION_REQUIRED | a destructive or irreversible call without confirmation | never |
GATE_DISABLED | a gated route answered with its disabled status | never |
UNEXPECTED_RESPONSE | a response that failed strict validation | never |
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.
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 checkwarning 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 | |
|---|---|
| Compile | 44 operations, 0 errors, 3 TG0301 warnings |
| Golden output | goldens regenerated |
| Generated CLI | unchanged |
| Contract suite | 64/64 on each of 4 runs, parity 64/64 ×3 |
| Tool schema size | largest tool schema 538 tokens, median 116, none over the budget |
| Webhook lifecycle | webhook 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 message | submit 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 key | missing key refused before sending: attempts 0, the proxy recorded 0 requests; CLI exit 2 |
| Disabled feature | gate off → bare 404 → GATE_DISABLED with the gate's text (also the sealed and workflow planes); CLI exit 3 |
| Pagination and revoke | 50 + 50 + 6 devices via the list-all macro; revoke needs confirmation; CLI exit 6 without --yes |
| Timeout with an unknown outcome | the 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.
- Reserved callback hosts. The server refuses callback hosts under
.test,.example,.invalid,.localand similar suffixes with a 400 error; the example host and the mock both accept them. Fix: theagent.ymlremediation text names the rule. - 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. - 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: agateon all 15 sealed and 9 workflow operations, plus a gate for the workflow plane; newtungsten checkwarning TG0614 for agatesentry no operation uses. - 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. - 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.
- 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 | |
|---|---|---|
| Coverage | alpha plane only (no owner-webhook or device client) | public, sealed and workflow planes |
| Size | TypeScript 284 lines, Python 239; signature verification 96 / 80 | TS 6,461 lines, Python 8,535, Rust 13,140 + CLI 812 |
MCP tools/list | its MCP server 9,473 tokens, its send-only MCP server 539 | progressive 765, discrete 14,205 |
Run live through the same flows:
| Flow | Hand-written | Generated |
|---|---|---|
| submit, resubmit, status, cancel | same | same |
| cancel again | an API error with status 409 | envelope CONFLICT / never with remediation |
| queue full | an API error with status 429 | RATE_LIMITED / same_key_only, retry_after_ms 60000 |
| bad key | a type or value error before sending | VALIDATION_FAILED before sending |
| drop or timeout | an outcome-unknown error; same-key retry gives created:false | OUTCOME_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.