Changelog
[v0.25.0] - 2026-09-16
Changed
- Pinned
pipelex0.59.0: up from==0.58.0, exactly, so a run no longer renders every traced input and output to Rich text and HTML on the execution path, which on the hosted runner happened on the Temporal worker's workflow thread. The release also fixes the "Output of pipe" panels hanging a run inpoorpretty-print mode on a narrow headless console, and escapes every<in the graph and stuff viewer pages. It addspretty_print_mode(rich,poororsilent) under[runtime.log], which this server leaves at itsrichdefault. - The graph spec's
IOSpecno longer carriesdata_textordata_html(Breaking):pipelex0.59.0 removes the text and HTML renderings of traced data, so both fields are gone from every graph spec the run routes return, and the committeddocs/openapi/pipelex-api.openapi.yamlis regenerated to match. A graph spec saved by an earlier version is refused when read back. The settings that asked for those renderings,stuff_text_contentandstuff_html_contentunder[interpreter.pipeline_execution.graph.data_inclusion], no longer exist and boot rejects them, so they are removed from this server's.pipelex/pipelex.toml; an operator running it against their own.pipelex/must runpipelex migrateor delete the two keys by hand. - A remote input is checked for syntax only (Breaking): an http(s) URL on a
DocumentorImageinput is refused before the run withPipelineInputUrlInvalidError, answered as a 422, only when it does not parse. Whether the resource exists is no longer checked up front: the operator that fetches it raisesRemoteFileFetchError, anINPUT-domain error naming the URL and the answer it got, in place of a rawhttpxerror. Those fetches now identify themselves asPipelex/0.59.0, without the(https://pipelex.com)suffix. A local file path must still exist before the run starts. POST /v1/codegenstampsengine_version0.59.0: the stamp is the pinnedpipelexversion, so acodegen.lockcommitted against0.58.0no longer matches until it is regenerated.
[v0.24.0] - 2026-09-14
Changed
- License (Breaking):
pipelex-apiis now licensed under the Elastic License 2.0 (ELv2) instead of MIT:LICENSEnames Evotis S.A.S. as the copyright holder, the package metadata declares the SPDX expressionElastic-2.0in place of the MIT license classifier, which makes hatchling 1.27 or later a requirement for building from source, the OpenAPI contract the server publishes carriesinfo.licenseElastic-2.0(the committeddocs/openapi/pipelex-api.openapi.yamlis regenerated to match, its summary now calling the runner source-available), and the Docker image'sorg.opencontainers.image.licenseslabel readsElastic-2.0; every earlier version and Docker image tag, up to and including v0.23.0, stays under MIT. ELv2 is source-available: you may runpipelex-apifor your own team or company, on your own infrastructure or in your own cloud account, while its main limitation rules out offering others a hosted API or runner that executes methods, whether they send the methods themselves or pick them from a catalog you make available.LICENSEcarries the full terms, including its conditions on notices and redistribution, and the license page explains how Pipelex reads them. - Pinned
pipelex0.58.0: up from==0.57.0, exactly, for the relicense ofpipelexto the Elastic License 2.0, which this server now depends on under that license. The runtime did not change and neither did the.pipelex/config schema, so no migration is required. POST /v1/codegenstampsengine_version0.58.0: the stamp is the pinnedpipelexversion, so every generated artifact header and everycodegen.lockthis release emits differs from the previous one, and a lock committed against0.57.0no longer matches until it is regenerated.
[v0.23.0] - 2026-09-08
Added
POST /v1/executecarries the run's I/O artifacts beside its execution graph:pipe_output.pipe_io_artifactsgroups thepipe_io_contracts,input_formandoutput_formthat/validateproduces, keyed by namespacedpipe_refover the run library's own pipes, so a consumer rendering a run's graph can describe a data node from the run itself rather than pairing that graph with a validation of some other bundle text. It isnullunless the run generated a graph withgraphs_inclusion.graphspec_jsonon, andpipe_io_artifacts_errorcarries the message when the build ran and failed.
Changed
- Pinned
pipelex0.57.0: up from==0.56.0, exactly, for thePipeIOArtifactscarrier and its SPI transport that the/v1/executeaddition above reads back onto the public wire. The.pipelex/config schema did not move, so no migration is required. - The normalized crate is stamped
mthds_version: "2.0.0"(Breaking):pipelex0.57.0 moves tomthds0.14.0, whoseMTHDS_STANDARD_VERSIONwas cut from1.0.0, so every cratePOST /v1/resolveemits andPOST /v1/build/runnernormalizes carries the new stamp and a consumer comparing against1.0.0has to move. Crate fingerprints are unaffected —mthds_versionis excluded from the hashed payload by design — but aMETHODS.tomlpinning the old major, such as^1.0.0, now warns where it did not before. The MTHDS Protocol version is unchanged at0.6.0. POST /v1/codegenstampsengine_version0.57.0: the stamp is the pinnedpipelexversion, so every generated artifact header and everycodegen.lockthis release emits differs from the previous one, and a lock committed against0.56.0no longer matches until it is regenerated.- OpenAPI artifact regenerated for the new
PipeIOArtifactsschema and thepipe_io_artifacts/pipe_io_artifacts_errorfields it brings toPipeOutputWire, both inherited from pipelex'sPipeOutputrather than declared here. Nothing was removed and no type loosened, butPipeOutputWirepublishesadditionalProperties: false, so a strictly-validating client generated from the previous artifact rejects the added fields until it is regenerated.
Fixed
- The
/executeresponse reference documents its execution graph:docs/pipe-run.mdlistedtokens_usagesandusage_assembly_errorunderpipe_outputbut nevergraph_specorgraph_assembly_error, so a caller reading the reference had no way to learn the graph was on the wire at all. Both are documented now, beside thepipe_io_artifactspair added above, each with the configuration setting that actually gates it. - The
/validatereference documents theoutput_formview:docs/pipe-validate.mdstill describedinput_formas the only supportedviewstoken and carried no field reference foroutput_form, which has been wrong since that view shipped in v0.22.0.
[v0.22.0] - 2026-09-03
Added
POST /v1/validatecarries the output-form descriptor, under the newviewstokenoutput_form. The valid arm gains a second opt-in structured view besideinput_form, with identical mechanics: a supported token attaches a same-named top-level field, keyed exactly likepipe_io_contracts; without the token the field is absent and the body is byte-identical to a request that omitsviews; an unknown token is still lenient-ignored rather than a 422.
Two tokens rather than one, because they answer separate questions — a caller building a fill-in form wants the inputs, a caller rendering a result or registering a tool signature with a return type wants the output — and neither should pay for the other. Like input_form, the field is a canonical report field re-declared with a default here so the wire gate keeps it out of the published schema's required set, and it rides the valid arm only: it derives from a crate an invalid bundle never assembled.
The payload's schema needs no token: it rides pipe_io_contracts[…].output.json_schema, which the standard added in the same version. That pairing is the point — the descriptor states what the output IS, the schema names the property its payload arrives under, and a consumer holding one but not the other is back to inferring the other from a value.
POST /v1/validatereports the effective entry pipe. The valid arm carries a new wire-only extra,default_pipe_ref(string | null): the qualifieddomain.pipe_codea caller gets by omitting the pipe selector — the pipe a selector-less run of that same request would execute. On amethod_refrequest it is the fetched package manifest'smain_pipe, qualified against the closure; otherwise, and when the manifest declares none, it is the closure's primary blueprint'smain_pipequalified by its domain. It isnullwhen no entry pipe is determined: no blueprint declaresmain_pipe, or a manifest names a pipe the closure does not declare or declares in several domains — cases in which a selector-less run by that address would fail to resolve it too. The field closes a real gap for validate-by-address: the canonical report is manifest-blind (bundle_blueprintis the batch's primary blueprint), so a package whoseMETHODS.tomlentry differs from, or exists without, a bundle-levelmain_pipevalidated with a report from which a consumer could only derive the wrong entry pipe, or none. It states the run default rather than the/build/*routes' stricter one: a closure whose domains each declare amain_pipeis refused by/build/*but runs happily, so the field names the pipe that would run. It rides the valid arm only — the invalid arm assembles no library. Seedocs/pipe-validate.md→ "The effective entry pipe".
Fixed
- The per-pipe build routes honor a fetched package's manifest
main_pipe.POST /v1/build/{inputs,output,runner}with an address-formmethod_refand nopipe_refused to answer422("the closure declares nomain_pipe") whenever the package's domains declared none themselves — even though itsMETHODS.tomlnamed an entry pipe, which the run routes already honored. The tooling routes now default the pipe with the run routes' precedence: the request'spipe_ref, then the fetched manifest'smain_pipe, then the closure's own declaredmain_pipe(still a422when that declares none, or several). Inlinefiles[]requests carry no manifest and behave exactly as before. A manifestmain_pipenaming a pipe the closure does not contain is a422that says where the selector came from./resolveand/codegenselect no pipe and are unchanged. The OpenAPI artifact'spipe_ref/requested_pipe_refdescriptions are regenerated to say so.
[v0.21.0] - 2026-08-29
Added
method_refon the wire — run a published method by address.POST /v1/executeandPOST /v1/startacceptmethod_refas a third run source, mutually exclusive withmthds_contentsand with afiles/bundle_b64bundle. An address-form reference —github.com/<owner>/<repo>, an optional package selector, an optional@<tag>(tags only; branch names are refused) — is fetched server-side, the package located by manifest identity, and its files materialized as the run's library. The entry pipe defaults to the manifest'smain_pipe;pipe_codeoverrides it. The/executeresponse, the/startack, and the run logs carrymethod_provenance— the resolved{address, tag, commit_sha}— so a run by reference is always attributable to an exact commit. Seedocs/pipe-run.md→ "Running a method by address"./v1/validateacceptsmethod_refnatively.mthds_contentsis now optional there: exactly one ofmthds_contentsormethod_refselects what is validated. A fetched package's real relative file names feedmthds_sources, so per-file attribution invalidation_errors[]names the package's own files. Selector-resolution failures (parse, fetch, no package found) are non-2xxproblem+json— never anis_valid: falseverdict.- Tooling routes resolve address-form
method_ref.POST /v1/resolve,POST /v1/codegen, andPOST /v1/build/{inputs,output,runner}now resolve an address-formmethod_refthrough the same fetch path (only.mthdsdata travels — a package's Python never loads on these routes). The registry form (any non-address reference) keeps the501MethodRefNotSupporteduntil a method registry exists. - SHA-keyed clone cache. Fetched repositories are cached on disk per resolved commit SHA — a
git ls-remotepre-resolution turns the reference into a SHA before any clone, so a cached commit is never re-cloned and a moved tag always fetches fresh (never cached by tag alone). The cache is bounded by count, total bytes, and age, tunable viaMETHOD_CACHE_DIR,MAX_METHOD_CACHE_CLONES,MAX_METHOD_CACHE_TOTAL_KIB, andMAX_METHOD_CACHE_AGE_HOURS(seedocs/configuration.md). - Execution-locus security gate for fetched packages. On a deployment that is not sandbox-hosted, a fetched package shipping any
.pyis refused with403CustomCodeRequiresSandbox. On a sandbox-hosted deployment, PipeFunc.pyis accepted (captured as text, executed in the sandbox), but a package declaring Python structure classes is always refused with403MethodStructuresRefusedError— structures would import into the runner's own process. - Distinct error surface for
method_reffailures.MethodRefParseError,MethodFetchError,MethodPackageAmbiguityError, andMethodPackageTooLargeErrormap to422;MethodPackageNotFoundErrorto404(its detail lists the packages the repository does contain);MethodStructuresRefusedErrorto403— each an RFC 7807problem+jsondocument. Documented indocs/error-responses.md.
Changed
- Pinned
pipelex0.55.0 (up from==0.54.0, exactly). It brings thepipelex.methodspackage — the reference grammar, the git fetcher with its bounds, package location by manifest identity, and the structure-refusal gate — that this release'smethod_refsupport is built on. The.pipelex/config schema did not move, so no migration is required. - OpenAPI artifact regenerated for the new surface: the
method_refrequest fields, themethod_provenanceresponse fields (including the newPipelexApiStartResponse), and the per-route403/404/501problem responses.
[v0.20.0] - 2026-08-28
Changed
- Pinned
pipelex0.54.0. Up from==0.53.0, exactly; it bringsmthds0.11.1, which now types the input-form descriptor and the pipe I/O contract models for the whole stack. The.pipelex/config schema did not move, so no migration is required. Two upstream changes reach callers of this server: - The
input_formview's published schema is now the standard's kind-discriminated union (Breaking for generated clients).PipeInputFormDescriptor.fieldsitems are aoneOfover one closed model per field kind —TextField,ProseField,DateField,NumberField,BooleanField,EnumField,DocumentField,ImageField,ObjectField,ListField,UnknownField— discriminated onkind, with a parallel*Itemfamily for list items. The flatInputFormFieldand theFieldKindenum leave the artifact, so a client regenerated fromdocs/openapi/pipelex-api.openapi.yamlloses those type names and gains the per-kind ones. The JSON on the wire is unchanged; hand-written clients are unaffected. The arms publish their full closed shapes (additionalProperties: false, real properties), asmthds0.11.1 fixed the serializer annotation that previously erased them to opaque objects. - A count of one is strictly singular (Breaking). Upstream,
Concept[1]is universally the single form: a[1]input takes the value itself and refuses a list, a[1]output produces one object rather than a one-item list, andConcept[0]is rejected at validation time. Methods executed through this server that relied on[1]list behavior must be updated. - OpenAPI docs — Regenerated
docs/openapi/pipelex-api.openapi.yamlfor the pin. Beyond theinput_formunion above, four previously opaque schemas —ConceptBlueprint,ConceptStructureBlueprint,ConstructBlueprint,InputSlotBlueprint— now publish their real closed shapes instead of{"additionalProperties": true}, another consequence of the same upstream serializer fix.
Added
- Contract test for the
input_formunion.tests/unit/test_openapi_contract.pynow pins thatPipeInputFormDescriptor.fieldsis discriminated onkindwith exactly the standard's kind set, that every arm is a published closed schema with real properties, and that the flatInputFormField/FieldKindnever resurface — so an upstream regression to the flat or opaque shape fails here instead of shipping silently.
[v0.19.0] - 2026-08-26
Added
- New validation error type — Added
input_presence_vacuoustoPipeValidationErrorType, an advisory lint that warns when a required method input points to a concept with no required fields. It is scoped to the bundle's declaredmain_pipe. This does not affectis_valid, but consumers performing exhaustive matches over the enum must handle the new member. (Breaking)
Changed
- Dependency — Pinned
pipelexto0.53.0(up from0.52.0). The.pipelex/config schema did not move, so no migration is required. - Warning output — Intent-hint warnings are now capped at five unknown-hint-key findings per site, collapsing the rest into an
...and N moreitem; hint keys or intent words longer than 60 characters are elided with an ellipsis. - OpenAPI docs — Regenerated
docs/openapi/pipelex-api.openapi.yamlto include the newPipeValidationErrorTypemember and updated docstring references (moved frompipelex.pipeline.optionality_warningstopipelex.pipeline.advisory_warnings).
Fixed
.mthdsparsing — An empty[concept.X.structure]table now parses as an object with no fields instead of being described as prose refiningnative.Text.- Python class reflection — Registered Python classes declaring no fields now reflect as empty objects rather than
unknown. - Domain locators — Hierarchical domains (e.g.,
legal.contracts) are now split at the last dot rather than the first when building an advisory warning's locator, so the locator names the correct domain.
[v0.18.0] - 2026-08-24
Added
- New
viewsparameter on/v1/validate.ValidateRequestnow acceptsviews: list[str](default[]), the structured-view counterpart torender. Tokens are resolved leniently and as a set: each is matched independently against the supported set, unknown or unsupported tokens are silently dropped and never trigger a 422, and order and duplicates do not matter.input_formis the only supported token today, attached exclusively to the valid arm of the response. The two lists are independent — a request may carry both, and a token does not cross axes. - Documentation.
docs/pipe-validate.mdnow covers the newviewsparameter, the previously undocumentedrenderparameter (including the lenient, set-based, mutually independent mechanics they share), theorchestration_modeoverride, and the opt-in response fields (input_form,rendered_markdown). - Tests.
tests/unit/test_validate_views.pypins the gate: absent by default on both arms, present on the valid arm when requested, never on the invalid arm even when requested, unknown tokens ignored rather than rejected, andviews/renderresolving independently.tests/unit/test_protocol_parity.pygained the complementary pair — a default call omitsinput_form, and an opt-in call matches the local canonical report exactly.
Changed
input_formis now an opt-in structured view on/v1/validate(Breaking). v0.17.0 shipped it unconditionally on every valid verdict, as a side effect of thepipelex0.52.0 pin rather than a decision. It is now gated: a caller that does not ask no longer receives it, so the high-frequency consumers — editor hooks, CI gates, agent loops — stop paying for a form they discard. An empty or unknownviewslist changes nothing either, so the default body is byte-identical to a request that omits the field entirely. Callers that need it must send{"views": ["input_form"]}, which returns exactly what v0.17.0 returned unconditionally. In the published OpenAPI artifact,input_formleavesValidReport'srequiredset to reflect the opt-in wire behavior; the canonical report still requires it internally, so a backend that forgets to derive it fails loudly rather than shipping an empty view.
[v0.17.0] - 2026-08-24
Added
- A source-less request now names the extension args this deployment does not handle. A body that carries no run source this server understands, but at least one key it does not handle, gets a
422naming those keys instead of the generic precondition text. The canonical case is a hosted client pointed at an open-source runner:{"method_id": "mt_…"}used to produce an obscure "pipe_code and mthds_contents cannot both be empty", and now explains that a hosted-only selector must be sent to the hosted API, which resolves it into a run source before any runner sees the request. The wording is deployment-neutral on purpose — the server reports what it handles rather than asserting anything about the caller's topology. Keys it does handle (pipeline_run_id,callback_urls,orchestration_mode,storage_scope, and the legacy singularmthds_content) are never named as unhandled, and a source-less body carrying only those keeps the base guidance. No wire-shape change and no OpenAPI artifact change.
Changed
-
Pinned
pipelex0.52.0 (Breaking). Up from==0.50.0, exactly, andmthdsfollows to 0.8.2 transitively. No code changed on this side — the report is assembled inside pipelex — but the published/v1/validatecontract moved in several places: -
ValidReportgained a requiredinput_form, mapping eachpipe_refto aPipeInputFormDescriptor, so a caller can render a form from the verdict alone instead of reverse-engineering one from the emitted JSON Schema. The closedFieldKindvocabulary arrives with it. A consumer deserializing the report strictly must add the member. PipeInputContract.optional(boolean) is replaced bypresence, a three-valuedPresenceMarker—plain,optional(?) orforce(!) — so a!use-site assertion is now visible where it previously read as an ordinary required slot. Readpresence != "plain"where you readoptional == true.IOMultiplicitygainedfixed, and both the input and the output contract now carrymultiplicity, with anitem_countpresent exactly when it isfixed. A fixed-count list slot (Concept[N], forN > 1) used to reportvariableand lose its count. Any consumer branching onvariableto mean "a list" must acceptfixedas a list too.- Every pipe blueprint's
inputsmap widened fromstringvalues tostring | InputSlotBlueprint. PipeValidationError.error_typegained the advisoryHintLintErrorTypemembers (hint_unknown_key,hint_unknown_intent,hint_inapplicable_intent). They ride the report'swarningsand never make a verdict invalid, but an exhaustivematchover the union has to handle them.
Two authoring changes reach anyone posting .mthds content: an unknown key in a concept structure-field table is now rejected at parse instead of silently dropped, and required = true may no longer be paired with default_value. The .pipelex/ config schema did not move, so no migration is required.
- Four schemas in the OpenAPI artifact went opaque, upstream.
ConceptBlueprint,ConceptStructureBlueprint,InputSlotBlueprintandInputFormFieldnow publish as a bare{"type": "object", "additionalProperties": true}. This is a side effect of how intent hints are serialized, not an intentional loosening: each model gained a@model_serializer(mode="wrap")returningdict[str, Any]in order to drop absenthintsrather than emitnull, and pydantic derives a model's serialization schema from that annotation — which is the one FastAPI publishes. The validation-mode schema is still complete and stillextra="forbid". The artifact is regenerated as-is rather than hand-patched, and the issue is filed upstream.
Fixed
- A source-less run request no longer slips through on an unknown key.
RunRequest.validate_requestused to waive the "at least one ofpipe_code/mthds_contents/ method bundle" precondition whenever the body carried any key outside the declared fields, on the theory that an extension might be the method selector. That theory is obsolete under the layered extension policy written down in the Pipelex workspace specdocs/specs/pipelex-platform-api.md: an extension-borne method selector is resolved by the layer that owns it before the request reaches this server, so a body arriving here with no run source is an error whatever else it carries. The waiver was also already dead on every live path — the run routes build the model throughRunRequest.from_body, which copies the declared fields only, so unknown keys were stripped before the validator ever saw them. Removing it is therefore behaviour-neutral, and the substantive change is the message described above. A body that does have a real run source still accepts unknown extension keys untouched: the model stays extension-open.
[v0.16.0] - 2026-08-20
Changed
- Built on
pipelex'sRunMetadatasplit (Breaking).JobMetadatamoved its run-constant half —user_id,pipeline_run_id,storage_scope,request_id— into a nestedRunMetadata, reached asjob_metadata.run_metadata.*. This server's own surface is untouched:pipeline_run_setupstill takes those four as flat keyword arguments and builds theJobMetadataitself, so no route, request model or response shape changes. Only the test doubles that construct aJobMetadatadirectly had to follow.
The wire contract is unchanged, including the published OpenAPI artifact — see below for the one place that was nearly not true.
- Pinned
pipelex0.50.0. Up from==0.47.0, an exact PyPI pin as this dependency is meant to be expressed. It was briefly a[tool.uv.sources]git rev whileRunMetadatawas unreleased; that is gone, and with it the transitive override a source imposes.
The jump crosses three releases, so it picks up more than the RunMetadata split: PipeFactoryErrorType / PipeValidationErrorType moved to pipelex.validation_error_types (0.49.0), and the import follows them.
Fixed
PipeOutputgained a field upstream, andtest_openapi_contractearned its keep.pipelexfirst carried the run's job onPipeOutputas a model field.PipelexApiExecuteResponse.pipe_outputreferencesPipeOutput, and/v1/executereturnsresponse.model_dump(...)— so that field publisheduser_id,request_id,otel_contextandtrace_contextinto this public API's response body and its committed OpenAPI schema.
test_execute_publishes_the_tokens_usage_wire_records asserts {"LLMTokensUsage", "ImgGenTokensUsage", "JobMetadata"}.isdisjoint(schemas), and it failed. Upstream now carries the value as a private attribute behind a property — readable by a transport resolving a storage scope, absent from model_dump, model_json_schema and therefore the wire. No change was needed on this side, which is the right shape of fix: the artifact was never supposed to have to trim it.
[v0.15.1] - 2026-08-20
Changed
- Pinned
pipelex0.48.0. Up from==0.47.0, exactly. A quiet bump for this server: nothing in it reaches the wire, the committed OpenAPI artifact is unchanged, and no tracked configuration moved. The release is upstream test infrastructure — a canonical MTHDS test corpus that now ships inside thepipelexwheel atpipelex.test_extras.mthds_corpus, with a loader API (iter_entries(),get_entry()) for selecting fixtures by tag, execution tier, validity and granularity.
Worth knowing rather than acting on today: that corpus exists so consumers stop keeping their own copies of language-level .mthds methods, which is a duplication this repo's own tests currently have. Adopting it is a separate change, and this pin is what makes it available.
[v0.15.0] - 2026-08-20
Added
storage_scope— the runner is told where to write instead of deriving it.pipeline_run_setuprequires it, and this server never passed it, so every/startand/executeagainst a currentpipelexdied withTypeError: pipeline_run_setup() missing 1 required keyword-only argument: 'storage_scope'— a 500 from the runner and a 502 at the caller, on the happy path.
It arrives in the body, not a header, because it is data rather than identity: the runner needs to know where to write, not who to trust. A multi-tenant host computes it where it knows its own tenancy (hosted Pipelex sends <org_id>/<method_id>/<run_id>) and this server treats it as an opaque prefix, composing its own leaves (assets/, results/, payloads/) onto it. Validated at the wire by validate_storage_scope, so a traversal is a 422 naming the field rather than a 500 from deep inside the run — the runtime seam validates too, so this is a second gate, not the only one.
_validate_extras is a key ALLOWLIST rather than a passthrough, so the field had to be added there too — a key missing from it is dropped with no error, and the run would have written under the fallback prefix while answering 202. Covered by tests that assert the constructor kwarg, not just the status code.
Omitted, a run is scoped to the caller's own id — never a shared constant. That is the safe default for a single-tenant deployment, and it keeps a multi-tenant deployment that forgets to send a scope isolating its callers instead of pooling them. A shared literal here would be the anonymous/ bug in a new spelling.
Fixed
-
uri_formatin the shipped.pipelex/pipelex.tomlrenders the filename only. The placeholder set narrowed again: the storage prefix ({storage_scope}plus thegenerated/leaf) is composed in code so a config cannot omit it, leaving{hash}and{extension}. A config still naming{storage_scope}is refused at boot. -
Every run's output was written to
<scope>/results/results/. The delivery target was built asStorageTarget(key_prefix="results"), from the layout where the executor composed{user_id}/{key_prefix}{pipeline_run_id}and the caller supplied the leaf. It now composes{storage_scope}/{key_prefix}results— the runtime owns theresults/leaf — so passing it here applied it twice. Nothing failed: the write succeeded, the run reported COMPLETED, and only a later reader of<scope>/results/found an empty prefix, which is exactly where the hosted platform's run reconciler looks.key_prefixremains the caller's slot for an extra level between the scope and the leaf; it is not where the leaf comes from. -
uri_formatin the shipped.pipelex/pipelex.tomlstill used the removed{primary_id}/{secondary_id}placeholders. The supported set is now{extension},{hash},{storage_scope}, so config validation raisedStorageConfigErrorat fixture setup and every test in the suite errored — a total red that looked like a code failure and was purely stale config. Migrated to{storage_scope}/{hash}.{extension}, matching the kit default inpipelex.
Changed
- Pinned
pipelex0.47.0 (Breaking, and it moves this deployment's own config files). Up from==0.46.4, exactly. 0.47.0 is the release that makes the runtime told where a run's bytes go instead of deriving it from who ran it, which is what thestorage_scopework above is built on — so this server cannot run on 0.46.x at all:pipeline_run_setupnow requiresstorage_scope, and calling it without one is aTypeErroron the happy path, not a degraded mode.
It also narrows uri_format, and a config that has not moved fails at boot rather than at first use. The format now renders the FILENAME only: the prefix is composed in code as {storage_scope}/generated/, so {storage_scope} is no longer a supported placeholder and {primary_id} / {secondary_id} stay retired. The tracked .pipelex/pipelex.toml here is already on {hash}.{extension}; anyone running this server against their own .pipelex/ must make the same edit, or boot raises StorageConfigError naming the offending placeholder and listing the supported set. There is deliberately no migration for it — a value-domain narrowing on a free string can never be proven stale from the schema, so the failure is loud at boot instead of a silent rewrite of a value an operator may have chosen.
Generated bytes consequently land under a generated/ leaf beside assets/ (what the caller supplied) and results/ (the delivered envelope), rather than loose at the root of the scope.
- Breaking: the
anonymouscaller no longer exists.ANONYMOUS_USER_IDis gone, along with every branch that produced or tolerated it. It was reached by fallback:_get_user_idreturned the literal"anonymous"whenever no identity had been established, and that string became the first path segment of every storage key the run wrote. A deployment serving many callers therefore put all of them in one namespace where each could read the others' outputs — and it looked like a working request the whole way through. A silent multi-tenant collision is the worst possible failure for a fallback to produce.
Two changes replace it.
TRUST_FORWARDED_IDENTITY_HEADERS=true now rejects a request with no forwarded id (401). Turning that flag on is a deployment asserting "a proxy in front of me authenticates every caller". A request arriving without the header means that proxy is absent, misconfigured or bypassed, and continuing under a shared owner is exactly the degradation this removes. Previously both an absent header and the literal anonymous were read as "the proxy says this one is anonymous" and let through. This is the hosted configuration, so on a hosted deployment an unidentified request is now impossible rather than merely unlikely.
A deployment that declares no user model gets a named single tenant, not an unknown caller. AUTH_MODE=none without a trusted proxy, or the shared static api_key, establishes no per-caller identity by configuration — that is one tenant, and SINGLE_TENANT_USER_ID names it. This is not the old sentinel renamed: nothing falls back to it, it is used only where the deployment has said it has no users, and verify_jwt refuses a token claiming it (so an authenticated caller on a server that does have users cannot land in that namespace).
Removed
POST /v1/uploadandPOST /v1/resolve-storage-urlare gone (Breaking). Both were explicitly non-contract — neither the MTHDS Protocol nor the Pipelex API extensions ever included them, and both carried a "slated for replacement by the storage redesign, do not build new integrations on it" notice in their descriptions. That redesign has landed, and the routes moved to the hosted platform.
They could not stay. Both keyed S3 objects by the caller's user_id ({user_id}/assets/{uuid}.{ext}) and authorized a read by comparing that first path segment against the requester — an ownership model with no notion of a team. This server has no organization concept to fix it with, which is precisely why the routes belong on the hosted side, where membership is already resolved before the request arrives.
A self-hoster who was using them needs their own upload path. The storage provider is untouched: get_storage_provider(), pipelex-storage:// URIs and everything the runtime does with them work exactly as before — only the two HTTP routes are removed. ErrorType.INVALID_URI, UPLOAD_FAILED and PRESIGN_FAILED are removed with them, and api.security.is_safe_user_id stays (the pipeline routes use it).
[v0.14.0] - 2026-08-18
Changed
-
Pinned
pipelex0.46.4 (Breaking, and it moves this deployment's own config files). Up from==0.45.0, exactly. 0.46.0 reshapes thepipelex.tomlroot to mirror the runtime layers and drops the redundant_configsuffix from keys —[pipelex]splits into[runtime]/[interpreter],[cogt]becomes[inference]— so the tracked.pipelex/files in this repo were migrated in place withpipelex migrate, which kept every value this deployment had chosen and carried each section banner along with the table it introduces. Anyone running this server against their own.pipelex/must runpipelex migrateafter upgrading, including on the inference backend files: 0.46.0 removesprompting_target, and a surviving per-model one fails the strict boot by name. Migrate with 0.46.1 or later: 0.46.0's migrator left a moved table's banner behind, so a file it rewrote needs its comments tidied by hand — or its.bakrestored and the migration re-run. The pin names 0.46.4 rather than 0.46.0 for a run of fixes that ride along and change nothing on this API's wire: 0.46.1 restores the migrator's comment fidelity, 0.46.2 keeps a.gitignoreinside.pipelex/so the timestamped backups a migration leaves behind stop showing up as untracked files, 0.46.3 makes that.gitignorereach a machine that had nothing to migrate (0.46.2 wrote it only on a run that actually carried a file forward, which is the minority case), and 0.46.4 is pipelex's own dogfooding of the convention plus a fix to its config-sync gate — developer tooling in that repo, invisible here. This repo's root.gitignorealready ignored*.bak.*, so the tracked.pipelex/here is unaffected either way. -
The runner tolerates a stale-but-migratable config the way pipelex's own boot does.
_resolve_http_error_mappersloaded the raw config dict and validated it itself, which bypassed the boot tolerance 0.46.0 added — a config whose drift the migration ledger can fully explain. Because that call resolves at module import, the app would have died on the import line over a filePipelex.makeaccepts moments later inlifespan, with the app-construction traceback instead of the "runpipelex migrate" warning. It now goes throughconfig_manager.load_config_validated, the same tolerant entry point the boot uses. A config the ledger cannot explain still fails the app fast, as before. -
Swept onto
build_registrar's newboot_orchestratorparameter. 0.46.0 removed theplugins.boot_orchestratorconfig key: the boot orchestrator is a boot argument now, and an orchestrator plugin gates its hub-slot claims on the value the registrar was built with._resolve_http_error_mapperspassesNone, which is correct rather than merely convenient — the gate governs only the slot claims, which this throwaway registrar never applies, while a plugin contributes its HTTP-error mapper unconditionally. So the resolved map is what it always was, and the import-time resolution stays honest. -
Config access moved with the keys.
get_config().pipelex.pipeline_execution_configisget_config().interpreter.pipeline_execution— the only reader of the config tree in this repo. -
OpenAPI artifact refreshed, and the suggested-fix op vocabulary changed shape (Breaking on the wire). A
suggested_fix'sops[]were a single openFixOpcarrying aFixOpKindenum beside optional fields; 0.46.0 makes them a union discriminated onkind, one member schema per kind (SetKeyOp,EnsureTableOp,DeleteKeyOp,DeleteTableOp,RenameTableKeyOp, and the two newMoveKeyOp/RemapValueOp). A client readingkindand branching is unaffected in substance; one that validates against the publishedFixOpschema must move to the union. Also in the artifact:PromptingTargetand everyprompting_targetfield are gone, andPipeLLMgainstemplating_style— 0.46.0 makes prompt templating an authoring decision on the pipe rather than something inferred from the model, with the runtime default now XML tags.
Fixed
docs/error-responses.mdno longer carries the "Suggested fixes" section twice. The whole section — prose, example, field list and op table — was duplicated verbatim; a reader hitting the second copy had no way to tell which was current. One copy remains, and it documents the discriminated-union op shape and the full seven-kind vocabulary.
[v0.13.0] - 2026-08-14
Changed
- Pinned
pipelex0.45.0 (Breaking, and a hard pairing). Up from==0.43.1, exactly. The pin crosses two releases, and the code changes below come from the first of them, not the one the pin is named after.
0.45.0 adds per-node token usage, cost and model attribution to the run GraphSpec: NodeSpec gains a usage object, GraphSpec gains a run-level usage rollup, and each node's usage carries by_model — the model that actually ran, read off the usage records rather than off the authored choice or an unresolved alias. Both models are extra="forbid", which is what makes the pairing hard in the other direction — an older pipelex rejects the new JSON — so this repo and pipelex 0.45.0 must move together. graph_spec is on this API's wire (POST /v1/execute carries one, POST /v1/validate a best-effort one) and is validated back from model_dump(mode="json") against strict=True models, so the new fields reach docs/openapi/pipelex-api.openapi.yaml, regenerated here.
0.45.0's other two changes do not reach this repo: a PipeCondition dry run is now reproducible (it sorts its branch walk instead of iterating a hash-ordered set), and a {concept, content} envelope carrying an empty list is now a value rather than an error — the latter only widens what an input may be, so no caller that worked before stops working.
- Swept onto automatic search scope (Breaking). 0.44.0 replaced the
search_domain_codeslist across the public API (PipelexMTHDSProtocol,pipeline_run_setup,InputShaper,WorkingMemoryFactory) with a singlesearch_scopestring thatpipeline_run_setupderives from the entry pipe itself, soApiRunner.startno longer passes one. Caller-visible in how a request's inputs resolve their concepts: the scope is now the entry pipe's own domain —alias->domainwhen the entry pipe comes from a dependency package — rather than whatever the runner supplied, and every entry-lookup refusal (invalid string, miss, ambiguity) now arrives asConceptLibraryConceptNotFoundError.
Fixed
- A bare
pipe_refresolves again on the build routes. 0.44.0 made in-body pipe references strict: the resolver behindget_required_pipeis a key lookup with deliberately no bare-code search, because a reference that can find a pipe in a domain its author never named is one[exports]cannot constrain.resolve_requested_pipewas still on it, soPOST /v1/build/inputs,/v1/build/outputand/v1/build/runneranswered422 Pipe '<code>' not found in the submitted closurefor every barepipe_ref— a closure declaringsmoke.echorefused"echo", the spelling the route's own contract promises to accept and echo back qualified.
A code a human typed at an entry point is a different question from a reference one pipe makes to another, and 0.44.0 gave it its own answer: get_required_entry_pipe matches a bare code across every domain the library holds, ignores [exports] (package visibility governs in-body references, not hand-typed entry points), and excludes aliased dependency entries so installing a package cannot make a host pipe's code ambiguous. The route now uses it. An ambiguous bare code raises rather than picking a winner, and still surfaces as a 422 naming the candidates.
[v0.12.0] - 2026-08-12
Changed
- Pinned
pipelex0.43.1 (Breaking, and a hard pairing). Up from==0.42.0, exactly. The two releases must move together in both directions: 0.43.0 retires the singlepipelex.pluginsentry-point group, so a plugin still declared under it now fails the boot withRetiredPluginEntryPointGroupErrorrather than going quietly undiscovered — while 0.42.0 does not read the new groups at all and would discover no plugin published under them. The rest of 0.43's breaking surface does not reach this repo:ImageReference/DocumentReferencemoved topipelex.kernel.prompt_references,LLMPromptBlueprintValueError/ImgGenPromptBlueprintValueErrorunified intoPromptContentError,StructuredContentComposerdroppedpipe_run_params,InputShapernow resolves through its injected provider, and 0.43.1 madePipeRunParams.batch_max_concurrencyrequired — none of those symbols has an import site here. - OpenAPI artifact refreshed. The committed schema tracks pipelex's own models, and 0.43.1 reworded the
CodegenTargetdocstring. Description text only — no route, field, type or requiredness moved, so the wire contract is unchanged. - Swept onto the plugin entry-point group split.
pipelexreplaced its singlepipelex.pluginsentry-point group withpipelex.plugins.kernelandpipelex.plugins.interpreter, andbuild_registrargained a requiredentry_point_groupsparameter naming which of them to read._resolve_http_error_mapperspassesENTRY_POINT_GROUPSfrompipelex.interpreter_plugins.builtins— the composed both-group list, matching theBUILTIN_PLUGINS/CORE_UNCONDITIONAL_PLUGIN_NAMESit already injects and thepipelex plugins listdiagnostic this function is modelled on. The runner boots the full interpreter, so it must read both groups: reading the kernel group alone would silently drop an installed orchestrator plugin's HTTP-error mapper and leave transport faults unmapped. The parameter is required rather than defaulted, so this omission would have surfaced as an import-timeTypeError— the call site resolves at module scope, taking the whole app down on the nextpipelexbump rather than degrading one feature.test_exception_handlers.pyfollows the matchingregistrar.begin_pluginsignature change, passinggroup=None.
[v0.11.1] - 2026-08-03
Fixed
- The catch-all 500 now honors
ERROR_DISCLOSURElike every other response.handle_unexpected_errorhard-coded"An unexpected error occurred. The request id is included for support."and never read the disclosure mode, so averbosedeployment — the default, and what every deployment runs, since the var is unset everywhere — got real messages for every classified error and a dead end for the one error nobody classified. That is backwards: an unclassified failure is precisely the one a caller cannot diagnose from a request id alone. A rawpydantic.ValidationErrorescaping input building (it is not aPipelexError, so no handler claimed it) landed here, and its reason was reachable only in CloudWatch.
Under verbose, detail is now "<ExceptionClass>: <message>". Under strict the body is byte-identical to before — a deployment that wants nothing leaked keeps that, and provenance-gated redaction (_authors_caller_facing_message) is unchanged. A traceback still reaches the client in neither mode; the class name and full traceback continue to go to the operator log, correlated by request id. Docs: docs/error-responses.md → "Disclosure modes".
[v0.11.0] - 2026-08-01
Added
- Storage transport documentation (
docs/storage-transport.md): documents the/uploadand/resolve-storage-urlroutes — their non-public contract status, their role as the current transport behind the SDKs' input-preparation surface, the two-layer size-limit enforcement (request-level422vs. post-decode413), and the planned migration topipelex-platform. Linked fromdocs/index.mdand added to themkdocs.ymlnav. - Upload size-limit test:
test_decoded_payload_over_cap_returns_413covers the narrow window where a base64 payload clears the request'smax_lengthvalidation but decodes overMAX_UPLOAD_BYTES— the only path that yields a413.
Changed
- Breaking — swept onto
pipelex's 0.41.0 restructuring. The release deletedpipelex.hub, moved the MTHDS parser and the Pipe machinery out ofcore/, and relocated a set of types to the packages that own them; every import is re-pointed at the address that now defines the symbol. - The hub split.
pipelex.hubis gone with no shim, split intopipelex.runtime_hubandpipelex.interpreter_hubby owning layer. This falls out neatly along the route boundaries: the storage and uploader routes takeget_storage_providerfrom the runtime hub and the pipeline route takes the orchestrator/bundle-validator registries from it too, while the build-runner and crate-ops routes take the library and pipe accessors from the interpreter hub. build_registrarnow requires its built-ins to be injected. It no longer imports them, because the built-in list spans both layers and importing it inside a runtime-layer module would put the method interpreter back into every runtime closure._resolve_http_error_mapperspassesBUILTIN_PLUGINS/CORE_UNCONDITIONAL_PLUGIN_NAMESfrompipelex.interpreter_plugins.builtins— the composed both-layer lists, matching thepipelex plugins listdiagnostic this function is modelled on. The discovery function stays pure and repeatable, so resolving the mapper map at import is still safe.- Address moves:
PipelexBundleBlueprint→pipelex.mthds_parsing.pipelex_bundle_blueprint;PipeAbstract→pipelex.pipe_machinery.pipe_abstract; the input/output renderers →pipelex.pipe_machinery.rendering.*;PipelexBundleBlueprintValidationErrorData→pipelex.core.exceptions;JobMetadata→pipelex.system.job_metadata. - The dotted pipelex class paths in
pyproject.toml'sruntime-evaluated-base-classes(PipeOutput,PipelexRunResultExecute) were checked against 0.41.0 and did not move. - Breaking — wire-visible error rename:
PipelexInterpreterErroris nowMthdsParserError. The class name is theerror_typevalue on the wire, so this changes what clients see —docs/error-responses.md(the caller-facing-detailallowlist) and the webhook-recovery test fixture are updated to match. - Dependencies: Bumped
pipelexfrom0.40.0to0.42.0.
Fixed
docs/error-responses.mdhad a duplicated "Suggested fixes" section. The 0.41.0 sweep accidentally duplicated the section (originally documented in v0.9.0) while moving surrounding content; removed the duplicate, no content change.
[v0.10.0] - 2026-07-19
Changed
- Breaking:
/executenow returnspipe_output.tokens_usagesas client wire records (TokensUsageRecord), not dumps of pipelex's internal usage models. The route applies pipelex'sapply_tokens_usage_wire_shapeto the response dump, so each record is flat and closed:model_type, model name/id,pipe_code,job_category,unit_job_id,nb_tokens_by_category, computed USDcost(nullwhen the model has no rate table), ISOstarted_at/completed_at. Runtime internals (job_metadataand its contexts/ids,unit_costs) no longer cross the wire. Null semantics unchanged (null= usage assembly off,[]= no inference). Documented in Pipe Run. - The OpenAPI artifact now publishes the wire shape it actually emits.
/executereturns aJSONResponsebuilt from the trimmed dump, so FastAPI never serializes throughresponse_model— the declared 200 body was still the internal usage union (LLMTokensUsage | ImgGenTokensUsage | …with requiredjob_metadataandunit_costs), and a client generated from the artifact would have rejected every real response carrying usage data. The route now declaresPipelexApiExecuteResponse(the run result whosepipe_output.tokens_usagesisTokensUsageRecord[]), so the published schema matches the body. The internal usage models leave the artifact entirely;tests/unit/test_openapi_contract.pypins the published record alongside the runtime guard, so the two fail together if either drifts again. - Dependencies: Bumped
pipelexfrom0.39.1to0.40.0. This release carriesapply_tokens_usage_wire_shapeand theTokensUsageRecordwire shape the/executechange above depends on. (It supersedes the interim0.39.2bump — which unblocked the hosted runner resolving against a git-sourceddev— with a released PyPI pin.)
[v0.9.1] - 2026-07-15
Changed
- Dependencies: Bumped
pipelexfrom0.39.0to0.39.1.
[v0.9.0] - 2026-07-14
Added
POST /v1/resolve(MTHDS Protocol resolution capability): resolve a library closure into its normalized library crate over HTTP. Request selects the closure with inlinefiles[](each{content, source?}) XOR amethod_ref; a produced verdict is a200discriminated onis_valid— the canonical JSON crate (qualified refs, flattened refinement, materialized natives, fingerprint) on the valid arm, the structuredvalidation_errors[]on the invalid arm.method_refis accepted by the envelope but answers501(MethodRefNotSupported) until server-side method-registry resolution exists.POST /v1/codegen(MTHDS Protocol type-projection capability): project the crate into typed artifacts with the two explicit axes —kind(types) ×target(ts-zod,python-pydantic, and the Pipelex-extensionpython-structures). The valid arm carries the stamped artifact set plus itscodegen.lock: a client that writes them verbatim reproduces a localpipelex codegen typesrun byte-for-byte and passes the offlinepipelex codegen check. An unknownkind/target(or apipe_refon the concept-set-widetypes) is a request-shape422problem+json. There is deliberately no server-side check route (the drift check is offline by design), and noinputskind: served-kind membership follows the stamp/lock trust chain, and an inputs template is a user-editable scaffold that is never stamped or locked — it ridesPOST /v1/build/inputsinstead.- Docs: new Resolve & Codegen page; Pipe Builder rewritten to the new envelopes.
-
postman-bundleskill (renamed frompostman-run-bundle) now covers the whole bundle surface: besides/v1/execute,/v1/start, and/v1/validate, the skill and itsbuild_postman_query.pyscript target/v1/resolve,/v1/codegen(with--target ts-zod|python-pydantic|python-structures), and/v1/build/{inputs,output,runner}(with--output-formatforbuild/output, and--inputs-format json|toml+--explicitforbuild/inputs). New make targetsbundle-resolveandbundle-codegen; the build routes ride the generic targets viaENDPOINT=. Postman pushes now land under aBundles/top folder (wasRun Bundle/) and merge by request name instead of replacing the whole per-bundle subfolder, so requests for different endpoints coexist. -
suggested_fixon validation errors (from pipelex codegen): avalidation_errors[]item may now carry a structured, deterministic repair the runtime's fix planner derived from the typed error data (never by parsing a message string) —fix_code(kebab-case rule id),description,safety(safe|unsafe), an optionalsource, andops[]: semantic TOML patch operations (set_key,ensure_table,delete_key,delete_table,rename_table_key) addressed bytable_path. The ops are the machine contract; any rendered diff is presentation. Optional and additive — a non-fixable item omits the field and a client that ignores it is unaffected. It rides every surface that carries the items: the200invalid arms of/v1/validate,/v1/resolve,/v1/codegen, and/v1/build/*, and the RFC 7807 problem documents. Documented in Error Responses → Suggested fixes.
Changed
pipelexis pinned to a git rev whilecodegenis unreleased. The dependency source moved from a local editable path (../pipelex) to a git rev on thepipelexrepo. The editable path only ever resolved on a workspace checkout — it failsuv syncanywhere else, CI included. The pinned rev carries the unreleased codegen engine this API rides; it reverts to a plain==<version>PyPI pin once apipelexrelease ships it.- The OpenAPI artifact now documents the real error contract. Every failure this server emits is an RFC 7807
application/problem+jsonproblem document, but the published artifact said otherwise: it advertised FastAPI's defaultHTTPValidationErroronapplication/jsonfor the 422 of nearly every route,/v1/executeand/v1/startdocumented no failure at all, and no route documented 401/403/409/413/429/500/501. Now every/v1operation documents401/413/422/500as a typedProblemDocumentunderapplication/problem+json, plus the statuses it alone can produce —/execute:403,429(withRetry-After);/start:400,403,409,501;/validate:403;/resolve+/codegen:501.GET /v1/versionstays public and 401-free.HTTPValidationErroris gone from the artifact entirely. - Breaking —
/v1/resolveand/v1/codegenare no longer taggedx-mthds-protocol. They are Pipelex API extensions, not MTHDS Protocol operations: the protocol is, and stays, the five routesexecute/start/validate/models/versionthat the standard's own normative OpenAPI defines. The flag is how a conformance suite or a third-party runner extracts the portable subset of the artifact, so tagging a Pipelex route misrepresented the standard. The crate/v1/resolveemits remains standard-owned (the MTHDS Library Crate Format) with brand-neutral wire fields — only the routes' protocol membership changed, and no request or response shape moved.tests/unit/test_openapi_contract.pynow pins the tagged set exactly, in both directions. - Docs: Error Responses gains a Suggested fixes section and the missing
missing_pipe_codefield, and its status-code list now covers the400(StartRequiresAsyncOrchestration), theorchestration_mode403, and themethod_ref501(the501bullet also named a stale/pipeline/startpath). The Overview endpoint catalog gains the Resolve & Codegen section andGET /, and corrects the uploader auth requirement —/v1/uploadand/v1/resolve-storage-urlneed an authenticated user identity, whichAUTH_MODE=api_keydoes not establish (the key is shared, not per-caller). - Breaking —
/v1/build/{inputs,output,runner}migrated onto the/validateverdict discipline: a produced verdict is now always a200discriminated onis_valid.build/inputsandbuild/outputwrap their previous bare JSON payload in the valid arm (inputs/outputfields); an invalid bundle — including a failed dry-run of the requested pipe — is now a200is_valid: falsewith the structuredvalidation_errors[]instead of a422.build/runnerdrops the retiredsuccessbool. - Breaking —
/v1/build/runnerrides the codegen types projection (D9): the generated script spells its imports, example inputs, and output cast with the emitted class names, and the valid arm now also carries thestructuresprojection it imports from (stampedstructures.py+codegen.lock, written into astructures/directory beside the script) — matching what a localpipelex build runnerscaffolds. A requested pipe recorded SKIPPED by the sweep (cross-package dependency absent from the request) remains a request-shape422. - Breaking —
/v1/build/*now ride the same request envelope as/v1/resolveand/v1/codegen: the closure selector is inlinefiles[](each{content, source?}) XOR amethod_ref(501until the method registry lands), replacing the baremthds_contents[]strings. The per-filesourcelabel means build diagnostics can finally name the owning file, and there is no second breaking migration waiting whenmethod_refresolution arrives. The pipe selector becomespipe_ref— the qualifieddomain.pipe_code, and now optional: omitted, it defaults to the closure's declaredmain_pipe, exactly likepipelex codegen inputs --pipe. A closure declaring nomain_pipe, or several across domains, cannot be defaulted, so an omittedpipe_refis a422there. Every valid arm echoes the resolvedpipe_refplus the submittedrequested_pipe_ref(absent when defaulted). The echoedpipe_refis always qualified, read back off the resolved pipe rather than off the request — a bare code still resolves through the engine's cross-domain fallback, and the caller is told the qualified ref it landed on. The old bare-mthds_contentsenvelope now survives on/v1/validatealone, which keeps it because changing a protocol route's envelope is the MTHDS spec's call, not this server's. - Breaking —
/v1/build/inputsand/v1/build/outputare static;allow_signaturesis gone from both. They now resolve their closure through the same static core as/resolveand/codegenand read the requested pipe's declared IO — no dry-run sweep, so noallow_signatures(the flag only ever parameterized that sweep). A valid verdict from them says the closure is structurally sound, not that the pipe runs; runnability stays/v1/validate's vocabulary./v1/build/runneris the deliberate exception — a runner script is a promise the pipe runs, so it keeps both the sweep and the flag. When itspipe_refis omitted the sweep cannot be scoped before the closure loads, so the whole closure is swept. - Breaking —
/v1/build/inputsgains the CLI's two rendering axes, and its payload's JSON type follows the format. Newformat(json, default |toml) andexplicit(bool, defaultfalse— the ceremonial{concept, content}envelope instead of the default light, signature-driven shape), rendered through the same engine rendererspipelex codegen inputsuses. The template rides the field its format names:inputs(a parsed object) for JSON,inputs_toml(raw text) for TOML — TOML cannot ride as a parsed object without losing the concept comments and key order that are the reason to ask for it. The unselected field is omitted from the body. A pipe declaring no inputs is a valid verdict carrying an empty template, not an error (the CLI likewise exits 0 on it).
Fixed
/v1/build/runnercould read the wrong pipe's output multiplicity. A blueprint's pipe map is keyed by bare code, and the multiplicity lookup scanned by bare code alone. In a closure where two domains declare a pipe with the same name, it returned whichever blueprint came first — so a list-output pipe could be handed a scalar runner script (main_stuff_asinstead ofmain_stuff_as_items), and vice versa. The lookup now matches the owning domain too./v1/build/outputwithformat=pythonwas a hard500. The route parsed every format withjson.loadsand typed the fielddict, so the Python-source representation crashed on arrival. The representation now rides the field its format names, mirroring/build/inputs:output(a parsed object) forschema/json,output_python(source text) forpython. Separately, a pipe whosenative.Anythingoutput has no determinable shape (whichrender_outputreports with a bareValueError) is now an honest request-shape422rather than an unhandled500.- Docs — Error Responses no longer contradicts the 200-verdict discipline. Two sections still said an invalid bundle rides a
422on the build routes, and the 422 example showedPOST /v1/validateproducing one; both now correctly attribute theValidateBundleError422 to the run routes (/execute,/start) alone, with the diagnostic routes answering200is_valid: false.
[v0.8.0] - 2026-07-06
Changed
- Dependencies: Bumped
pipelexfrom0.37.0to0.38.0. - Adapted to pipelex 0.38.0 breaking changes:
StrEnumis now imported from the stdlibenum— pipelex 0.38.0 dropped Python 3.10 and deleted thepipelex.typesre-export module that used to provide it. AndPipeSignatureis no longer a pipe type: a signature (contract-only pipe) is declared by omittingtype, so the signature test fixtures and the/validatedocs were updated to the typeless form (writingtype = "PipeSignature"is now rejected by the runtime).
Added
- Optionality surface on
/validateand working memory (from pipelex 0.38.0): the regenerated OpenAPI artifact exposes pipelex's new optionals feature —optional(?) markers on IO contracts and data edges,liftable_pipes(pipes that may be skipped when an optional input resolves absent) plus a generalwarningsarray on the validation report,skip_reasonon graph nodes, and anabsencesledger (AbsenceRecord/AbsenceKind) on the serialized working memory.
[v0.7.0] - 2026-07-04
Added
- Core: Introduced
PipelexPipeDispatchAckpayload to handle fire-and-forget job acknowledgments (returning IDs only), replacing incompletePipelexPipeRunOutputobjects for async dispatches.
Changed
- Architecture: Replaced the
DeliveryModeflag and unifiedorchestrator.run()method with explicit wait-semantics methods: synchronous/executerequests drive the blockingorchestrator.execute(), while asynchronous/startrequests drive the fire-and-forgetorchestrator.start(). - Dependencies: Bumped
pipelexfrom0.36.0to0.37.0. - Developer Tools: Streamlined
Makefilecommands andCLAUDE.mddocs: renamedmake ctomake agent-checkandmake ttomake agent-test(both silent on success), and addedmake cleanderivedto clear caches and compiled files. - Testing: Aligned test stubs (
_StubOrchestrator,_RecordingStub) and protocol conformance tests with the newexecute/startprotocol, and enhanced theexecutestub to promote input stuff to main stuff via theMAIN_STUFF_NAMEalias, enforcing the completed-memory invariant. - Documentation: Updated internal API routing comments to reference
{MTHDS_BASE_URL}instead of{MTHDS_API_URL}.
[v0.6.0] - 2026-06-30
Orchestrator-agnostic base. pipelex-api no longer hard-wires Temporal: the published base names no orchestrator and runs every pipeline in-process, while distributed execution (Temporal, Mistral Workflows, …) becomes a deployment flavor = base image + exactly one orchestrator plugin + that plugin's activation. api/ imports no pipelex.temporal / temporalio.
Added
orchestration_modedeployment config: a top-level run's backend is read from a new packagedapi.toml(orchestration_mode, defaultdirect;allow_request_orchestration_mode_override, defaultfalse), env-layered like the Pipelex config (api_{env}.toml/api_override.toml).orchestration_modeis an open string token (core ownsdirect; each orchestrator plugin owns its own, e.g.temporal); an unregistered token fails loud at dispatch.POST /v1/execute,POST /v1/start, andPOST /v1/validatemay each carry a per-requestorchestration_modeoverride, honored only when the deployment opts in — otherwise refused with a403(OrchestrationModeOverrideForbidden). The delivery axis (blocking vs fire-and-forget) is endpoint-intrinsic, never configured or requestable. See Configuration → Orchestration mode.- Orchestrator HTTP-error mappers: the app discovers each installed orchestrator plugin's transport-fault mapper (via the plugin SPI
add_http_error_mapper) at construction and registers one RFC 7807 handler per mapped exception type — so a plugin's transport fault renders correctly while the base imports no orchestrator SDK. make docker-run-hubtarget: pull and run the publishedpipelex/pipelex-apiDocker Hub image without a local checkout or build, using the same.envcontract asmake docker-run. Pin a tag withHUB_TAG=<tag>(defaults tolatest).
Changed
- Two orthogonal axes —
orchestration_mode(open token) + delivery (endpoint-set): the backend (which orchestrator) and the wait-semantics (whether the caller waits) are now distinct.orchestration_modenames only the backend and is an open string token. Delivery isDeliveryMode {BLOCKING, FIRE_AND_FORGET}, threaded into the orchestrator by the endpoint and never received from a caller:/executeand/validatedispatchBLOCKING,/startdispatchesFIRE_AND_FORGET. The conflatedexecution_modeenum (which carriedtemporal_blocking/temporal_fire_and_forget/mistral_native) is gone. /executedispatches byorchestration_mode:POST /v1/executeselects its backend from the resolvedorchestration_mode— dispatching the locally-built run job through the hub'sOrchestratorRegistrywithBLOCKINGdelivery, symmetric with/start— instead of the boot-global pipe-run slot.orchestration_modeis the single source of truth for top-level dispatch (boot_orchestratornarrows to the execution stack). A per-requestorchestration_modeoverride is gated by the same403policy as/start. The full output is preserved across the dispatch: the orchestrator's JSON-safe result is rehydrated back into the canonicalPipeOutputthe response wraps./startis HONEST about fire-and-forget:POST /v1/startbuilds the run job locally (preservingrequest_id,output_multiplicity,dynamic_output_concept_ref, run registration, and telemetry) and dispatches it through the hub'sOrchestratorRegistrywithFIRE_AND_FORGETdelivery. It checks the resolved orchestrator'ssupports_fire_and_forgetcapability before loading a library: an async-capable backend (temporal, contributed by thepipelex-temporalplugin) acks202with itsworkflow_id, while the in-processdirectbase is blocking-only and is refused with a400(StartRequiresAsyncOrchestration) — use/execute— instead of silently running blocking and acking./validatedispatches byorchestration_mode(verdict-as-value): like/start,/validateresolves the deployment'sorchestration_modeand dispatches through the hub'sBundleValidatorRegistry, returning the verdict as a value — a 200 validPipelexValidationReportor a 200 invalidErrorReport(no exception-as-control-flow at the route). Validation is inherently blocking, so there is no delivery axis here.directvalidates in-process on the API side; atemporalmode dispatches the whole job to a worker (contributed by thepipelex-temporalplugin) and assembles the same canonical report API-side from the worker's result. A per-requestorchestration_modeoverride is gated by the same policy as/start(403when forbidden). The verdict wire is byte-identical across backends. Size in-process/validatetraffic for the library load; on a worker-dispatched flavor the library work happens worker-side.- Dependencies: pinned
pipelexto the published0.36.0(pipelex[mistralai,anthropic,google,google-genai,bedrock,fal]==0.36.0), replacing the prior git-rev pin ([tool.uv.sources]removed). Dropped thetemporalextra — the base depends on no orchestrator plugin — and dropped the now-redundant directmthdsdependency (pulled in transitively bypipelex).
[v0.5.0] - 2026-06-18
Added
- Opt-in Markdown rendering on
/validate:POST /v1/validatenow accepts an optionalrenderlist of view-format tokens (e.g.,["markdown"]). When requested, the response includes arendered_markdownfield on both valid and invalid arms, matching the local CLI output. Unknown tokens are ignored to keep presentation hints separate from the verdict contract. This is supported end-to-end via a new--renderflag inbuild_postman_query.pyand aRENDERvariable inMakefile.local.mk. - Postman sample bundles: Added a suite of sample
.mthdsbundles and.inputs.jsonfiles inpostman/sample-bundles/covering valid, invalid, and runnable pipeline states.
Changed
mthds_sourcesmigrated toextrahook:ApiRunner.validatenow passesmthds_sourcesthrough the generalizedextra: dict | Noneextension point, matching the updatedPipelexMTHDSProtocol.validatesignature frommthds-python 0.5.0.main_pipeis now optional for validation: Bundles without a main pipe validate successfully (is_valid: true) but returngraph_spec: null. Documentation and scripts updated accordingly.- OpenAPI Makefile targets depend on
install:openapi-exportandopenapi-checknow depend oninstallinstead ofenv, ensuring the schema is generated and validated against a fully synced environment to prevent silent CI drift. - Dependencies: Bumped
pipelexto0.35.0and addedmthds>=0.5.0topyproject.toml.
[v0.4.0] - 2026-06-17
MTHDS Protocol surface alignment (Phase 2): /validate (like /models) now routes through ApiRunner (extending PipelexMTHDSProtocol), exactly like /execute and /start. The runner owns backend selection (in-process vs a single dispatched Temporal activity), the runtime owns the canonical artifact shapes, and the route only adds wire extras — so hosted and local runners answer with identical artifacts.
Added
- Source threading for diagnostics: Added an optional
mthds_sourcesparameter toPOST /v1/validate, letting clients pass per-file sources parallel tomthds_contents. Each source is threaded onto the correspondingblueprint.sourceand onto the structured errors, so cross-file diagnostics map back to their owning file. Additive — sourceless callers are unchanged; a length mismatch againstmthds_contentsis a request-shape 422. - Structured validation errors: An invalid bundle now returns a non-empty
validation_errorsarray on the 200InvalidReport, where each item carries detailed locators (category,message,error_type,source,field_path, etc.) to map the error back to a specific construct and line. The items are builder-identical to the agent CLI'svalidation_errors(one shared pipelex builder, two surfaces). - Runnability facts: Added
validated_pipes,pending_signatures, andis_runnableto the validation report to communicate the runnability verdict across the HTTP surface.
Changed
/validateis now a 200-diagnostic endpoint: An invalid bundle is a successfully produced verdict and returns 200 OK with anInvalidReport(is_valid: false) instead of a 422. 422/5xx are reserved for no-verdict conditions (malformed request body,mthds_sourceslength mismatch, auth, server faults). The response is a discriminated union keyed onis_valid, returning either aValidReport(structural artifacts) or anInvalidReport(structured errors). The previousmain_pipeprecondition is dropped: bundles without a main pipe now validate successfully withgraph_spec: null.- Canonical validation report schema updates: Renamed
pipelex_bundle_blueprinttobundle_blueprint(the primary blueprint — first file declaringmain_pipe, else first);pipe_io_contractsis now keyed by the namespacedpipe_ref(e.g.domain.code) instead of the bare pipe code; and within each entry the input/output identifier was renamed fromconcept_codetoconcept_ref(now always the namespacedconcept.concept_ref).BuildConceptResponse.concept_codeon/v1/build/conceptis unaffected — it accepts a bare or qualified code. allow_signaturesbehavior: UnimplementedPipeSignaturedeclarations no longer reject a bundle.allow_signaturesnow only controls sweep mechanics (whether signature pipes are mock-run and listed invalidated_pipes); outstanding signatures are reported viapending_signatureswithis_runnable: falseon a 200ValidReport./modelsreturns the protocolModelDeck:/modelsnow routes throughApiRunnerand returns the canonicalModelDeck— the flatmodelslist plus this implementation's category-keyedaliases/waterfallsrouting extensions (a flat map would silently drop the same alias name across categories). The?type=query is now strictly single-value, matching the protocol arity.- Protocol version from the SDK:
/versionand the OpenAPI metadata now sourcePROTOCOL_VERSIONfrommthds.protocol.protocol; thepipelex.pipeline.runner.MTHDS_PROTOCOL_VERSIONalias is gone. - Temporal mode optimization: The worker now computes all library-dependent artifacts (
pipe_io_contracts,pending_signatures,graph_spec, status map, structured errors) within its single library load and ships them on the activity result, so the API side no longer re-acquires a library to build response artifacts. - Dependency & version bumps: Bumped the API version to
0.4.0and updated the pinnedpipelexdependency to track the keyword-only-arguments refactor (parse_pipe_spec(spec, pipe_type=...), keyword-onlyPipelex.make) and the new structured-error / signatures-as-data contract. - Documentation: Updated the OpenAPI artifact (
pipelex-api.openapi.yaml),pipe-validate.md, anderror-responses.mdto reflect the 200-diagnostic contract, structured errors, and source threading.
Removed
successwire field: Dropped the redundantsuccessfield from the/validateand/modelsresponses in favor of the canonicalis_validdiscriminant.- Legacy
/modelskeys: Dropped the unused rawpresetskeys (keyed by category) from the/modelsresponse and removed support for multi-category?type=queries.
[v0.3.0] - 2026-06-17
Changed — user_id is opaque again (path-safe, not UUID-shaped)
The runner no longer requires user_id to be a bare UUID. user_id is the owner segment of every storage key (<user_id>/...), and the runner is a generic execution engine: identity is the deployment's concern (enforced upstream by the gateway/auth layer that injects X-User-Id, or by the JWT issuer), not the runner's. The previous UUID-shape check wrongly rejected any non-UUID id — including hosted deployments that use prefixed ids like user_<uuid>, which were silently downgraded to anonymous and wrote results under anonymous/....
USER_ID_UUID_REGEXis replaced byis_safe_user_id(value)inapi/security.py: the only constraint is that the id be a single, unambiguous path segment — path-safe (no/,\, NUL/control chars, DEL; not./..) and free of URI gen-delims (:,?,#,[,],@). Any other opaque id (user_<uuid>, a bare uuid,user-123, …) is accepted as-is.- URI gen-delims are rejected because the id is embedded into a
pipelex-storage://<user_id>/...URI: a value likegoogle#abcis ownergoogle#abcby raw split butgoogleunder a standard URI parser (urlparse), so a consumer could resolve a different owner than the one this server authorized. - Applies to all three sites: the JWT
user_idclaim, the forwardedX-User-Idheader (TRUST_FORWARDED_IDENTITY_HEADERS=true), andpipelex-storage://URI parsing (/resolve-storage-url). - This reverses the prior "must be a UUID" constraint. Path-traversal protection is unchanged (it never depended on the UUID shape).
Fixed — close the anonymous-sentinel and malformed-forwarded-id gaps
The opaque-id change above made anonymous a path-safe value and left the forwarded-id path failing open. Both are now closed in api/security.py:
- The reserved
anonymoussentinel can no longer be claimed by an authenticated token. A JWT withuser_id: "anonymous"previously passed the path-safety check and bound that exact value — landing the caller's runs in the sharedanonymous/...namespace while storage/upload routes still treated them as unauthenticated.verify_jwtnow rejects it with401 InvalidToken. The value is centralized asANONYMOUS_USER_ID. - A malformed forwarded
X-User-Idnow fails closed. WhenTRUST_FORWARDED_IDENTITY_HEADERS=trueand the proxy forwards a non-empty but path-unsafe id,no_authpreviously logged and silently downgraded to anonymous; it now rejects the request with400 BadRequest. The absent-header and explicit-anonymouscases still stay anonymous (the proxy's deliberate "this request is anonymous" signal). is_safe_user_idnow rejects DEL (\x7f). The unsafe-character class covered C0 controls (\x00-\x1f) but let DEL through, contradicting the "no control characters" invariant. DEL is now rejected across all three sites (JWT claim, forwarded header, storage-URI parsing).
[v0.2.0] - 2026-06-12
Changed — extension args are this server's own (callback_urls)
- The MTHDS Protocol no longer defines
callback_urls(or any completion channel) — it is now formally THIS server's extension. The/startOpenAPI schema is published from the server's ownPipelexApiStartRequestmodel (protocolStartRequest+ the documentedcallback_urlsextension) instead of relying on the protocol model to advertise it.ApiRunner.startdrops the deadmethod_idcompatibility param (the hosted platform handlesmethod_iditself and never forwards it) and gains the protocol's genericextraslot. SDK clients pass server-specific args viaextra— e.g.client.start(..., extra={"callback_urls": [...]}).
Breaking Changes — MTHDS Protocol alignment (master plan 05, Phase C1)
This server is now the reference implementation of the MTHDS Protocol (contract nesting: MTHDS Protocol ⊂ Pipelex API ⊂ Pipelex hosted API). Clients on the new SDKs (mthds Python/JS protocol releases) require a pipelex-api image carrying these changes — the minimum image version for the /v1 surface is this release; an older image 404s on every /v1/* call.
- Base path:
/api/v1→/v1, no aliases. The API router now mounts at/v1(SDKs compose{MTHDS_API_URL}/v1/{endpoint}). Zero/api/v1routes remain. - Run routes renamed:
POST /api/v1/pipeline/execute→POST /v1/execute,POST /api/v1/pipeline/start→POST /v1/start./startnow answers 202 Accepted (protocolStartAck) instead of 200. - Wire fields renamed (D1): request extra
pipeline_run_id→pipeline_run_id(client-supplied run ids on/startare still accepted — the protocol allows it andStartAck.pipeline_run_idis authoritative); responses serializepipeline_run_id/stateinstead ofpipeline_run_id/pipeline_state. The pipelex runtime internals keeppipeline_run_id— only the wire renames. GET /versionreplacesGET /pipelex_version+GET /api_version(both deleted, no alias). Returns the protocolVersionInfo:{protocol_version, implementation: "pipelex-api", implementation_version, runtime_version}. PUBLIC — excluded from auth exactly like/health(it's the handshake clients use before they have credentials).- Completion-callback payload now carries
pipeline_run_id. The webhook POSTed tocallback_urlscarries the protocolpipeline_run_idfield alongside the runtime's existingpipeline_run_id/statuskeys; theX-Completion-SignatureisHMAC-SHA256(secret, pipeline_run_id)(unchanged scheme, renamed input). Thestatus→statekey rename lives in the pipelex runtime's delivery executor and ships with a later pipelex release — receivers should readpipeline_run_id+statusfor now. - Pipelex pinned to 0.33.0 (
PipelexMTHDSProtocol— the renamedPipelexRunner— with protocol methodsexecute/start/validate/models/version;PipelexRunResult/PipelexStartAckresponse models).
Added — MTHDS Protocol alignment
- Committed OpenAPI artifact + drift gate.
docs/openapi/pipelex-api.openapi.yamlis the layer-2 contract, exported from the live app viamake openapi-exportand drift-checked in CI viamake openapi-check(wired into the lint workflow andmake check). The five protocol routes are taggedx-mthds-protocol: true;/uploadand/resolve-storage-urlare documented as NON-CONTRACT in their descriptions (kept in the schema for self-hosters' interactive docs). - Protocol conformance suite.
tests/unit/test_protocol_conformance.pygates CI on: the five protocol paths under/v1(and zero legacy paths), theRunRequestanyOf rule (422 on empty body), the public/versionhandshake + shape, client-suppliedpipeline_run_idacceptance, and the completion-callback E2E — a local in-test HTTP receiver verifies delivery, theX-Completion-SignatureHMAC, and the payload'spipeline_run_id/status fields through the realDeliveryExecutorcode path (Temporal dispatch faked in-process).
Changed — /validate fast path restored
- Temporal-enabled
/validatedispatches one in-process activity again. The earlier temporary regression (direct in-processvalidate_bundle+dry_run_pipeline) is undone now that the pinned pipelex shipswf_dry_validate/act_dry_validate. On a Temporal-enabled runner,/validateruns the whole sweep + graph dry-run as ONEact_dry_validateactivity (a single worker round-trip) instead of dispatching the dry-run pipeline pipe-by-pipe through Temporal (one workflow + activities per pipe) — restoring the fast path first added in PR #12. Direct (Temporal-disabled) mode is unchanged. Same wire contract; the error contract and best-effort-graph semantics are identical across both backends.
Changed
- Duplicate
pipeline_run_idnow returns 409 Conflict instead of 500.PipelineManagerAlreadyExistsError— raised when a submission reuses apipeline_run_idthat is still registered for an in-flight run — is mapped to 409 via_ERROR_TYPE_STATUS_OVERRIDES, so a genuinely concurrent duplicate is a client-visible conflict rather than an opaque internal error. Pairs with the pipelex-side fix that frees a run's registry entry when it completes or fails, making serial resubmission of the same id succeed (previously every resubmission of a used id 500'd until process restart). Documented indocs/error-responses.md. - Error-log disposition is now keyed off the final HTTP status, not the error domain. A 4xx logs at
warning(no traceback); a 5xx logs aterror(with traceback). This keeps API-level 4xx overrides — the new 409 conflict, and the provider-429 passthrough — out of error dashboards instead of paging on a normal client conflict. The previous rule (onlyINPUT-domain →warning) left domain-less 4xx errors logging aterror. - Temporal-enabled
/validatenow runs as ONE worker round-trip. Whentemporal.is_enabledis true, the route dispatches the whole job — validation sweep + graph dry-run — as the one-step wrapper workflowwf_dry_validate(→ the single in-processact_dry_validateactivity) via pipelex'sdispatch_dry_validate, instead of runningvalidate_bundleAPI-side anddry_run_pipelineas a top-level worker workflow with a tracing-backend round-trip. The worker traces the graph in memory and returns{status map, graph_spec}on the activity result; the route re-parses the blueprints and buildspipe_io_contractsfrom a local load-only library acquisition. The wire contract is unchanged on both backends: same 200ValidateResponseenvelope, same best-effortgraph_spec(null when the graph dry-run fails), same RFC 7807 422 carryingerror_type=ValidationErrorfor the missing-main_pipeprecondition anderror_type=ValidateBundleErrorfor validation failures — both witherror_domain=input(the structured report crosses the activity boundary and the global handler renders it identically). Direct mode (Temporal disabled) is untouched. Requires a pipelex version that shipsact_dry_validate(newer than v0.32.1).
Breaking Changes
- Every error response is now RFC 7807
application/problem+json. Replaces the legacy{"detail": {"error_type", "message"}}envelope across pipelex domain errors, validation (422), auth (401/403), payload limits (413), and the catch-all 500. Standard members on the wire:type/title/status/detail/instance. Extension members:error_type,error_domain,retryable,request_id, and — when populated by pipelex —error_category,user_action,provider_metadata,model,provider. Content-Type isapplication/problem+json. Clients reading the legacydata.detail.messagemust read RFC 7807detail(top-level string) instead. /validatefailure envelope removed. A failing validation no longer returnsHTTP 200with{success: false, mthds_contents, message}; it now returnsHTTP 422(ValidateBundleError) with the RFC 7807 envelope. The former 400 "nomain_pipe" path is also 422 now. Success path (200ValidateResponse) is unchanged — samemthds_contents,pipelex_bundle_blueprint,graph_spec,pipe_io_contracts,success: true,messagefields. Cross-repo consumers inpipelex-appandmthds-jsupdated in companion PRs.X-Request-IDis now echoed on every response (success and error). InboundX-Request-IDis respected; otherwise the server generates a UUID. The same id rides throughJobMetadata.request_idto every Temporal worker log record.
Added
ERROR_DISCLOSUREenv var.verbose(default) renders the fullErrorReport;strictredactsdetailfor non-caller-facing errors and always stripsmodel/provider/provider_metadata. Provenance-gated via pipelex's_authors_caller_facing_messageClassVar —error_domainno longer drives redaction. Server logs stay verbose regardless of disclosure mode.docs/error-responses.md— public API error-contract page describing the envelope, status-code mapping (input→422,config/runtime→500), thetypeURI namespace, disclosure modes, request correlation, and worked examples. Linked fromdocs/pipe-run.mdanddocs/pipe-validate.md.allow_signaturesAPI flag. Opt-in boolean on/validate,/build/inputs,/build/output, and/build/runner. Whentrue, the validation sweep tolerates unimplementedPipeSignatureplaceholders (dry-running them by minting a mock) instead of rejecting the bundle. Defaults tofalse(strict).- Postman &
curlbundle runner. Newpostman-run-bundleClaude skill andbuild_postman_query.pyscript that turn a local MTHDS bundle into a Postman request, acurlcommand, or a direct API execution. Resolves the bundle exactly likepipelex run bundle <path>and targets/api/v1/pipeline/execute,/start, and/api/v1/validate. - Bundle testing Make targets.
make bundle-run,bundle-validate,bundle-curl,bundle-postman, andbundle-dryexercise a bundle against the API from the CLI. - Local Pipelex WIP support.
make run-wip/install-wip-pipelexrun the API against a local, editablepipelexworking tree without hand-editingpyproject.toml.
Changed
- Adapt to post-#931/#933 pipelex surface.
- Phase 6 module relocation:
EnvVarNotFoundErroris now imported frompipelex.system.exceptions(waspipelex.system.environment). Tests updated; no production code touched the moved import. - Acronym-casing fix: pipelex's
pascal_case_to_sentencenow preserves trailing acronym casing (InvalidJSON→Invalid JSON); thetest_error_uri.py::test_error_type_titleassertion updated. - Native
request_idwiring at dispatch.POST /pipeline/startnow reads the request-scopedrequest_idcontextvar and passes it asrequest_id=topipeline_run_setup(...), so it lands onJobMetadata.request_idand rides every worker-sideWorkflowLogrecord. No morewebhook.payload["request_id"]piggyback needed (andWebhookTarget.payloadwould now reject it as a reserved key anyway). - Cross-path consistency regression (T6). New
tests/unit/test_webhook_recovery.pypins the invariant: given the same sourceErrorReport, the classification fields surface identically via the sync HTTP RFC 7807 response and via the webhookerrorpayload (composed upstream byDeliveryExecutor._notify_webhook). - STRICT-disclosure audit (no code change). Confirmed
api/problem_document.pydelegates wholesale toreport.to_problem_document(disclosure_mode=...), so pipelex's provenance-gated keying flip (Decision D1) flows through untouched. The twoerror_domain == INPUTsites inapi/exception_handlers.pyare log-level switches, not wire-disclosure switches, and remain correct. - Shared request validation. Consolidated the MTHDS payload validation (the
mthds_contentsbound + per-file size guard) and the newallow_signaturesflag into a sharedMthdsContentsRequestPydantic base model that/validateand the build routes subclass, so the validation routes can't drift. /build/inputsand/build/outputreuse the validated library. Both now read the requested pipe from the libraryvalidate_bundlealready opened and left current, instead of opening and loading a second one — less work and memory per request — and scope the dry-run sweep to the requested pipe.- Unit tests run with Temporal disabled.
tests/unit/conftest.pyforcestemporal_enabled=False, so the suite executes pipelines (including dry-run validation) in-process and hermetically. pyproject.tomltooling config. Set pyrightvenvPath/venvand expanded theexcludelists to ignorenode_modules, hidden files, and.claude/.
Fixed
- Library resource leak in
/validate,/build/inputs, and/build/output. The libraryvalidate_bundleopens and leaves current on success was never torn down, orphaning a library in theLibraryManageron every successful call. Each route now owns that teardown in afinally. /build/runnerreturned 500 on a failed dry-run. A failed dry-run of a caller-submitted bundle now becomes a 422ValidateBundleError(RFC 7807 problem response), matching/validate,/build/inputs, and/build/output. (The bareDryRunErrorcarried noerror_domain, so the global handler had been rendering it as a server fault.)/build/runnergenerated code for aSKIPPEDpipe. When the requested pipe wasSKIPPEDduring validation (an unresolved cross-package dependency), the endpoint used to emit runner code for a pipeline that can't actually run; it now rejects the request with 422.- Makefile
helpoutput.Makefile_basics.mkno longer overrides the roothelptarget, so the local and deployment help sections all compose.
Known follow-ups (deferred, not in this set of changes)
- Structured logging on
_notify_webhook(event=webhook_delivery/event=webhook_failure) — lives in pipelex upstream atdelivery_executor.py, not in this repo. Tracked for a separate pipelex PR.
[v0.1.2] - 2026-05-20
Changed
- Trimmed
RequestUsertouser_idonly. Dropped theemail,sub, andauth_methodfields. The runner is a generic execution engine — the only piece of identity it consumes is an opaque user id, which it scopes S3 storage keys under (<user_id>/...). Anything else (email, OAuth subject, auth method) is metadata the runner has no use for; handlers that need it look it up byuser_idagainst the deployment's own user store. - JWT auth now requires a
user_idclaim (UUID). No fallback to the standardsubclaim, and the value is now validated as a UUID (^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$) at the auth boundary. Storage URIs require the owner segment to match the same shape; provider-issuedsubvalues like"google#abc"(or any non-UUIDuser_id) would otherwise let a caller upload to S3 under a key that/resolve-storage-urlwould later refuse to resolve. Deployments using OAuth JWTs must mint their ownuser_idclaim (a UUID for that caller) when issuing tokens. - Forwarded identity headers reduced to a single
X-User-Id(must be a UUID). WithTRUST_FORWARDED_IDENTITY_HEADERS=true, onlyX-User-Idis honored and its value is validated against the same UUID shape as JWTuser_id. Non-UUID values are silently ignored and the request stays anonymous. The previousX-User-Email,X-User-Sub, andX-Auth-Methodheaders are gone — they were metadata the runner never used. - Bumped Pipelex to v0.28.0. See changelog here: https://docs.pipelex.com/latest/changelog/
Fixed
mkdocs build --strictno longer fails on relative links inside theCONTRIBUTING.mdsnippet included intodocs/contributing.md. Those links are authored to resolve from the repo root on GitHub; mkdocs validation for them is downgraded fromwarntoinfo.
Security
- Tightened
pipelex-storage://owner-segment validation.parse_storage_uripreviously accepted any 36-character mix of hex and dashes (^[a-f0-9-]{36}$), so values like 36 dashes oraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaacould pass through. The check now enforces the canonical UUID positional shape8-4-4-4-12, matching the validator newly applied at the auth boundary so the two layers cannot drift.
[v0.1.1] - 2026-05-07
Added
/releaseClaude Code skill (.claude/skills/release/) automating the release workflow: version bump inpyproject.toml, changelog finalization,make check,make li, release branch creation, commit, push, and PR.
Changed
- Bumped Pipelex to v0.27.0. See changelog here: https://docs.pipelex.com/latest/changelog/
[v0.1.0] - 2026-05-04
Changed
- The published
pipelex/pipelex-apiDocker image is now generic. Temporal is off by default, no S3 storage is configured, and no DynamoDB tracing is enabled. Anything environment-specific is now expected to come from a user-supplied.pipelex/pipelex_override.toml(orpipelex_<env>.toml) mounted into/root/.pipelex/. Seedocs/configuration.md. - Removed deployment-specific env configs from the repo.
pipelex_dev.toml,pipelex_staging.toml,pipelex_prod.toml, andpipelex_service.tomlpreviously baked one specific deployment's Temporal cluster, S3 buckets, and DynamoDB table names into the public image. They are gone — bring your own via a mounted.pipelex/override (seedocs/configuration.md). - AWS/ECR deploy targets removed.
Makefile.deploy.api.mknow only containsdeploy-docker-hub. The previousdeploy-api,deploy-api-dev,deploy-api-stagingtargets and their corresponding workflows are gone — they were specific to one deployment's AWS account. CI in this repo now publishes to Docker Hub only on every push tomain. Self-hosters wire their own deploy from a separate infra-as-code repo. - Hardcoded
temporal_enabled=Trueremoved fromapi/main.py. Temporal is now controlled entirely by[temporal] is_enabledin your config, with no Python-level override. - Dockerfile cleans up
/app/.pipelexafter copying it to/root/.pipelexso user-supplied overrides at/root/.pipelex/*.tomlare not shadowed by a project-level.pipelex/inside the image. .env.exampletrimmed to universally-needed vars only (PIPELEX_GATEWAY_API_KEY). Auth, Temporal, and callback-secret vars are now optional / deployment-specific.COMPLETION_CALLBACK_SECRETis no longer required at boot.api/routes/pipelex/pipeline.pynow reads it lazily inside_completion_signature()instead of at module import. The image boots with justPIPELEX_GATEWAY_API_KEY; the secret is only required when you actually usePOST /api/v1/pipeline/startwith acallback_url. Seedocs/pipe-run.md→ "Async Completion Callbacks".
Added
docs/configuration.md— explains the env vars this Docker image accepts, the Pipelex config layering inside the container, and how to mount your own.pipelex/overrides. The actual Pipelex configuration syntax (storage, tracing, inference, Temporal, …) lives at https://docs.pipelex.com.
Fixed
- Move
Pipelex.make()from module top-level into a FastAPIlifespanhandler inapi/main.py. Importingapi.mainno longer mutates the global Pipelex singleton, which was causingmake tto fail underpytest-xdistonce enough unit tests existed for workers to share processes with the e2e import-smoke test. - Set a placeholder
COMPLETION_CALLBACK_SECRETintests/conftest.pyso test collection succeeds in CI environments where.envisn't present.api/routes/pipelex/pipeline.pyreads this var at import time, which was breakingmake gha-testscollection as soon as any test transitively importedapi.routes.
Security
/uploadnow rejects unauthenticated andanonymouscallers with401 Unauthenticated, mirroring/resolve-storage-url. Previously, anonymous uploads were silently accepted and stored under ananonymous/assets/…prefix.
Changed
/uploadreturns400 InvalidBase64(instead of500) when the request body'sdatafield isn't valid base64. Genuine storage failures still surface as500.UploadRequestnow usesConfigDict(extra="forbid"), so unknown fields produce422instead of being silently ignored.mthds_content→mthds_contents— UpdatedApiRunner.start_pipeline, pipeline routes, and build routes to usemthds_contents: list[str] | Noneinstead of singularmthds_content. Aligns with the updatedRunnerProtocolinmthds>=0.2.0.bundle_uri→bundle_uris— Renamed tobundle_uris: list[str] | NoneinApiRunnerto match the updatedPipelexRunnerinterface.
[v0.0.12] - 2026-01-15
Changed
- Bump
pipelextov0.18.0, theChicagorelease: See changelog here
[v0.0.11] - 2025-12-01
Added
pyjwtdependency.
[v0.0.10] - 2025-12-01
Changed
- Bump
pipelextov0.17.3: SeePipelexchangelog here
Added
- JWT authentication support.
[v0.0.9] - 2025-11-26
Changed
- Bump
pipelextov0.17.1: SeePipelexchangelog here
[v0.0.8] - 2025-11-04
Added
- Documentation for the API.
Changed
- Reverted the
all_blackboxairouting profile to the pipelex default one.
[v0.0.7] - 2025-10-29
Changed
- Hardcoded routing profile
all_blackboxaifor the pipelex hackathon.
[v0.0.6] - 2025-10-29
Changed
- More robust library management.
[v0.0.5] - 2025-10-27
Fixed
- Updated
Pipelexdependency tov0.14.0.
[v0.0.4] - 2025-10-26
Fixed
- Updated telemetry settings.
[v0.0.3] - 2025-10-25
Feature
- You can now run directly a pipelex bundle from the API.
Added
Pipelex.make(IntegrationMode.FASTAPI)to the API
[v0.0.2] - 2e25-10-22
Added
docker-compose.ymlfile
Changed
README.mdAdded more precise instructions for local, docker run, and docker compose.
[v0.0.1] - 2025-10-22
- Initial commit!