Changelog¶
All notable changes to AAT are documented here. The format follows Keep a Changelog; versions follow semver with a 0.x caveat: the graph and plan formats may still change before 1.0.
Unreleased¶
Added¶
- Runs can be replayed with the same pool picks. Every run draws its pool orders and
randomselections from a seed, and a run that made such a choice prints it;aat run show, the archive, andsummary.jsonrecord it.aat run plan --seed Nand MCPexecute_plan'sseedrepeat those choices, andaat run batch --seed Nnow fixes each run's picks as well as the shuffle. A step's picks depend on its ID, not on the order steps run in, so parallel steps replay too, and a step's random named selections draw in name order, so several of them replay as well. A--shufflewithout--seedpicks one seed for the order and the runs and prints it, and a batch reports each run's seed in its--jsonoutput andbatch.json.--seed 0picks a new seed inrun planas inrun batch.{{uuid}}and{{random N}}stay unique. See Replaying a run's picks. - Defaults can draw from the domain file's value pools.
poolRef: airportCodes, orpoolRef: airportCodes.usfor one group, works whereverpooldoes: graph defaults, layers, and step values, withconstraintandpoolStrategy. One list then serves every input that takes that kind of value, where each default used to copy it.aat validatechecks every name, in the graph, its workflows'injectvalues, layers, plans, workflow files, and verification values; the archive and web UI record which pool a value came from, and MCPlist_value_poolsshows each pool's groups and the inputs that use it. See Value Pools. --fuzztries a step with generated values, inside a real flow.aat run plan smoke --fuzz addItemruns the plan, then sendsaddItemvalues its inputs allow, values they forbid, and values they say nothing about, each as a sibling step on its own copy of the steps before it. The cases come from each input's type and constraints and the domain file's types and pools, so nothing about fuzzing goes in the graph; when the node has an OpenAPI operation, a request the spec refuses is judged as negative. A 5xx, no response, or a response that breaks the spec fails the run (--fuzz-failchanges that); a forbidden value accepted or an allowed one refused is a warning. Fuzz steps never stop the run. Case IDs such asquantity.above-maxare stable, so--fuzz-casereplays one, and the archive,summary.json,--json, andaat run showrecord each case and its finding.run batchtakes the same flags. See Fuzzing.- Fuzzing leaves fields out and fuzzes what the template writes itself. Each input the template sends in the
body, the query, or a header gets
missing(andnullin the body), judged as negative for a required input and positive for an optional one. The template's own body values getremove,null,wrong-type, andempty, the body gets an unknown property, and literal query parameters getremove, all as edge cases unless the OpenAPI spec refuses them. They patch the request the template built, so the rest of it is what the plan sends. A new finding,undocumented-status, marks a status the node's operation doesn't list. A case's copy of the setup now includes the earlier steps that build on it, such as theaddItema checkout needs, and a copied step that fails reports its case asnot-sentwithout stopping the run. The shop example'squantityinput now declaresmin: 1. - Fuzz cases share their setup while the API refuses them. The new default scope,
reuse, runs a target's cases on one copy of the steps it depends on until a case is accepted, fails with a 5xx, or gets no response. Then the next case gets a fresh copy. Refused cases, most of a run, cost one setup, and accepted ones never pile onto one resource (a cart, a reservation with a traveler limit). Read-only setup steps (a GET with no cleanup pairing) aren't copied at all. After three setups in a row fail, the target's remaining cases aren't tried. Copies' progress lines are hidden unless they fail. The fuzz summary says how cases were set up (setup: 2 fresh, 7 reused), andsharedscope on a step that changes state is warned about.isolatedkeeps a fresh copy per case. On the shop, fuzzingaddItemandcheckoutsends 143 requests where it sent 228; on Duffel's test API, ten booking cases took 73 seconds with three searches instead of ten. Fuzz steps and their copies are named likeaddItem__fuzz_quantity_below_min, so a copied expression such as{{search.firstSliceOrigin}}still parses. - Fuzz cases run after the happy path, and a used-up setup is rebuilt. Cases on copies of their target's setup
run once the plan's own steps and its verification are done, so a long run of cases no longer holds up the plan
until a session it made expires. They are still sent when a later step of the plan fails, and before a
--stop-aftercheckpoint stops the run;sharedcases still run right after their target. AnerrorDetectionrule's newstale: truesays its error means the state the request worked on is used up (an expired session, a reservation an earlier refused case filled), not that the request was wrong; an HTTP 410 says the same. A case that gets such an answer on a reused setup is sent again, once, on a fresh copy and judged on that answer. The fuzz summary counts it asrebuilt, and the archive and--jsonkeep the answer it replaced asfuzz.stale. See When cases run and Stale Errors. - A step's
fuzz:block fuzzes it on every run, and--fuzz-savekeeps findings as regression plans. The block holds what--fuzzflags would say (mode,inputs,cases,only,scope,fail) plusskip(inputs never to fuzz),accept(statuses no case is faulted for, such as a busy API's 409), andpinnedcases sent exactly as written.--fuzz-save DIRwrites a plan per failing case with that case pinned, so it fails until the API is fixed;--no-fuzzignores the blocks. The MCP toolgenerate_fuzz_caseslists a step's cases as a block to keep. See Fuzzing: the fuzz block. - Fuzz verdicts hold up on real APIs and templates. A 429 (or
RESOURCE_EXHAUSTED) is a new finding,throttled, rather than a refusal, and a case retries it when its target'sretryblock would. A request the client refuses before sending, such as a header value with a control character, isnot-sent, not a server error. Findings are tried most serious first, soundocumented-statusno longer hidesaccepted-invalid. A spec violation the target's own request has too doesn't make a case negative, and a positive value the step'sconstraintrules out is judged as edge. Cases the request can't carry as themselves aren't made:""where the template leaves an empty input out, a value a header can't hold, and length cases past 65,536 characters; positive length cases match the input's pattern.--fuzz <node>skips steps withexpectFailureor arawBody;--fuzz-inputnames inputs of any target;--fuzz-caseand a block'sonlyare checked before the cap; pinned cases follow the mode, input, skip, and cap settings; and a batch fails when a--fuzzname matches in none of its plans. Fuzz copies and cases never stand in for the happy path's resources in verification and cleanup, aknownIssuecovers a setup copy as it covers its original,--stop-afterkeeps a fuzz failure, andaat run showand MCP failure analysis judge a fuzz case by its finding.--fuzz-savekeeps the case'saccept,scope, andfail, so the saved plan passes once the API is fixed; a run prints the plans it saved with--quietand in a batch too, and--jsonlists them infuzz_saved. Awrong-typevalue carries JSON quotes only where the template writes the input unquoted in its body. A setup copy leaves out steps that change nothing the target works on (reads and negative steps it doesn't depend on), and a read-only target's cases run on the happy path's own setup underreuse. The step counter numbers the plan's steps and cases, not their setup copies, and a fuzz case's value is archived fewer times over (a tenth smaller on the shop). See Fuzzing. raw: truesends a step value exactly as written, with no expression evaluation or type coercion. Its{{…}}is text that validation doesn't check, and an unquoted YAML date is sent as written, not as a timestamp. See Raw Values.- An error a 200's body reports can stand for the status it means. An
errorDetectionrule takesstatusandcategories(the category itsdetails.categoryreads, matched without case, to an HTTP status), and the graph's newerrorStatusgives every rule that names none the same mapping, so a project whose errors all look alike declares it once. That status is whatexpectFailureand mutations'expectStatus, status assertions, retries, and fuzz judging read: a validation error given 400 passesexpectFailure: {status: [4xx]}, a temporary one given 503 is retried by default, and in a fuzz run an unexpected one given 500 is aserver-errorrather than a refusal. Without a status, a body error behaves as before. See The Status an Error Stands For. expectFailuretakes status classes.status: [4xx]passes on any refusal and still fails on a 5xx or a success; it works in mutationexpectStatusand overlayexpectFailuretoo, and a gRPC status matches the class of the HTTP status it maps to. See Negative Testing.
Fixed¶
- An error a 200's body reports is read before the step's outputs. Such a body rarely has the shape a success's
outputs are read from, so a step whose
errorDetectionrule matched used to fail as "extract path … not found": an adapter error that hid the API's message and thatretry.on: [response_error]never retried. The step now fails with what the body says. - Body errors are reported everywhere. The
--jsonsummary used to report a step whose body held an error as passed; it now fails it, with abody_errorobject. The progress line marks it (BODY ERROR VALIDATION as 400) and prints the message, andaat run show --stepshows it.
Changed¶
- Steps run in the order the plan lists them, wherever
dependsOnallows. Of the steps ready to run, the one written first now goes next; before, a step that became ready went to the back of the queue, so a plan could run its steps in an order it never wrote. See Step Execution Order.
0.3.2 - 2026-09-22¶
A bug-fix release: a gRPC call that outlives its deadline is now always an error, never a response the step asserts on. The docs also lead with what one description of an API does, and say how AAT differs from tools that script the wiring between calls.
Fixed¶
- A gRPC call that outlives its deadline is always an error. The server is sent the call's deadline and answers
DEADLINE_EXCEEDEDwhen it passes. When that answer arrived a moment before the client's own timer fired, the step treated it as a response: its assertions ran against it, and the archive recorded an exchange that never finished. This happened with the run's own deadline (such as the time budget an aborted run gives its cleanup) and with aat's request timeout. The clock now decides, not the timer, so both are errors, as they are over HTTP. ADEADLINE_EXCEEDEDthe server reports on its own account is still a response.
Changed¶
- The README and the docs home lead with four jobs one description does. They are: testing real flows over REST and gRPC; testing locally without editing anything; knowing when an API you depend on changes; and getting an integration working before handing it to an agent. The Shippo regression a nightly run caught, and the clean-room builds from the launch post, now sit beside the real-API projects as proof.
- CI/CD gains scheduled runs against a
provider: a cron trigger, archive retention, and
knownIssuefor a defect the provider has confirmed. - Why AAT exists says how AAT differs from scripted tools. Bruno, Karate, and Hurl keep the wiring between calls inside each test, and nothing checks it until the requests run. AAT declares it on the operations and checks it first: guardrails at the interface between agents and APIs.
0.3.1 - 2026-09-22¶
A small release with one new primitive: a failure you already understand can stop turning CI red, but only until a date you name.
Added¶
knownIssue: a failure with a deadline. A step — or a whole plan — can declare a defect it is waiting on:
- id: refund
node: createRefund
knownIssue:
until: 2026-10-06
reason: >-
Vendor test-mode defect, confirmed: refunds answer 201 with status ERROR
where they answered PENDING. Live is unaffected.
url: https://example.com/tickets/4471
Until that date the failure does not turn the run red. Everything else stays honest: the step still runs, still
checks its assertions, and still reads as failed in the progress line, in aat run show, in the archive, in
the MCP tools, and in the web UI. Only the run's outcome is forgiving, so the exit code is 0 and CI is green on
a fact somebody wrote down. Execution carries on to the next step when the failing step produced a response, so
the rest of the plan keeps testing; when it did not — a 500, an unexpected status, an error in the body — there
is nothing to carry on from, the run ends there, and it says endedEarly rather than passing off a short run as
a complete one.
Then the date passes and the entry stops applying, with the expiry named in the failure:
step "refund" failed mechanical validation (knownIssue expired 2026-10-06). That is the whole point of it.
Turning a test off is easy; this is the version that turns itself back on, so a suppression cannot quietly
outlive the problem it was written for.
Both fields are required. An entry with no reason is one nobody can judge later, and an entry with no until
is the thing this feature exists to prevent. A transport error is never covered: a step that could not reach the
API at all is infrastructure, not a defect with a date. It is not a way to quiet a flaky step — that is a retry
rule — and not expectFailure, which says the API should refuse the call.
A step that passes while an entry is in force is reported under knownIssuesResolved: the entry has outlived
the defect and should be deleted. That never fails a run, so good news never breaks a build.
aat validate gains a Known issues section listing every entry in the project with how long it has left, or
how long it has been lapsed. It never changes the exit code — whether a covered failure still counts is a
question for the run.
--json gains known_issues, known_issues_resolved and ended_early; the archive gains knownIssues,
knownIssuesResolved, result.knownIssues and result.endedEarly, and a step gains knownIssue.
tools/aat-to-junit.py reports a covered step as <skipped> rather than <failure>. Documented in
Plans, and in the AI assistant
primer as the fifth negative-testing primitive.
Changed¶
- "Why AAT exists" tells the actual origin story. The page opened on the conclusion — a pile of Postman
collections — and skipped how it got there. It now opens on the problem that produced them, a service that could
not be tested on its own behind a dozen setup calls, and adds the two attempts that failed before the graph
existed: a recording proxy that discovered the chained values on its own and generated a Postman collection with
the extraction scripts written, which worked but baked the wiring into the recording so any other path to the same
goal meant recording again; and handing an LLM a list of operations and a goal, which failed for the mirror-image
reason, with nothing written down to guess from. Both kept the connections attached to a run rather than to the
operations, which is the move the graph makes. A new section walks
the features in the order the problems arrived — local overrides, archives and the web UI, layers, then the MCP
server — and a new section makes the case the page never made outright: one description does several jobs. The
batch a developer runs while changing something is the batch CI runs on every commit and the batch a release is
signed off with; debugging a local build is that same run with one node redirected rather than a fork of the suite;
what a pipeline leaves behind is an archive that opens in the web UI instead of scrollback; and with one
description instead of a copy per person, a correction lands once and holds for everyone who runs it next. The page
also now says why a described API is a Rosetta stone an assistant writes a client from, that
aat validate --strictis the guardrail that closes an agent's loop, and thataat prompt, the surviving piece of the second dead end, is the least load-bearing thing in the project. The README'sWhyand the docs home carry the short version, as before. - The docs home lists four real-API projects rather than three;
aat-qdranthad been missing from that sentence.
Added¶
- A case study, What a nightly run caught.
aat-shippo's scheduled run went red on 2026-09-18 because Shippo's test environment began answeringPOST /refundswith201 Createdand"status": "ERROR", where it had answeredPENDINGthe day before. The page is the diagnosis end to end — the failing batch downloaded as one file,aat run show --step refundnaming the predicate that failed, the same command against the last passing run four days earlier, and the two checks that ruled out a label-state race and an intermediate status — followed by what the vendor's own documentation says and what the OpenAPI spec could not say, becauseERRORis a legal value in it. Shippo confirmed it as a known test-environment defect; live refunds are unaffected. Linked from the Why page and Real APIs. - A blog post for the gRPC release, "gRPC, without a second toolchain":
the cross-protocol demo run, how a value crosses the boundary between an HTTP step and a gRPC one, status names
against a shared HTTP status, what
aat validatecatches offline, proto3's 64-bit integers, and where the support was stress-tested.
0.3.0 - 2026-09-18¶
The gRPC release, and the one aat-qdrant was built on; it needs this version or later.
Highlights. AAT drives gRPC services the way it drives REST ones: a node names a method with proto:, a template
declares protocol: grpc with an rpc, metadata, and a message written as JSON, and an environment routes to a
grpc:// or grpcs:// target, so one plan can check an order out over HTTP and charge it over gRPC. Protobuf reaches
the rest of AAT as JSON, so extraction, predicates, assertions, retries, cleanup, archives, aat run show, the web
UI, and the MCP server all work unchanged. Statuses are asserted by name (expect: OK, status: [NOT_FOUND]), in
plans, overrides, overlays, and retry rules, because several gRPC statuses share one HTTP status. aat validate
checks every gRPC node against a descriptor set offline: unknown methods, streaming methods, inputs where the message
puts them, and extract paths, including one spelled by its .proto name. It is tested over TLS and mutual TLS, runs
unary methods only, and ships with an offline demo (aat-sandbox serves payments over gRPC;
aat-sandbox init --example grpc-payments), the gRPC guide, and a fourth
public project that drives all 52 of Qdrant's unary methods with it.
Breaking and noticeable. fieldEquals compares a list or an object by its structure, so ["4"] no longer equals
[4], and <, >, <=, and >= compare a decimal string with a number by value. --override NODE=URL keeps the
headers and credential the node already had, from its matching overrides: entry, where it used to take the
environment's. aat-sandbox serve listens on a third port, :8767 (--grpc-port). aat docs generate and the MCP
node tools print a list or object default as JSON, and aat docs generate orders a requirement's satisfiers by name.
Ctrl-C ends aat web at once. The binary grows from about 28 MB to about 44 MB with the gRPC and protobuf libraries.
Added¶
- Outputs echoed from inputs. A node output can declare
fromInput: <input>to be that input as the step sent it instead of something the template extracts. It is for an API where the client names what it creates and the reply doesn't say it back — Qdrant's create-collection answers{"result": true}— so a later step and the node's cleanup can read the name like any other output.aat validatechecks that the input exists and that the template doesn't also extract the output. - gRPC, for unary methods. A graph node names a gRPC method with
proto:, a template declaresprotocol: grpcand describes the call withrpc:,metadata:, andmessage:, and an environment routes to it with agrpc://orgrpcs://target. Descriptors come from a descriptor set — whatprotoc --descriptor_set_outandbuf build -owrite — named byproto:inaat-project.yamlor the graph.
Messages cross into the rest of AAT as JSON, so extract rules, predicates, assertions, error
detection, archives, and aat run show work exactly as they do for HTTP; metadata: carries what
headers carry, credentials included, so oauth2, apikey, and bearer need no new configuration.
expectFailure (a step's, or an override's or overlay's), expectStatus, a status assertion, and
retry.on/retry.failOn accept gRPC status names —
expect: OK, status: [NOT_FOUND] — which match more precisely than a code can, because several
gRPC statuses share one HTTP status. Numbers still work, so a plan can read against either
protocol. aat validate checks gRPC nodes against the
descriptors offline, catching an unknown or misspelled method, a streaming method, and an extract
path written with a field's proto name when the response encodes it under its JSON name. An input
is checked where the template's message: places it, however deep — vectorsConfig.params.size,
a oneof member, a map value — rather than required to name a top-level field. It reads
an extract path the way extraction does — an escaped dot stays in its key, a #(...) query is one
step, a map field's next step is any key, a well-known type is walked by its JSON form (a
Timestamp is a string, a Struct any JSON), and a modifier or a pipe ends what it checks — and
a path that reads nothing says where it went wrong: a list read without an index, or a scalar
read into. A field spelled by its proto name is caught at any depth, with the path to read
instead.
Streaming methods are rejected: a step is one request and one response. Server-streaming may come later; bidirectional will not.
Every surface reads a gRPC run in its own terms. The web UI shows the method and the service it
went to in place of a verb and a URL, names the status code the server sent rather than the HTTP
status AAT maps it to, labels request and response metadata as metadata, gives trailers a pane of
their own, and offers Copy as grpcurl in place of Copy as cURL. aat run show, the run
progress lines, and the MCP archive tools do the same, and the MCP template and node tools describe
a gRPC operation by its service, method, metadata, message, and the inputs the message and
metadata need; aat docs generate names each node's method. aat validate reports an optional input with no default that a message or
metadata always sends, as it does for a body, rather than leaving it to fail at run time. aat generate does not understand
gRPC yet.
This adds google.golang.org/grpc and google.golang.org/protobuf, which take a go install
build from about 28 MB to about 44 MB.
- A gRPC demo, offline.
aat-sandbox servenow servesshop.v1.Paymentson:8767beside the HTTP shop (:8765) and HTTP payments (:8766), against the same orders. The newexamples/grpc-paymentsproject runs one plan across both protocols: a cart opened and checked out over HTTP, then charged and refunded over gRPC, with the order id crossing the boundary untouched.make example-grpcruns it, and CI does too, so nothing about gRPC is unexercised.
The service is a façade over the payments HTTP handler rather than a second implementation, so
the order state machine, the declined card, and the region's currency rules behave identically
whichever protocol reached them. It is served dynamically from the same descriptor set the
project reads, so the two cannot describe different APIs. make proto regenerates it.
aat-sandbox init --example grpc-payments <dir> extracts the project from the sandbox binary, so an
installed aat-sandbox runs the gRPC demo with no checkout of the repository; init with no flag
still extracts the shop.
-
A fourth public project, aat-qdrant, is listed in the README, on the Examples, Real APIs, and Why pages, and in the AI assistant primer as the reference for gRPC idioms: all 52 of Qdrant's public unary gRPC methods, against a pinned local container. It is what the gRPC support was stress-tested against.
-
docs/user/grpc.md, a guide covering descriptor sets, the template and graph shape, routing and auth, status names, and the proto3 JSON encoding rules most likely to surprise a plan author — starting with 64-bit integers, which encode as strings. The AI assistant primer gained the same ground so agents author gRPC plans correctly.
Fixed¶
- A predicate orders a number sent as a string against a number.
size > 0failed with "cannot compare string with float64" whensizewas"147456"— which is how protobuf's 64-bit integers, and many APIs' amounts, arrive — although the gRPC guide promised it worked.<,>,<=, and>=now compare a decimal string with a number by value.==and!=stay strict, and the error says to quote the number. aat docs generatewrites the same file every time. Where several nodes satisfied one requirement, the diagram listed their arrows in map order, so regenerating an unchanged graph could reorder lines and fail a "docs are current" check. A requirement's satisfiers are now in name order, for the diagram and for backward chaining alike.- A list or an object default reads as JSON. The MCP node tools and
aat docs generateprinted an input's default with Go's%v, so a list of objects came out as[map[id:map[num:1]]], unreadable, and with the quotes gone that made"1"a string, which an assistant copying it would get wrong. They now print JSON; generated docs shorten a long list to its first element and a count. fieldEqualscompares a list or an object by its structure. Anything but a scalar used to be compared by its printed form, so{population: 3645000}failed against the JSON{"population": 3645000}(a float prints as3.645e+06), and["4"]passed against[4]. Lists and objects now match element by element and key by key, numbers by value and strings only as strings, at any depth; a failure shows both sides as JSON.errorDetectionrules compare the same way.- gRPC rough edges, found by reviewing the feature against its own docs. A descriptor set named
by
proto:inaat-project.yaml— the form the guide teaches first — was parsed and read by nothing, so every gRPC node went unvalidated and the run then failed telling the user to do what they had already done. It works now, foraat validate,aat run,aat promptand the MCP server alike, and a project that names a descriptor set its nodes do not use, or uses descriptor sets it does not name, is told so rather than passed over in silence.
A node's proto: is now checked against its template. Pointing a gRPC node at an HTTP template,
the natural mistake of forgetting protocol: grpc, used to pass aat validate --strict and fail
three steps into a run, after a cart and an order already existed; it fails validation, and fails
a run before its first step. A node whose proto: and template rpc: name different methods is
caught too — validation was checking one method while the wire called another.
A gRPC call had no deadline at all, so a server that accepted and never replied held the step for
as long as the run lasted; it now gives up after the same 30 seconds an HTTP request does, and
says so in the same words. pathRewrite beside a grpc:// baseUrl is rejected, as the
documentation always said it was. A status assertion written as a gRPC status name can no longer
contradict expectFailure unnoticed.
A gRPC target with a path after its port — grpc://localhost:8767/shop.v1.Payments, the natural thing
to write — passed every check and then failed the step as UNAVAILABLE; only a target with no port was
checked for a path. And grpc://localhost, with no port, dialled 443 in plaintext. Both are now
errors that say what is wrong, from aat validate as well as from a run; grpcs://host means 443.
A gRPC step interrupted with Ctrl-C, or cut off by the time an aborted run allows its cleanup, was
recorded as a CANCELLED or DEADLINE_EXCEEDED response: a client error with a body, which the
step's assertions then ran against. It is an error on the step, as it is over HTTP. The details a
server attaches to a failure — google.rpc.BadRequest and its field violations, ErrorInfo,
RetryInfo — were archived as a bare type URL unless the project's descriptor set happened to include
error_details.proto; they are now always read. And a reply over 4 MiB, gRPC's default limit, failed
as RESOURCE_EXHAUSTED and was retried as transient; a reply is now read whatever its size.
The MCP server's tool and resource descriptions — the text an assistant chooses a tool by — still
called every template an HTTP template with a method and a path, though the tools themselves had
learned gRPC. They now say what the tools return for either protocol. aat://api/overview listed a
gRPC node with no method at all, and get_oas_operation told an assistant that a gRPC node was
missing its OAS reference; the overview names the method, and the tool says what the node is.
Copy as grpcurl produced a command that failed against any server without reflection, the
sandbox included, with nothing to say why; it now opens with a comment naming the -protoset flag
to add.
No call had ever been made over TLS: grpcs://, a private CA, mutual TLS, serverName, and
insecureSkipVerify were tested only as far as building their settings. Each is now tested against
a server that needs it, from env.yaml to the handshake, and a run against a public grpcs://
endpoint verified the system roots. Doing it found that a failed handshake came back as
UNAVAILABLE, which is transient and so retried, though an untrusted certificate fails the same way
every time; it is now an error on the step that says what TLS objected to and names the grpc.tls
settings. Binary metadata, a key ending in -bin, was encoded twice on the way out and written into
archives as bytes JSON cannot hold; a template now writes it as base64, as grpcurl -H takes it, and
an archive records it the same way.
The demo's gRPC replies carried an empty payment id. The HTTP payments API calls it paymentId and
payments.proto called it id, and the sandbox's gRPC service discarded the field it could not
place, so examples/grpc-payments extracted "" and nothing asserted otherwise; a refund came back
as a half-empty Payment, and an amount of 0 refunded the whole order. The proto now mirrors the
HTTP API field for field — a refund is a RefundRecord with its own id and the payment's — a field
the two disagree on fails the call instead of vanishing, and the example and make example-grpc
assert the id.
The web UI shows the gRPC status a plan wrote rather than the HTTP status it maps to — several
gRPC statuses share one, so the number could not say which — and so do the CLI's failure summary
and the MCP archive tools. Copy as grpcurl always emits -d, without which grpcurl reads the
message from stdin and the pasted command appears to hang. The progress lines keep their columns
aligned when a step reports a status name instead of three digits. gRPC metadata is archived with
the lowercase keys the wire uses, so a key grepped for in an archive is the key the server sent.
- Ctrl-C ends
aat webat once, and without an error. A browser keeps spare connections open to a server it is showing, and Go will not close a connection that has sent nothing until it is five seconds old;aat weballowed its shutdown five seconds. So stopping it with a browser tab open could wait out the whole of that and then exit withcontext deadline exceeded. A connection that never sent a request is now closed when the shutdown starts. It also made one of AAT's own tests fail intermittently, which is how it was found. --override NODE=URLkeeps the node's own credential. The flag took the environment's top-level auth even when anoverrides:entry gave the node another, so--override paymentCharge=URLsent the shop's bearer token to the payments host in place of its API key, and the call failed as unauthenticated. A flag now changes the URL and nothing else: the node keeps the headers, the credential, and thepathRewriteof the entry that matches it, or the environment's when none does.- The Homebrew cask clears the macOS quarantine attribute with a declarative
postflight_stepsstanza, sobrewno longer warns thatpostflightis deprecated and asks users to report it to the tap. The published cask ingburgyan/homebrew-tapwas updated in place, so the warning is gone without waiting for the next release.
0.2.0 - 2026-09-16¶
The public launch release, and the one the three real-API projects (aat-duffel, aat-stripe, and aat-shippo) were built on; they need this version or later.
Highlights. Form-encoded request bodies end to end, including Stripe-style bracketed keys, validated against the
spec and shown as fields in aat run show, the web UI, and MCP. aat run show reads a run or a whole batch from the
terminal: the steps, or one step's request, response, inputs, outputs, resolutions, or response shape. repeat polls
and pages with until and next, with request pacing (minRequestInterval), Retry-After, and a retry ceiling.
Values and assertions reach across steps: {{step.output}} in predicates, offsets on references, uuid, random,
now, lists and maps as step values, and header extraction. Cleanup chains, when, releasedBy, and guard plans.
OpenAPI specs that used to be rejected (circular references, 3.1 type lists, allOf/oneOf/anyOf bodies) now load,
validate, and scaffold. Credentials are redacted from --dump-state by default and from every archive. Validation and
composition are stricter: literal value shapes, leftover AUTOWIRE markers, unknown assertion types, and bad
expressions fail before a request is sent. The MCP server keeps parity with the CLI. The docs gain
Why AAT exists, Real APIs,
and the AI assistant primer as llms-full.txt.
Breaking and noticeable. aat run plan --dump-state redacts credentials; --dump-state-secrets keeps them.
aat run show --step --json names a step's assertion results validation, not assertions; summary.json records
OpenAPI results under oas; --resolutions reports a graph default's literal as graph_default, and plan_default
now means a value the plan sets. A node's second cleanup step is deleteCart_2 in archives and --json output. Exit
code 2 for aat generate over existing files (use --force), for --oas-validate strict when a spec fails to load,
and for a plan that ran as several runs. A list in a URL or form body is sent as repeated pairs or a comma-joined
value, not as JSON text, and a form-encoded body: | no longer sends its final newline. <, >, <=, and >=
compare two decimal strings as numbers; uuid, now, and unixtime are reserved words in expressions. A registered
cleanup is skipped when a later main step already released the resource.
Added¶
- An
apikeyauth can write a scheme of its own before the key withvaluePrefix:headerName: AuthorizationwithvaluePrefix: "ShippoToken "sendsAuthorization: ShippoToken <key>, so an API that defines its own scheme doesn't force the scheme into the secret. The prefix is written exactly as given, takes${var}substitution, and is inherited throughextends. It is refused on any other auth type, andbeareris told that it already sendsBearerbefore its token. - An expression can put an offset on a reference, and a step value can read an earlier step's output:
amountToCapture: "{{authorize.amount - 500}}","{{order.totalAmount - 10.50}}"on a decimal written as text, and"{{clock.frozenTime + 32 days}}"on Unix seconds. A number offset keeps whole numbers whole and decimal text at its places, and a time offset moves Unix seconds by its unit or a date by whole days. Assertions,repeat.until, and selection filters take the same offsets, and a value's reference impliesdependsOnand is checked byaat validate. - A form-encoded body reads as what was sent, rather than as one escaped string.
aat run show --requestprints its decoded fields, onename=valueper line;--compactprints the body as it went over the wire; and--pathand--shaperead the value its bracketed keys describe, so--path metadata.sourceselects frommetadata[source]=aat-stripe, and a key ending in[]or sent twice reads as an array. - The step view notes the body as
91 bytes (form, 5 fields), and its--jsonaddsrequest_body_form. - The web UI's Request and Response tabs show a form body as its fields, and the API adds
formFieldsto each. - The MCP server's
inspect_archiveprints form bodies as fields too. aat run show --step IDlists a repeated step's requests: each one's status, time, and whetheruntilheld, the inputs it sent that differ from the step's when it pages, and its outputs.--iteration Nshows one of them, and with--request,--response,--inputs,--outputs,--path, or--shapeprints that request's part.--jsongives the list as the step'siterations, and--iteration N --jsongives the request.- The web UI's step detail has a Requests tab. Selecting a request loads its bodies, headers, inputs, outputs, and
OpenAPI validation from
GET /api/runs/{id}/steps/{stepId}/iterations/{index}. - The MCP server's
inspect_archivelists each repeated step's requests. - A selection
filtercan read an earlier step's output, as an assertion can:filter: 'objectId == "{{create.customerId}}"'picks the element about the object an earlier step made. The reference impliesdependsOn,aat validatechecks it, and the step's selection record shows the filter with the value it compared. - A
fieldAbsentassertion passes when its path is missing or null, the counterpart offieldExists. A plan can assert that a response leaves a field out, such as an error body without acode, which a predicate can't name. - An assertion can compare with an earlier step's output, written
{{step.output}}:amount == "{{checkout.total}}"in a predicate,value: "{{checkout.total}}"infieldEquals, and a quoted literal inrepeat.until. - A main step reads the main steps before it, and a verification step reads the main steps. A reference implies
dependsOn, and addon composition and isolated mutation clones rename the steps it names. - An extracted number compares as a number. A null or list output, a step that stored no outputs, or an output the response left out fails the assertion, with a message saying which.
aat validatereports a reference to an unknown step or output, to the step's own output, to a list output, or to a step that expects failure, and one in a step value, wherefrom:reads an earlier step.- The shop's full-lifecycle plan checks that checkout keeps the cart's subtotal and that the refund and the returned order match checkout's total.
- A step can read every page of a listing with
repeat.next, which sends each response's cursor as the next request's input, as inrepeat: {next: {after: nextCursor}, collect: [orders, orderCount], max: 50}. - The step passes when every cursor comes back missing,
null, or"", anduntilbecomes optional. It fails whenmaxortimeoutcomes with a cursor left, so an audit can't pass on a listing that was cut off, and when a response gives cursors an earlier request already sent. - Pages are requested without a wait unless
intervalis set, and a retry resends that page's cursor. - The step's inputs are its first page's. Each request under the archive's
iterationsrecords the inputs it sent, andrepeatStopcan beexhaustedorloop. aat validatechecks thatnextmaps the node's inputs to its string or integer outputs.- The shop sandbox lists orders a page at a time with
GET /orders(limit,after,customerEmail), which the shop graph reads aslistOrders. The state-machine plan's verification pages through its customer's orders and asserts that none is left open. - A visualizer can match a nested field with
match.bodyPath, a gjson path that must reach a value other thannull, such asdata.receipt_number. It tells apart the responses of an API that wraps every body in the same envelope, wherebodyContainssees only the shared top-level key. - A step can repeat its request until a condition over the response holds, as when polling a background job:
repeat: {until: 'status == "complete"', interval: 2s, max: 30, timeout: 2m}. - Every request sends the inputs resolved for the first, and each is retried under the step's
retry:block. A response'sRetry-Afterlengthens the wait, up to 60 s. collectgathers outputs across the responses: a list's items are appended, and an integer or float is added. The step's assertions run once, on the last response's outputs with the collected ones.- Reaching
max(default 50, at most 1000) ortimeoutbeforeuntilholds fails the step with arepeatassertion result. aat validatechecks thatuntilreads the node's outputs, thatcollectnames lists or numbers, and that the step isn't anexpectFailurestep or on a node with a cleanup pairing.- The archive records each request under
iterationsand why the step stopped underrepeatStop. OpenAPI validation counts every request. The progress line andaat run showgive the number of requests, and the MCP server's step details do too. - An extract rule can read a response header, as in
requestId: {header: X-Request-Id}, and Lua transforms getheader(name). - Names match in any case, and a header sent more than once gives its values joined with
,. - A header value is converted to its graph output's
integer,float, orbooleantype, so a predicate such asrateLimitRemaining > 5compares numbers. optional:anddefault:work as for a path. A template whose rules all read headers needs no JSON body, so a204 No Contentstill gives outputs.- The MCP server shows a header rule as
header X-Request-Idwhere it shows extract paths. - The shop sandbox sends
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Reseton its API responses. It counts each bearer token's requests per minute and never refuses one. The shop example reads the first two onlistProducts, and Quick Purchase asserts that its request was counted. - An extract rule can take
default:, the output's value when the path is missing or holdsnull, as innextCursor: {path: meta.after, default: ""}. A rule takesdefault:oroptional: true, not both, and a rule withfieldstakes a list default.aat validatereports a default whose shape doesn't fit the output's type. - The
not found in responseerror names both remedies. - The template docs and the AI assistant primer show gjson counts and queries in extract paths. They include
==~nullfor a null or missing field, since==nullcompares a string and never matches a JSONnull. - A step value can be a YAML list, which is the list itself, as it already was in a slot's
inject:skus: [SKU-1004, SKU-1006], or a list of maps for a template's{{#lineItems}}…{{.sku}}…{{/lineItems}}. Before, a bare list was an error, and a list had to be written{default: [...]}, which still works. {value: …}in a step value means the same as{default: …}, so the literal form of graph defaults, layers, andinjectworks in a plan too. A step value with both keys is an error.- Strings inside a list or map value are evaluated as expressions, at any depth, wherever expressions are evaluated. A new list or map is built each run, so the plan keeps its expressions.
aat validatechecks expression syntax inside lists and maps in step values, pools, andfieldEqualsvalues, and names the item, as ininvalid expression for "skus": item 1: random takes a length from 1 to 64.aat run show batch-ID/PLANnames a plan's run in a batch by the plan name the batch view's PLAN column shows, such asbatch-…/negative/state-machine. Before, onlybatch-ID/run-IDworked.- A run ID of the batch wins over a plan of the same name, and a permutation skipped as a duplicate doesn't count.
- A plan that ran as several runs, such as its layer permutations, exits with code 2 and lists each run as
batch-ID/run-IDwith its layers. - The MCP server's run lookups accept the same, in
get_sample_response,inspect_archive,analyze_failure, anddiff_archives. summary.jsonrecords a run's OpenAPI validation underoaswhenever the graph references a spec, a clean run included:mode,validatedRequestsandvalidatedResponses(cleanup steps included), andviolations. Before, only violations were recorded, inissues.- The archive records the mode as
metadata.oasValidation. aat run showprints anoas:line under a run's header, and a batch's totals across its runs under its totals line.--jsonprints them asoas.- The web server's run list passes
oason. - A predicate assertion that names an output the step didn't produce says so, as in
unknown field "trackingNumber": the step produced no output "trackingNumber", since an optional output the response didn't hold is absent. aat run show <batch-id>, or a path to a batch directory or itsbatch.json, shows the batch. Before, a batch reference was an error.- It shows the totals, and one row per permutation with its layers, outcome, and step counts.
- It shows what cleanup did across the runs: per cleanup node, the steps that ran and failed, and the pairings skipped
as released or by
when, plus each failed cleanup step. --jsonprints the same.- Without
--step, a part flag or--pathprints that part of every step that has it, cleanup steps included, as inaat run show latest --response --path error.code. It prints one line each, or a JSON array with--json. - A predicate assertion's message shows the expressions in its literals expanded, as in
predicate "amountReceived == 1000" is true. The result'sexprkeeps the predicate as written. - Under
--oas-validate, cleanup exchanges are checked against the spec and recorded inoasValidation. Understrict, an invalid one fails the cleanup step, and its cleanup chain still runs. - Form bodies can be written as
form:, a mapping of field names to values, in place of abody:query string. - The request is sent as
application/x-www-form-urlencoded, and every key and value is URL-encoded, keeping the brackets of a key. - A field whose whole value is one placeholder is left out when that input has no value (absent, null,
"", or an empty list or map), so optional fields need no conditional blocks. - Nested mappings write bracketed keys:
metadata: {source: web}sendsmetadata[source]=web. An input whose value is a map does the same, a list repeats its key as written (tags[]), and a map in a list writesitems[0][sku]. - Other text is rendered as one value. Iteration blocks, placeholders in field names, duplicate fields, and fields that nesting would send twice are errors when the template loads.
- A credential or overlay header that changes the Content-Type fails the request.
- The MCP server's template view shows the form.
aat validatematches an input that is the whole value of aform:field to that field, so a graph can name its inputs its own way:order_id: "{{orderId}}"counts as the spec'sorder_id, in both the unknown-input and the required-field checks.aat validatematches path and query parameters the same way:/orders/{{orderId}}fills the spec's/orders/{order}, andstatus={{orderStatus}}counts asstatus, inside a conditional block too. Paths line up from their last segment, and a segment with other text around its placeholder still needs the spec's name.aat validateandaat runreport a header orform:field whose whole value names no input of the node, which would never be sent.- Verification steps take
values, as main steps do, to read a particular step:values: {orderId: {from: firstCheckout.orderId}}. A reference names a main step, andaat validatechecks the input names, the references, and the values' shapes. --resolutionsandaat run show --stepname the layer that set a value. A layer's literal value readslayer, and the record carrieslayer: shipping-expressfor any value a layer set. The MCP server'sinspect_archiveshows it too.settings.minRequestIntervalpaces requests for APIs with rate limits: the starts of any two requests are at least that far apart, such as250ms. One interval covers everything a command sends, including the plans of a parallel batch, retries, verification, and cleanup.aat promptand the MCP server'sexecute_planhonor it too. A value without a unit, such as250, is rejected when the environment file loads.- Step retries honor the server's
Retry-Afterheader, and on a 429 without one,RateLimit-Reset. A retry waits at least as long as the failed response asks, in seconds or until an HTTP date, instead of only the backoff. - Cleanup chains: a cleanup node can declare its own
cleanup. It runs right after that cleanup step succeeds and takes its outputs first, so a resource released in two calls (request, then confirm) is cleaned up completely. Chained outputs stay inside the chain. Archive cleanup records gaincleanupFor(cleanup_forin--json), naming the step each one cleans up after. - A cleanup pairing can say when it isn't needed:
cleanup: {node: voidPayment, when: 'status == "authorized"', releasedBy: [capturePayment]}. releasedBylists nodes whose success, after the creating step and with the same values for the inputs they share with the cleanup, releases the resource.whenis a predicate over the creating step's outputs, or the previous cleanup step's in a chain. The cleanup is skipped when it's false. Awhenthat can't be evaluated runs the cleanup and recordswhenError.- A skipped cleanup's chain doesn't run. Skips print as
skipped:lines undercleanup:, and are recorded in the archive'scleanupSkipped, incleanup_skippedin--json, in acleanup skipped:table inaat run show, and in the MCP server'sinspect_archive. - Loading a graph checks that
releasedBynames other nodes and thatwhenparses and names the node's outputs, soaat validate,aat run, and the MCP server all reject a bad pairing.aat docs generateadds When and Released By columns when a pairing uses them. - A
minormaxselection whose candidates tie prints a warning under its step, as inwarning: selection "cheapest": 3 of 8 elements from listProducts.products tie for min price at 19.99; picked index 0. - The warning is also in the step's
warningsin--json, inaat run show --step, and in the MCP server'sinspect_archive. - Archive selection records gain
field,sortField,sortValue,ties, andonTie. onTie: failon a selection fails the step on a tie instead, andonTie: firsttakes the first without a warning. Plan validation acceptsonTieonly onminandmax.aat run show --step ID --resolutionsprints how each of the step's inputs got its value, as JSON.- Each entry gives the source, the upstream step and output, the expression, pool pick, or constraint, and the selection that picked the value.
--steplists each input with its source, and--step --jsonincludesresolutions.- A step that fails before its request is sent keeps the resolutions made up to that point, with an
errorrecord for the input that failed. That covers an input that can't be resolved, a missing adapter, a request that can't be built, and a send error. Before, such a step recorded none. - The MCP server's
inspect_archiveshows each input's selection and error. aat run show --compactprints JSON on one line: a step part, with or without--path, and with--jsonthe step list, the step, or the shape.--compactwhere the output is text, such as--shapewithout--json, is an error.aat validateandaat runcheck that literal values fit their input's shape: a list for an array input, and no map for a single-value input. A list for a single-value input is still allowed, since a template can send it as repeated pairs.- It covers step values and pools, graph defaults, layers, and slot
injectvalues. - A step expected to fail, such as a mutation, isn't checked, since a negative test may send the wrong shape on purpose.
- A slot
injectvalue fails only when no input with its name takes it, since composition decides which steps it reaches. The composed plan's steps are then checked like any others. - A pool entry for an array input gets a hint: a bare list in a graph default or a layer is a pool, so write one list
as
{value: [...]}. - Expressions and custom types aren't checked.
- Expressions in assertions: a
fieldEqualsvalueand a quoted string in apredicateexprcan hold{{…}}expressions that name the step's inputs, such asvalue: "{{today + 3 days}}"orexpr: 'quantity == "{{quantity}}"'. - A quoted expression that yields a number or a boolean compares as one.
- An expression that can't be evaluated fails its assertion.
today,now, andunixtimeread the time the step's inputs were resolved, so a retry compares with what it resent.- Selection filters and cleanup
whenconditions stay literal. AUTOWIRE?in workflow templates marks an optional input that only some compositions feed. It is wired when a step produces the output, such as one an addon adds, and left unset otherwise.aat generatescaffolds HEAD, OPTIONS, and TRACE operations, form-encoded request bodies, and cookie parameters, which it sends as oneCookieheader. Body and response properties fromallOfbranches become inputs and outputs, and an OpenAPI 3.1 type list such as["null", integer]maps to its non-null type. The MCP server's operation search lists operations of every method too.aat run show <run>prints what an archive recorded, without a browser.- Without
--step, it lists the steps with their node, HTTP status, result, duration, and output names, then the verification and cleanup steps. --stepshows one step.--request,--response,--inputs, and--outputsprint that part as JSON.--pathnarrows the part with a gjson path.--shapeprints the part's structure: each path with its type, array sizes, how many objects hold a key, and a sample value.- The run is
latest(runs inside batches included), a run ID, a batch ID and run ID joined by a slash, or a path to a run directory, an archive file, or an.aarexport. - The MCP server's
get_sample_responsetakespathandshape, asaat run showdoes. - Its
run_idacceptslatestand a batch ID with a run ID, and finds runs inside saved batches. - The same goes for
inspect_archive,analyze_failure, anddiff_archives. - The AI assistant primer is published as raw Markdown, for tools that fetch pages.
llms-full.txtat the docs site's root holds the whole primer, andllms.txtindexes it and the reference pages.aat docs primerprints the same primer from the binary, in the version that matches it.- The primer links to docs pages by URL, so its links also work outside the site.
- OAS validation checks form-encoded request bodies (
application/x-www-form-urlencoded) against the operation's schema. Bracketed keys such asitems[0][sku]=…andtags[]=…are read as nested objects and arrays, and values take the types the schema allows. Before, only JSON request bodies were validated, and a form body was not checked. {{@index}}in an iteration block is the element's position, counting from 0, for keys such asitems[{{@index}}][sku].aat generate --operationand--pathscaffold only some of a spec's operations: the operationIds named, or the operations under a path, matched by whole segments. An operationId the spec doesn't have is an error that suggests similar ones, and so is a path that matches nothing.- Expressions for generated values and timestamps:
{{uuid}}(a random version 4 UUID),{{random N}}(Nrandom digits and lowercase letters),{{now}}(UTC, RFC 3339), and{{unixtime}}(Unix seconds, an integer).nowandunixtimetake offsets in seconds, minutes, hours, or days, such as{{unixtime - 1 hours}}. Each occurrence generates its own value, and a retried step resends the values its first attempt generated.
Changed¶
- The docs describe the three real-API projects on one page, Real APIs, the README says what people use AAT for, and the guide pages open with why each feature exists before how it works.
- The documentation says why AAT exists, not only what it does. A new "Why AAT exists" page (
docs/user/why.md) opens the Getting started section with the tool's origin — a pile of shared Postman collections that had stopped being trustworthy — maps each problem to what replaced it, and then covers what else turned out to be able to read the graph once it existed: the MCP server, run archives as evidence you can send someone, generated reference docs, and handing off mid-run to CI or another tool. It also says plainly what an on-ramp from an existing OpenAPI spec or Postman collection looks like. The README'sWhyand the docs home carry the short version. - The README, the examples index, and
ROADMAP.mdlist the three projects built against real, public APIs in test mode — aat-duffel, aat-stripe, and aat-shippo — with their coverage and batch results. They were listed as planned work before. - A predicate's
<,>,<=, and>=compare two strings that are both decimal numbers as numbers, so"1000.00" > "999.50"holds; before, they compared as text.==and!=still compare text, and dates and other strings still order as text. This applies wherever predicates are read: assertions, selection filters, constraints, and cleanupwhen. aat run show --step --jsonnames a step's assertion resultsvalidation({passed, results}), asarchive.jsondoes, where it saidassertions.aat validatenotes a workflows, layers, or plans directory that the manifest names but that doesn't exist yet. It reads as empty, where before it failed the manifest check.- Manifest messages print paths relative to the manifest.
- A missing layers directory loads no layers. A run that asks for a layer says the directory doesn't exist.
- A header whose whole value is one placeholder, such as
Idempotency-Key: "{{requestKey}}", isn't sent when that input has no value, instead of failing withunresolved placeholders. The{{?requestKey}}…{{/requestKey}}wrapper still works, and a placeholder inside other text still needs a value. aat generatescaffolds a form body asform:, one field per property with noContent-Typeheader, and writes an array property the spec encodes as adeepObjectasname[]. An object property no longer warns, since a map value is sent as bracketed keys. An optional header parameter is a plain placeholder.- A
from, afromInput, or a named selection'sfromadds the step it reads todependsOnwhen the plan is instantiated, as graph defaults and composition already did. Plan validation no longer reports a reference whose step is missing fromdependsOn, and a cycle message names the value or selection that implies a dependency. An isolated mutation checks its clone IDs against the dependencies references imply. - A graph default's
from: node.outputreads the nearest earlier step on that node that isn't expected to fail, instead of the node's first step. On a verification step it reads the last such step, chosen before mutations expand. - Before, a verification after a refused request on the node read the refused step and failed with
no outputs, and one after two successful steps read the first. - AUTOWIRE passes over steps expected to fail as well.
- A graph default's literal value reads
graph_defaultin--resolutions, where it readplan_default, which now means a value the plan sets. - Composition wires the AUTOWIRE markers that the slot and addon passes leave, once the plan is complete: a base or slot step can take an output that only an addon produces, and a base workflow without slots or addons resolves its markers. The final pass takes the nearest earlier producer that does not depend on the step. Markers the earlier passes already wired are unchanged.
- A plan that still holds an
AUTOWIREmarker after composition fails validation, naming the step and input, instead of sending the wordAUTOWIRE.aat promptasks the model for those inputs, andaat validate plancomposes standalone base workflows before validating them. - Workflow compatibility checks cover base workflows and slot options. A plain
AUTOWIREthat the base and its slots cannot feed is a warning that names any addon producing the output.AUTOWIRE?on a required input with no graph default is also a warning.AUTOWIRE?in an addon never warns as unfed. aat generateno longer replaces an existing graph file or template: it lists them, writes nothing, and exits 2 unless given--force. OperationIds that differ only in case are an error, since their templates would be one file on a case-insensitive file system.- A generated template sets
Content-Typeonly when it has a body, to that body's media type; before, every operation with a request body gotapplication/json.aat generatewarns about what a template leaves to write by hand: multipart and other bodies, a body schema withoneOf,anyOf, or no properties, and a parameter with a non-defaultstyleorexplode: false. OAS validation reads request body properties fromallOfbranches too. - A list value in a request URL or form body is no longer sent as JSON text. Right after
key=in a query or form body it repeats the pair (tags=a&tags=b); anywhere else its elements are encoded one by one and joined with commas (/items/1,2). - A step whose failed response asks for a wait longer than 60 seconds stops retrying (
failed_fast), and the error detail says how long the server asked for. Before, the retries went out after the backoff regardless. - Docs: the Homebrew cask is documented for Linux as well as macOS.
brew install gburgyan/tap/aatinstallsaatandaat-sandboxwith Homebrew on Linux. minandmaxselection compare numbers sent as strings by value: asortFieldof"99.10"sorts below"1000.00", so an API that returns prices or totals as decimal strings selects the cheapest element. A string that is not a number still fails the selection, naming the element.- Graph validation rejects cleanup pairings that loop back, such as a node cleaned up by
bwhose own cleanup is that node again. The error names the cycle once, asa → b → a. - Cleanup step IDs are unique within a run: a node's second cleanup step is
deleteCart_2in archives and--jsonoutput, as the web UI already named it. Cleanup responses are checked against the graph's error detection rules. A flagged response recordsresponseBodyErrorand ends its chain; the run outcome is unchanged. - Breaking:
aat run plan --dump-stateredacts credentials by default, as run archives do. - Credential headers such as
AuthorizationandX-API-Keyread[REDACTED], at the top level and in every step. - Known secrets are replaced wherever they appear, and the export gains
"redacted": true. --dump-state-secretskeeps live credentials, for a harness that sends requests as the run's session. It warns on stderr when the dump goes to stdout, and it is an error without--dump-state.- A harness that reads
auth.headersto send requests needs the new flag. - Docs: the AI assistant primer covers more ground:
- layers and batches
- step value forms: pools, literal lists, and
{} - expressions and assertion details
- retries and selection ties
- Lua transforms, and reading results with
aat run show - Docs: two pages disagreed with the code and are corrected:
plans.mdno longer says{}skips graph defaults for required inputs.value-flow.mdsays an inlineminormaxcan usefieldalone, and how ties break.--oas-validate strictstops before the first request, with exit code 2, when a spec the graph references fails to load. Before, the run printed a warning and continued without validating, and--quietand--jsonhid the warning. Inautomode the warning now goes to stderr, where--quietand--jsonkeep it visible.aat promptbehaves the same way.- In a form body, or in a path after its
?, an iteration block whose body writes a wholekey=valuepair joins its copies with&instead of commas, so{{#tags}}tags[]={{.}}{{/tags}}sendstags[]=a&tags[]=b. A block whose body starts or ends with&is repeated with nothing between the copies. Blocks elsewhere, including JSON bodies and blocks inside a pair, still join with commas. aat generatewrites each optional form body field as one conditional block that brings its own&, so a body of many optional fields grows with their number rather than its square. A request body the spec declares empty (no properties andadditionalProperties: false) gets no body and no warning. A form body property that takes an object, or an array of objects, gets a warning to write its keys by hand as bracketed pairs.- A run that stops on a step's error names the step, as in
step "addSocks" (addItem): executing HTTP request: …, so theaat:line and the--jsonerror say which step failed. - A retried step sends the same inputs on every attempt. Its values are resolved once, before the first attempt, so a
random pool pick, a
todaydate, and an overlay value no longer change between attempts. A plan-level--retriesrerun still resolves them again. uuid,now, andunixtimeare reserved words in expressions, so{{now}}no longer refers to an input namednow. An offset in hours or minutes ontoday, or on a reference, is an error that suggestsnoworunixtime.- Plan validation lists each distinct problem once,
aat validateprints an error repeated word for word once, and a step's values and selections are checked in name order, so the output is the same on every run. - A registered cleanup no longer runs when a later main step on the cleanup node already released the resource with
the same inputs, such as an explicit cancel of the order the pairing would cancel. Before, the cleanup ran again
and usually failed with a 4xx. The skip is recorded, as for a pairing's
releasedBy. - A slot option's
injectvalues decode like graph input defaults. {value: …},{pool: …},{from: …}, and expressions work. Any other key is an error, soinject: {ages: {default: [35]}}fails validation instead of sending a map. A bare list is still the literal list.- An injected value no longer replaces a step's pool, constraint, or
fromInput. injecton a base workflow or an addon, where it was ignored, is a validation error.- Validation reports an assertion of an unknown type, such as
fieldEqual, and bad expression syntax in step values, pools, and assertions. The checks run inaat validate, when a workflow template loads, and when a recipe's override assertions are applied. Before, an unknown type failed only at run time, a recipe override of one was dropped silently, and a bad expression failed only when its step ran. - An optional input that takes
from:an output the earlier step didn't return, directly or through a named selection, is left out, asAUTOWIRE?leaves one unset, instead of failing the step. Its resolution recordsoptional_skip. A required input still fails, andaat validate --strictwarns when a required input takesfrom:an optional output, in a graph default or in a plan or workflow file, or reads one through a named selection. The MCP server'svalidate_planandsave_planlist the warnings for a plan file. - Docs: the OAS validation pages no longer claim checks that don't run (the HTTP method and input types in
aat validate, and every request at run time). The AI assistant primer covers starting from an OpenAPI spec, form bodies and query strings, headers and idempotency keys, lists and pagination, the request timeout, and reaching an object after an expected failure.
Fixed¶
aat docs generate's diagram draws the wiring between nodes. It readrequires/satisfiestokens and cleanup pairings only, so a graph that wires its nodes the usual way — an input defaultingfrom: otherNode.output— came out as a wall of unconnected boxes. An input'sfrom:is now an edge too, after the tokens and the cleanup arrows, so an explicit pairing keeps its own style. On the example projects the diagrams go from 0 edges to 13, 39, and 63.- An input sent as a JSON body property the spec names otherwise counts as that property, as one sent as a form
field, a query parameter, or a path segment already did.
"mass_unit": "{{massUnit}}"now matches the spec'smass_unit, quoted or bare and inside a conditional block, in both the unknown-input and the required-property checks. Until now only form-bodied APIs could name inputs for the package rather than the spec; a JSON-bodied one had to spell every input the spec's way or carry a warning for each. aat validate --strictknows the inputs of a node whose request body is aoneOforanyOf, or whose inputs name properties of an object nested inside it. A template routinely flattens a nested body into one input per leaf, and every one of those was reported as unknown. Only the schema and itsallOfwere read, so a body written by hand for a composed schema had every one of its inputs reported asinput "weight" not found in OAS parameters or request body.aat generatedeclines to write such a body and says so, so the operations it hands over were exactly the ones that then warned — six of them in Shippo's spec. Property names are now collected from every branch the body can take; an input no branch declares is still reported, and what counts as required is unchanged, since a property required in one branch is not required of the body.- A block key may end in
[], so an input named for an array query parameter can gate its own block:{{?status[]}}?status[]={{status[]}}{{/status[]}}. Block tags matched letters, digits, underscores, and hyphens only, so the tags were never recognized:aat validate --strictrejected the template withtemplate requires placeholder "status[]" but graph input is optional with no default, and a request failed withunresolved placeholders: ?status[], /status[].aat generatewrites exactly that shape for a parameter whose name ends in[], so its own output failed validation for any spec that has one. - A conditional block around an iteration block of the same list renders, as in the templates guide:
{{?ids}}, "ids": [{{#ids}}"{{.}}"{{/ids}}]{{/ids}}. Both blocks close with{{/ids}}, and each block took the first one, so an absent list failed withunresolved placeholdersand a present list rendered malformed JSON, such as["p"],"q"], without an error. A closing tag now closes the innermost open block with its name. - A plan with a list or map step value reads back after it's saved, as by
aat prompt --saveor the MCP server'ssave_plan. A list was written bare, which the parser rejected, and a map was written as the step value's own keys; a map is now written underdefault:. - OpenAPI specs with circular references load. A schema that refers back to itself, directly or through another
schema, used to fail with
infinite circular reference detected, soaat generate,aat validate, and the MCP server rejected the spec, andaat runskipped OAS validation. Large published specs have such cycles. Other errors in a spec still fail it, and libopenapi's log lines no longer reach stdout. - Runs with OAS validation start quickly on a spec with hundreds of operations. The validator is built for the operations the graph's nodes name, not for the whole spec, which could take most of a minute before the first request.
- OAS validation accepts
nullfor an OpenAPI 3.0 schema markednullable: truethat is built withanyOforoneOf, such as a field that holds either an ID or an expanded object. The validator addednullonly to a schema's owntype, so a response with such a field set tonullfailed withgot null, want object. - A request body that is neither JSON nor form-encoded, or a schema the validator can't compile, is reported as not
validated. The archive marks the payload
skippedwith a reason, the step line showsOAS: request not validatedorOAS: response not validated, and aschemaassertion is skipped with the reason. Before, the step line could readOAS: 0 warning(s), and aschemaassertion failed with an empty message. - A form-encoded body no longer sends the final newline of a
body: |block, which the server read as part of the last value. Whitespace around the body is removed for form bodies only. - The static OAS check no longer reports a required form field or query parameter as missing when the template writes
it. The keys of a form-encoded body count as supplied, and a bracketed key such as
metadata[source]suppliesmetadata. - The static OAS check no longer reports an input that the template sends only in request headers, such as an
idempotency key, as missing from the operation's parameters and request body. The template names the header, and
specs often leave such headers undeclared, so
aat validate --strictfailed on it. - Two inputs that pick from the same array with
minormaxby different fields no longer get the same element. The selection cache left out the compared field, so the second input got the element chosen for the first. Amatchselection'sfilteredSizeis now the number of matching elements, not the array's size. - A required input marked
{}falls back to its plain default with layers applied, and evaluates the expressions in it, sopostalCode: {}with a default of{{env.postalCode}}sends the variable's value, not the text. A layer's value for the input wins over the graph default, as it does without{}. - A request that times out says so, naming aat's 30-second request timeout, instead of giving only Go's
context deadline exceeded (Client.Timeout exceeded while awaiting headers). The limit is named only when the whole limit passed, so a shorter timeout or an interrupted run isn't blamed on it. aat generatewrites the graph'soas:reference relative to the graph file's directory, so a spec kept elsewhere resolves. It used to write only the spec's file name.aat generatetypes object body propertiesobjectand inserts them as JSON literals instead of quoted strings, and no longer writes a node-levelname:line.aat run planandaat run batchredact the access token they authenticated with, such as an OAuth2 token, wherever it appears in an archive. Before, it was masked only in credential headers.
0.1.0 - 2026-09-12¶
The first release with release archives, a Homebrew cask, and a Docker image. v0.0.1 through v0.0.4 are
retracted, so go install and go get skip them.
Added¶
aat-sandbox, a second binary that serves an offline e-commerce demo API (aat-sandbox serve: shop API on :8765 with OAuth2 tokens, payments API on :8766 with an API key,us/euregions with their own currency, tax, tiers, and coupons, an order state machine, simulated latency, and two chaos hooks for retry demos) and extracts theexamples/shopproject (aat-sandbox init). The contract lives inexamples/shop/openapi.yaml; the server tests validate every response against it.make buildbuilds both binaries;make sandboxbuilds only the demo server. The sandbox binds127.0.0.1unless--hostsays otherwise.examples/shop, the offline quick-start project foraat-sandbox: a 17-operation graph, Quick Purchase and Checkout workflows with slots and addons, 12 layers, 7 plans (full order lifecycle, retries, a negative state-machine walk,addItemmutations), a declined-card overlay, a receipt visualizer,us/euenvironments with payments routed to their own host and credential, and MCP configuration for AI coding tools. Its graph describes the API for the AI tools that read it through MCP: each operation lists its error codes in the order they are checked, and each input and output says what it must be, where it comes from, and what it means.- The shop example ships an integration kit, the part of an AAT project that an API's integrators get.
aat-kit.yamlnames the graph, templates, OpenAPI spec, domain file, workflows, sandbox environment, and three reference plans.sh package-kit.shcopies them into a directory and a tarball, with the kit manifest as itsaat-project.yaml. Theshop-apiserver in.mcp.jsonreads the kit manifest, so it shows what an integrator's AI tool would see.make example-shoppackages the kit, then validates and runs the unpacked copy. - Multi-environment files: one environment file holds named environments with shared settings, inheritance
(
extends),${var}substitution in every string, abstract environments (names starting with_) to inherit from, andinclude:to split out files such as secrets.--env NAME,AAT_ENV_NAME, an overlay'senvironment:, or the manifest'sdefaultEnvironmentselects one, in that order.defaultEnvironmentapplies only to the manifest's own environment file, and a single-environment file still loads, with--envan error for it.aat env listlists the environments, andaat validatechecks each one. --var KEY=VALUE(repeatable) onaat run plan,aat run batch,aat prompt,aat validate,aat env list, andaat mcp servesets a var of a multi-environment file for one invocation, for example to pointexamples/shopat a sandbox on other ports or in a container. A key the file never declares or references is an error.- An environment's
values:map holds per-environment data that plan values, graph input defaults, and layers read with{{env.KEY}}; an OS environment variableKEYwins over it. - Overlays (
--overlay FILE,.aat-overrides.yaml, and nowaat-overrides.yaml) can setauthandheadersfor every request, andenvironment:to choose the environment. Overlay headers win over the credential on every route.overrides:entries in environment files and overlays acceptvalues:andexpectFailure:, so a happy-path plan reruns as a negative test without edits; the archive records each override value as the input's resolution, with the sourceoverride_value. - OAuth2 auth accepts
grantTypeandextraParamsfor providers such as Auth0.--verbose-authlogs token requests and responses to stderr, with the access token masked and at most half of a short password or client secret shown. - Step
mutations:expand a step into independent sibling steps when the plan is instantiated, each withset:input overrides or arawBody:for a malformed payload.mutationScope: isolatedgives each sibling its own copy of the steps it depends on, and--no-mutationsdrops mutations for a quick run. Theschemaassertion type validates a response against the OpenAPI spec; it was a stub. - Checkpoints:
aat run plan --stop-after STEPstops after a step and skips cleanup, so what the run created stays alive, and--dump-state FILE(-for stdout) exports the live session for another tool: the environment's base URL and auth headers, each step's base URL and request headers, unredacted, and the step outputs. The file is written with mode0600. With--json, the summary nests the state understateand reportsstopped_at. - Ctrl+C ends
aat run planandaat run batchasaborted(exit code130), even during a request or a retry wait. Cleanup still runs, within a 30-second budget, and the archive is written. The web UI styles theabortedandstoppedoutcomes. aat mcp serve --httpserves MCP over Streamable HTTP, with a/healthzendpoint.--loglogs each tool call as JSON to stderr.--hostonaat web,aat web view,aat web viewtrace, andaat mcp servechooses the interface to bind (default127.0.0.1; theAAT_HOSTenvironment variable sets it when the flag is absent). The Docker image setsAAT_HOST=0.0.0.0.- Plan-level
execution.cleanupsteps execute after the main flow, honoringrunOn: always|success|failure. A step that names a node paired with a graphcleanup:does not run on its own: itsrunOndecides whether the pairings run, once per created resource, newest first, and never for a create step that failed. Other cleanup steps run first, in declaration order. - Plan
execution.verificationsteps execute after the main flow and before cleanup, with their assertions counting toward the run outcome. They appear in archives asverify_<node>steps. - Workflow slot options and addons can declare
verification:. Composition merges it into the base workflow's, and a node they verify replaces the base's verification of that node. - A step that succeeds after retrying shows
retried Nx: <category>in run output and on the web UI's timeline and step page. Archives record the category of each retried attempt inretriedOn, and the--jsonstep summary inretried_on. - Run output lists each failed assertion under its step (
status: expected status 200, got 201), and the--jsonstep summary includes them asfailed_assertions. statusassertions accept a status class such asexpect: 2xxorexpect: 4xx.retry.onandretry.failOnaccept HTTP status codes (on: [503]) alongside category names;aat validate planrejects unknown rules.--oas-validate strict(andsettings.oasValidation: strict) now fails a step whose request or response violates the OpenAPI spec; it previously behaved likeauto.expectFailuresteps are exempt, and skipped validations or schema compilation warnings never fail a step.aat validatechecks the layers directory: parse errors, duplicate layer names, and layer input keys that match no node input (which layers silently ignored).aat validatechecks the domain file andvisualizers.yaml(Domain and Visualizers sections), and names each error's file relative to the working directory.- Template conditional and iteration blocks accept keys with hyphens, such as
{{?X-Request-Id}}…{{/X-Request-Id}}for a header parameter. - The web UI's step request view copies the request as a
curlcommand. aat run cleandeletes auto-generated run and batch archives and keeps named ones.tools/aat-to-junit.pyconverts a run archive to JUnit XML for CI test reports.- Workflows accept a
selectionHintthataat promptshows the model when it chooses a workflow. The trace viewer's Copy Conversation button copies a planning call's model settings and conversation as JSON. aat webreports a clear error (exit code 2) when the frontend bundle is not embedded, such as a plaingo installbuild; the CLI, MCP server, and CI features still work in that build.aat --versionreports the module version forgo install …@vX.Y.Zbuilds.- Release pipeline: versionless archive names for stable
releases/latest/download/…URLs, a Homebrew cask in a tap (brew install gburgyan/tap/aat), and a multi-arch image atghcr.io/gburgyan/aat. Release archives and the Homebrew cask includeaat-sandboxnext toaat, and the release notes are this file's section for the version. make example-shoprunsexamples/shopagainst a local sandbox the way the CIexample-shopjob does, andmake clibuildsaatwithout rebuilding the web UI.- A documentation site built from
docs/userwith Material for MkDocs (mkdocs.yml), deployed to GitHub Pages by a new Docs workflow that fails on broken links, broken anchors, and pages missing from the navigation.make docsruns the same strict build locally. - Documentation pages for installing, checkpoints, archives,
aat generate,aat docs generate, the examples, and the airline case study; the Lua transforms page is written out. The quickstart (on the public Petstore API) and the tutorial (built by hand againstaat-sandbox) are rewritten and were run verbatim; the previous versions did not load. - Docs: Share Your API with Integrators describes an integration kit for any API: what to ship and what
to keep, a kit manifest beside the project manifest, internal environments that
include:the shipped file, packaging and checking the kit in CI, and what an integrator's AI tool can read through theapipersona. It publishes the prompt, the setup, and the results of the shop's single-prompt Python and Go client runs, so the claim that a kit lets an AI tool write a working client from one prompt can be checked. The docs home page, the examples index, Project Setup, and MCP Server link to it. make demosregenerates the docs site's recordings and screenshots against a freshaat-sandbox: VHS recordings ofaat run plan full-lifecycleand a parallel layer-group batch, Playwright screenshots of the run timeline, a step's request with Copy as cURL, and the batch matrix, plus an MP4 of the plan run and the repository's social preview (demos/). It checks that both recorded runs passed and that each GIF stays within its size budget before writing anything. The docs site's home, matrix-testing, and web UI pages show the results.- Repository scaffolding: issue and pull request templates,
SECURITY.md, andROADMAP.md.
Changed¶
- BREAKING: the environment file flag
--env FILEis now--env-config FILE, and--env NAMEselects a named environment.--env-overlayis now--overlay. - BREAKING: exit codes follow one rule on every command:
0passed.1a test or validation ran and found a failure.2AAT could not do what was asked. That covers an unknown flag, argument, or subcommand, and a project, environment, or--varerror. Most commands exited1for these.130aborted.
Command by command:
- aat validate exits 1 when it finds a problem, and 2 when there is no manifest to validate or
--var is bad.
- aat prompt exits with its run's outcome code, instead of 1 for any run that did not pass.
- aat import, aat generate, and aat docs generate exit 2 on an error.
- BREAKING: project YAML is decoded strictly. A key that no field accepts — in the manifest,
environment files and their includes, overlays, the graph, templates, the domain file, visualizers,
workflows, layers, plans, recipes, and plan YAML given to the MCP plan tools — is an error naming the
file, the line, and the likely intended key (plans/smoke.yaml: line 12: unknown key "fromSelecton"
in step value (did you mean "fromSelection"?)). Such keys were silently ignored, so a typo produced a
plan that loaded and did something else. A second YAML document in a file (--- followed by content)
is an error too. aat prompt model output (JSON) is unaffected. To migrate, run aat validate and fix
what it lists.
- BREAKING: a manifest that exists but fails to load is an error for every command that discovers
it; it was skipped, so commands fell back to a lower-priority project or to none. A missing manifest
is still skipped, and a higher-priority manifest that loads still wins.
- BREAKING: request templates escape each substituted value for where it lands. Values used to go in
raw.
- Path: a value is URL-encoded as one path segment before the first ?, and as a query component after
it. a/b stays one segment, and & or # in a value can no longer add a parameter or cut the URL.
- JSON body: a value inside quotes is JSON-escaped, so a quote, backslash, or newline in a field such
as notes no longer breaks the body. A value outside quotes is written as JSON: arrays and objects as
JSON, numbers in plain digits, and null.
- Form body: values are URL-encoded.
- Headers and other bodies: unchanged.
To send a malformed payload on purpose, use a step's rawBody.
- BREAKING: JSON keys follow the convention of the document they are in:
- The aat run batch --json summary's batchId is now batch_id. It is left out when the batch stopped
before it started.
- In run archives, a step's duration_ms is now durationMs. AAT and tools/aat-to-junit.py still read
archives that use the old key.
- A plan's auth in metadata.plan and metadata.instantiatedPlan uses camelCase keys (tokenUrl,
credentials) instead of Go field names (TokenURL, Credentials).
The state object that --json nests with --dump-state - keeps the camelCase keys of the --dump-state
file.
- BREAKING: a recipe's overrides must name steps of the composed plan. An override for any other step ID,
such as an addon step without its inc0_ prefix, used to be ignored; now the recipe fails to load, and the
error lists the plan's steps. A value override on an input that the workflow template wires with from,
fromSelection, or fromInput now replaces that wiring and sends the override. It used to have no effect.
- BREAKING: Lua transforms can no longer load code or reach the host process. The package library is gone,
and so are the base library's dofile, loadfile, load, loadstring, require, module, getfenv,
setfenv, collectgarbage, and newproxy. An integration kit's templates run on its users' machines, so a
transform must not read their files. Also, dofile() and loadfile() with no argument read stdin, which under
aat mcp serve is the MCP connection.
- BREAKING: aat run batch <filter> selects plans by whole path segments. orders selects orders.yaml
and every plan under orders/, and orders/refund selects one plan. The filter matched the start of each
plan's path, so smoke also ran smoke-eu.yaml.
- BREAKING: a batch that finds no plans exits 2, and the message names the filter and the plan
directories. It used to pass with nothing run, so a mistyped filter passed in CI. An absolute path that does
not exist gets the same error.
- BREAKING: aat run batch --json reports an error that stops the batch before any plan runs in a top-level
error field, with an empty runs array. It used a run entry with no plan name.
- BREAKING: only aat run plan and aat run batch take the execution flags: --env, --env-config,
--graph, --templates, --domain, --override, --overlay, --var, --retries, --layer,
--no-auto-overrides, --oas-validate, --verbose-auth, and --no-mutations. aat run clean and
aat run rebuild-summaries accepted and ignored them; they now reject them.
- Template headers no longer replace the auth credential, an override's own headers, or overlay headers. They
still replace environment and plan headers, such as a per-operation Content-Type. Header names compare
case-insensitively when headers merge.
- Run output names each step by its step ID, which --stop-after, dependsOn, the archive, and the web
UI use, with the node in parentheses when the two differ and the column has room
(addProduct (addItem)); it printed the node, so two steps on one node looked alike. Engine errors do
the same (step "addSocks" (addItem) returned status 409), and so do the parallel batch display and the
MCP execute_plan table.
- Durations are wall-clock: a retried step's duration runs from its first attempt to the end of its last,
so retry waits count, and a run's duration (the PASSED line, the --json summary.duration_ms,
batch.json run entries, the web UI, and MCP) is the time the run took, recorded in the archive as
result.durationMs. Both were sums of the last attempt of each step, so full-lifecycle printed 955ms
for a three-second run. Archives written before keep showing the sum. Step durations of a second or more
read 1.4s.
- A step that fails after retrying prints its error followed by the same retried Nx: <category> note as
a step that recovers; the note used to take one of two other forms depending on the terminal width.
- aat run progress output marks OpenAPI violations on each step (OAS: 1 warning(s)) and totals them
after the outcome, as documented; only an unused summary path printed them before.
- aat validate and aat validate workflow show OpenAPI and workflow-compatibility warnings as a WARN
section without --strict instead of reporting OK, and counts read "1 file" rather than "1 files".
- The sequential batch header no longer prints mode=strict, a leftover of the runtime modes removed in
0.0.2.
- aat web and aat mcp serve --http listen on 127.0.0.1 by default instead of every interface.
Pass --host 0.0.0.0 (or set AAT_HOST) to accept connections from other machines.
- Steps composed from workflow templates (recipes, aat prompt) get a default status: 2xx
assertion instead of status: 200, and none when they declare expectFailure. On a step with
expectFailure (including one added by an overlay), a status assertion that expects success is
reported as skipped, since the expected-failure status list is the status check; one that agrees
with it, such as 409 or 4xx, is evaluated.
- The --json step summary's name is the step ID, as documented, instead of the node name, so
mutation siblings and repeated nodes are distinguishable.
- aat generate --oas places optional query parameters, headers, and body properties in conditional
blocks, orders body properties as the spec does, and writes integer, number, boolean, and array body
values as JSON literals. With --output-graph - it writes no files unless --output-templates is
given. A template header that resolves to an empty conditional is not sent.
- A layer that sets a value source (value, pool, from, fromResolved) replaces the graph
default's source instead of merging with it, so a layer value is no longer shadowed by a default's
from.
- Requesting layers (--layer, --layer-group, or a recipe's selection.layers) without a
layers: directory in the manifest is an error; the layers were silently ignored before.
- An override that declares its own auth no longer sends the inherited credential header
(Authorization, or the top-level API key header) to its host.
- Override precedence: among glob (and among exact) overrides the last registered match now wins, so
.aat-overrides.yaml, --overlay, and --override take precedence over env.yaml overrides as
documented. Exact names still beat globs.
- Cleanup input matching scans earlier steps in execution order (it was map order).
- MCP operation details and explain_field label a graph input's default "Test default": it is data AAT
sends in tests, not a value the API fills in. Read as a plain default, the shop's quantity: 1 and
method: card made required fields look optional.
- CLI description and --help text describe AAT as graph-based API workflow testing; the LLM is
optional and authoring-time only, and execution never calls one.
- Documentation covers features that had none: the Ctrl+C aborted outcome, --oas-validate, the batch
matrix view, archive import and export, and aat run rebuild-summaries.
- libopenapi-validator v0.14.0 and libopenapi v0.38.7.
- Minimum Go version is 1.25.7 (the OpenAPI libraries require it).
Removed¶
- BREAKING: YAML keys that nothing read, which strict decoding now rejects: step
fallback,assertions.semantic, the environment settingsmaxRunDuration,defaultRetries, andarchiveFormat(retry with a step'sretry:or--retries), templateresponse.validate, thepromptfield of selections and graph defaultselect, and recipeoverrides.descriptions. - BREAKING: the
llmselection strategy, which plan validation already rejected butaat promptaccepted from the model, and thewarnOpenAPI validation mode, which behaved exactly likeauto. - The MCP
execute_plantool no longer accepts the obsoletemodeparameter (the runtime strict/lean/adaptive modes were removed in 0.0.2). - Repository leftovers from the private airline project (
setup.sh, a root-level plan, IDE run configurations, an empty case-study stub).
Fixed¶
- Building the web UI (
make frontend,make build) no longer rewrites a tracked file, andmake cleanno longer breaksgo buildby deleting one. Vite writes the bundle toserver/web/dist/app, which git ignores, and the embed is satisfied by the trackedserver/web/dist/placeholder.txt. - An
errorDetectionequalsrule with a number (value: 0) matches the JSON number. The YAML integer and the JSON number used to compare as different types, so the rule never matched. A map or listvalueis now a validation error; at run time it crashedaat runand the MCP server. - A request path value with an encoded
/(%2F) keeps it inside its segment; the executor used to decode it into a real/. The archive records the URL as the executor joins it, instead of concatenating the base URL and the path. - A number of a million or more fills a placeholder in plain digits instead of exponent form (
1.2e+06), and a map fills one as JSON instead of Go syntax (map[k:v]). - Archive references stay inside the archive directory:
aat import --namemust be a single directory name. A name that starts withrun-orbatch-gets the!prefix, as a name derived from the file does.--name ../xused to import outside the archive directory.- The web server answers
404for a run, batch, or trace ID that is not a single directory name. An ID of..read the parent directory'sarchive.json.PUT /api/runs/{id}/namerenamed any directory in the archive directory; it now renames only runs and batches. - The MCP archive tools reject a
run_idthat is not a directory name. The plan tools reject absolute plan names and names that climb out of the plans directory. - An unknown subcommand is an error (exit
2), with a suggestion when the name is close. This coversaat run bogusand unknown subcommands ofaat plan,aat env,aat mcp, andaat docs. They printed help and exited0, so a mistyped subcommand passed in CI. Commands that take no arguments, such asaat validateandaat web, now reject stray arguments instead of ignoring them. aat mcp servereports a manifest that fails to load with the load error; it said the manifest was not found.aat importfails on such a manifest instead of importing into_output/runs.aat run plan --jsonandaat run batch --jsonprint the error document for every error that stops them before a plan runs. Before, a manifest that failed to load, a bad--var, or an overlay environment that could not be resolved left stdout empty.- Graph-level cleanup deletes the resource each step created. Cleanup looked up the creating node's
outputs by node name, but outputs are stored by step ID, so a step with its own
idfell through to the first step with an output of that name: twocreateCartsteps with their own IDs deleted the first cart twice and left the second. Cleanup now reads the registering step's outputs, then the most recent step with a matching output. - A request that fails before any response reports
executing HTTP request: …once, notexecuting request: executing HTTP request: …. aat mcp servestarts when a relative--manifestnames anoas:spec. The spec path was joined onto the graph's directory a second time (examples/shop/examples/shop/openapi.yaml), so a project loaded from another directory failed with "no such file or directory".- The MCP
get_sample_responsetool returns the newest successful response for an operation, and a failed one, marked as such, only when no run succeeded. It took the newest response of any status, so a negative test's409could pass for the sample. It also searches the runs inside batch directories, and a manifest withoutarchivesgets the expected output shape instead of an error. The archive tools accept the ID of a run inside a batch. - A run archive that cannot be redacted is not written.
aat run, batch runs, and the MCPexecute_plantool report the error; before, the error was ignored and the archive was written with its secrets in place. Redaction fails only on a value JSON cannot hold, such as a NaN from a Lua transform. aat mcp servewithout--personaregistersget_data_flow,get_response_shape, andexplain_field, which only theapipersona had, so it has every tool: 39 with an OpenAPI spec, 32 without.- The web UI's run timeline shows a step's assertion count only when the step has assertions, not
0 / 0on every step. - Run archives redact known secrets from every string they hold: request URLs and query parameters,
request and response bodies, outputs and display outputs, error, assertion, and OpenAPI messages,
and plan step values, as well as headers, inputs, and resolved values;
batch.jsonentries too. In JSON bodies only string values change. Before, a credential used as an input was redacted ininputsbut kept in the body of the same request. - Archive redaction no longer mangles ordinary data: the oauth2
usernameandclientIdare not treated as secrets, and a secret shorter than eight characters is redacted only where a whole value equals it. The shop sandbox'sdemocredentials had turneddemo@example.cominto[REDACTED]@example.comin inputs. Overlapping secrets are redacted completely; map order could leave part of one visible. - Run archives redact an API key sent under a custom
auth.headerName, the credentials of host overrides and overlay overrides (they were never collected as secrets), and the literal credentials and credential headers of the plan stored inmetadata.planandmetadata.instantiatedPlan;aat promptarchives also collect the credentials of.aat-overrides.yaml. aat generate --oasmarks a response property the schema does not list asrequiredas an optional output with an optional extract rule, so a scaffolded step no longer fails when the API omits it.- A
selectwith afilterand nostrategyworks likematchinstead of failing at run time withunknown selection strategy. --manifestnaming a file that does not exist is an error instead of silently falling back to manifest discovery.aat run planno longer prints a failed or errored run's message a second time on stderr.- A project's manifest no longer inherits fields it leaves out (such as
domain,layers, ordefaultEnvironment) from a lower-priority project named byAAT_PROJECTor the user config'sdefault_project; the highest-priority manifest found describes the whole project. aat validate,aat validate plan, and the MCP plan tools resolve recipe layers, so a misspelled layer is reported and layer-supplied inputs no longer fail validation.- MCP
execute_planapplies override values,expectFailure, and recipe layers likeaat run plan. aat plan listsummarizes recipes instead of reporting a parse error for each.- Workflow compatibility checking (
aat validate) accounts for slots: an addonAUTOWIREinput that every option of a slot produces is no longer reported as unfed, an addon that attaches after a slot option's node is checked instead of skipped, and slot options are no longer checked as bases of their own. - The static OpenAPI output check looks each output up at its template extract path, through nested objects and array items, instead of requiring a top-level response property named after the output.
settings.oasValidationand--oas-validatereject unknown values instead of treating them asauto.- A workflow template whose
verification:names a node missing from the graph fails to load, as cleanup entries already did. aat run batch --parallel Nwith runtime OpenAPI validation no longer has a data race: parallel runs share one loaded spec, and libopenapi-validator v0.13.1 wrote into the schema model while rendering a response schema behind a$ref. The upgrade to v0.14.0 removes the race, so validations still run concurrently.- Workflow composition fills slots in declaration order, so merged cleanup, slot verification, and slot
injectvalues no longer vary between runs, and neither can batch dedup fingerprints. - The sequential batch display prints a run's
OAS: N warning(s)total, as the plan display does. - Cleanup steps carry a step ID and a start time in archives, so the web UI places them on the timeline; their start time was empty.
- The batch By Test matrix no longer clips its rotated permutation labels: the header grows to fit the longest, and a wide matrix uses the space beside the page column. The run timeline shows a step's node only when it differs from the step ID.
--override NODE=URLroutes keep the environment headers, plan headers, overlay headers, and the credential, like anoverrides:entry with thatmatchandbaseUrl; they used to send no headers.aat promptrejects layers when the manifest sets no layers directory instead of silently running without them.- Lua transforms:
print()writes to stderr instead of stdout (where it corrupted--jsonand--dump-state -output),return {}is a valid empty set of outputs, and a template with a transform but noextractrules runs its transform; outputs a transform computes no longer fail the adapter output check. aat generate --oasextracts an array property of an object response by its name instead of@this.- The static OpenAPI check accepts a required parameter or body property that the template sends itself
(such as a literal
"photoUrls": []), which madeexamples/petstorefailaat validate --strict. aat validatechecks workflow templates in subdirectories of the workflows directory (such asworkflows/slots/), which it skipped.- A layer's
fromResolvedapplies over an existing graph default instead of being dropped. - The plan summary resolves
intent.goalas a step ID;aat plan listtruncates long goals on character boundaries. - Visualizers receive
--color-text-secondary,--color-danger, and--color-warningas documented; the web UI sent variable names it does not define. aat validate,aat generate --oas, and the MCP OpenAPI operation details include parameters declared on an OpenAPI path item (such as a shared{cartId}), not only those on the operation.
0.0.4 - 2026-03-04¶
Changed¶
CONTRIBUTING.mddescribes what a pull request needs and the project's policy on AI-assisted contributions.
0.0.3 - 2026-03-03¶
Added¶
- MCP server personas:
--persona apifor integrating with an API and--persona testfor writing and running tests. New tools browse the OpenAPI spec, return a sample response from past runs, and show templates, and tool errors say what to try next. - Runtime OpenAPI validation of requests and responses (
--oas-validate), with schema compilation problems reported as warnings and request and response errors shown separately. - Layer-group batches detect plans that come out identical across layer permutations and run each once. The web UI's batch page hides the skipped runs, dims their representative outcome, and adds a By Test matrix with a filter for each dimension.
- Visualizer plugins (
visualizers.yaml): custom views of a step, rendered in a sandboxed iframe in the web UI. - Run and batch summaries and the web UI group issues by category.
fromInputfills an input from another step's input, and an output can be optional when a response may omit it..aat-overrides.yamlis discovered automatically, for local development overrides.- Terminal output uses color and fits the terminal width, batch runs show progress and accept
--shuffle, and progress output shows retries. - A
rawflag on mechanical assertions evaluates the whole response body. - Archives and the web UI record when an override routed a step to another URL.
- Archives and the MCP server report the build's version.
Changed¶
- The project is renamed from Adaptive API Testing to Adaptive API Toolkit.
- Plan composition has a single entry point, and
aat prompt's planning pipeline is simpler, with a plan summary written for the user.
Fixed¶
- Inputs with graph defaults are no longer classified as supplied by a layer.
- Layers embedded in a recipe load during layer-group batch runs.
- Batch runs no longer get duplicate run group IDs (#1).
- A batch run that errored no longer shows "run not found" in the web UI.
aat web view latestopens a batch when it is the most recent archive.- Duplicate cleanup step IDs no longer leave the web UI spinning.
- Dependencies from
requiresandsatisfiestokens stay within an addon or the base workflow, so composed plans no longer get dependency cycles across them.
0.0.2 - 2026-02-25¶
Added¶
aat web view <file>opens an archive file without a project.- GitHub Actions CI runs the tests with the race detector, golangci-lint, and a gofmt check;
make checkruns the same checks locally.
Fixed¶
make clean test buildworks.- Data races found by the race detector.
0.0.1 - 2026-02-24¶
The first tagged version.
Added¶
- API graphs in YAML: nodes with typed inputs and outputs, auto-wiring, constraints, and value pools from a domain file.
- Request templates with conditional (
{{?a|b}}) and iteration blocks, response extraction, and Lua transforms. - The execution engine: dependency-aware scheduling; values wired from earlier steps, with array selection
strategies and named selections; constraint-aware fallback; retries by error category;
expectFailurenegative tests; response-body error detection; mechanical assertions; and graph-level cleanup. - Workflow templates composed from slots and addons; plans, compact recipes, and data layers, with
--layer-grouppermutation matrices. aat run planandaat run batch(with--parallel), plan-level retries,--jsonand--quietoutput, and exit codes for CI.aat prompt: an LLM drafts a plan from a sentence, with opt-in planning traces.- OpenAPI support:
aat generate --oasscaffolds a graph and templates, andaat validatechecks them against the spec. aat validatefor the graph, plans, and workflows, with project manifest discovery (aat-project.yaml).- The MCP server (
aat mcp serve): tools for the graph, templates, domain, OpenAPI spec, plans, execution, and archives, plus resources and prompts. aat docs generate: Markdown and Mermaid documentation from the graph.- Run archives with the full decision trail and redacted secrets; export and import, saved names, and fast run listing.
- The web UI (
aat web): run and batch lists, a run timeline, step detail with audit tabs, and a planning trace viewer (aat web viewtrace). - Per-node routing to other base URLs (
--overrideand environmentoverrides:), plan-level auth and headers, and OAuth2 token caching. - The Petstore example, the user documentation, and the Apache 2.0 license.