Tale v0.5.24
0.5.24 is a fix release on the 0.5 line that carries more than its fixes: the fifth set of REST contract corrections from an external API evaluation (contract 1.5.0 → 1.6.0), JSON refusals at the edge where a machine caller used to get the HTML maintenance page, a reply cap that bounds a whole chat turn, and a repaired managed on-premises deployment path in the CLI. Two platform migrations (0100, 0101) apply themselves on tale deploy, each an additive nullable column written to run while the previous image is still serving. No knowledge-database migration, no configuration-file change and no new environment variable. The proxy image changes and a plain tale deploy does not roll it — see Upgrading. The Known issues from 0.5.20 are unchanged; the two verification gaps 0.5.23 listed are closed, and this release's own are listed with them.
Highlights
The fifth API evaluation pass (#3335)
The fifth external black-box evaluation of the REST and MCP API ran against 0.5.23 (~1,800 requests, no backend 5xx) and reported 0 S1, 2 S2, ~13 S3 and ~15 S4 findings. Every item was second-confirmed from source before a fix was designed. The headline S2 — an intermittently slow or failing TLS handshake over IPv4 while IPv6 was clean — was the evaluating machine, not the platform: a tunnel interface on that host captured every locally originated IPv4 flow while IPv6 left directly, and bound to the physical interface the IPv4 handshake was as fast and as reliable as IPv6 (40 of 40, about half a second). No platform change. The second S2, a listed model the provider's plan did not cover, was closed in the operator's model allowlist during the round, where 0.5.23's notes had placed it. The evaluation also drove the project-chat scope 0.5.23 shipped on live model turns — a project thread reads its project's file, the organization thread stays on the hub — which closes the verification gap those notes listed. What did hold is fixed here; every wire change is listed under API contract changes.
- Edge. A truncated HTTP/2 upload — a declared
Content-Lengthlarger than the bytes sent — made the edge answer the HTML maintenance page with a 502, under the body cap too, so the platform never saw the request; it now answers 400BODY_LENGTH_MISMATCHin the JSON envelope. While the platform restarts, a machine door (/api/*,/scim/*,/http_api/*,/events,/status.json,/openapi.json,/.well-known/*) gets a JSONUPSTREAM_UNAVAILABLEwith the error's own status (502, 503 or 504) andRetry-After: 5, where a browser navigation still gets the maintenance page. AHEADis never compressed, so it reports the uncompressedContent-LengththeGETwould carry (the encoder used to stamp the length of an empty gzip frame). The dot-segment rule also refuses a raw URI whose dots fold into the API prefix (/x/../api/v1/me), which the backend's own parser folded and served. - Door.
Idempotency-Keyis printable ASCII, enforced: any other character answers 400INVALID_HEADERon run starts and chat sends (the pattern was declared, not enforced, so two spellings ofénamed two starts of one retry). Starting a task's workflow answers 404AUTOMATION_NOT_FOUNDor 409AUTOMATION_NOT_DEPLOYEDwhere it answered200 {started: false, reason: "not_started"}. The upload mint and the bind require a file name that ends in an allowlisted extension whatevercontentTypedeclares —CON, an extension-lessattachment-4711andprogram.exedeclared astext/plainall got in through the MIME half of the check — and the app's document pickers inherit the rule. File and folder names are stored trimmed and NFC-normalized;FOLDER_NAME_INVALIDnames the rule broken and carriesdata.issues. A website searchlimitoutside 1..100 is refused with 400INVALID_BODYinstead of clamped.HEADon/api/health,/status,/status.jsonand/openapi.jsoncarries theGET'sContent-Length; the two webhook doors answerX-Tale-Api-Version; the app-wide guard refusals (thex-api-key401, the NUL-in-URL 400) carryCache-Control: no-store. - Chat.
maxOutputTokensbounds the whole turn across model rounds: a later tool round gets what the earlier ones left, a round that would start with nothing left is not run,finishReason: "length"marks the reply whenever any round was cut, and a tool call the cap cut mid-arguments is not executed — its result readsstatus: "invalid_args"(it used to run with whatever arguments fitted, and the next round settledstopover a cut reply). 0.5.23 saidlocalepins the reply language; the evaluation measured a hint (four German replies out of six). The directive now rides the system prompt, naming the language, and the message itself, where a reasoning model on a short prompt looks; the reference says "asked to answer in" and tells a client that must have the language to check the reply and resend.costEstimateCentsis rounded to a millionth of a cent (the rates' binary noise,0.042601999999999994, reached the wire and the ledger). The assistant is told to search before answering that it does not know a term. - Projects, tasks, automations, skills, documents.
POST /projects/{id}/files/{documentId}/retry-indexingindexes a REST-bound project file without delete and re-bind — 0.5.23's notes pointed at the Hub document's retry, which answers 404 for a project file.externalState: "open"reopens a task the mirror itself parked atin_review(migration 0101 stamps such a park; a park a person or an agent made stays theirs) — a mirror that closed an item and reopened it upstream used to leave the card parked for good.testsPassedreadsfalseafter the deploy gate refused a version (it persisted nothing, so a release pipeline could not tell "no tests" from "the tests fail"),testsCheckedAtsays when (migration 0100), and the MCPsave_automationrecords the save's own test run. A run summary carriesidbesiderunId. An identicalPUT /skills/{slug}writes nothing — the history trail no longer evicts real versions for a mirror's re-push. A content-only document download honoursIf-Modified-Since(it re-downloaded every inline document on every poll), andGET /documents/{id}/contentdeclares its conditional headers and 304.
Managed on-premises deployments repaired (#3333, #3336)
A managed deployment's backend-local phases — deploy provision, and the native half of deploy export-client — run inside the backend container and import the backend's own database and auth modules, which import packages from the backend's node_modules. They ran as the compiled executable, which resolves bare imports against its embedded filesystem, so they failed with Cannot find package 'postgres', swallowed twice and surfaced as an operator email attestation failure (or an export failure), which sent debugging the wrong way. Every managed bundle now ships an interpreted build of the CLI, cli/tale.mjs, beside the executable under the same manifest hashes, and both phases run under the container's own bun, so the imports resolve the way the running backend's do. The CLI also requested the provider-credentials collection with a trailing slash the backend does not serve (a 404); the three collection calls use the slashless route. Both were verified live on a real on-premises backend and locked with regression tests.
Behaviour changes
- A 502, 503 or 504 at the edge answers JSON to a machine door and the maintenance page to a browser; a client that parses JSON no longer meets 4 KiB of markup mid-roll. A body shorter than its declared length over HTTP/2 is a 400, not an outage.
- A
HEADthrough the edge is never compressed; itsContent-Lengthis the uncompressed length. - A file name without an extension, or with one outside the allowlist, is refused at the mint, at the bind and in the app's upload and replacement pickers, whatever MIME type is declared; a declared type never stands in for the extension.
- File and folder names are stored trimmed and NFC-normalized from now on; rows written before keep their bytes (no backfill).
- Starting a task's workflow on a missing or undeployed automation is a refusal, not a 200 that started nothing;
reason: "not_started"is left for a deployment withdrawn between the check and the start. - A tool-calling chat turn spends
maxOutputTokensround by round, a cut call is not run, andlengthis reported when any round was cut. - A REST send with
localecarries the reply-language directive on the system prompt and on the message; the app's chat is unchanged. - Re-pushing an unchanged skill bundle through
PUT /skills/{slug}leavesetagandupdatedAtalone and writes no history entry. - A website search
limitout of range is refused, not clamped. - An external
openreopens a task the mirror parked; any move through the board's own doors ends the mirror's claim on that park. - The deploy gate persists a failed test verdict; the latest verdict wins over the one recorded at save time.
- An
Idempotency-Keyoutside printable ASCII starts nothing. costEstimateCentson a chat message and in the usage ledger is rounded to a millionth of a cent.
API contract changes
The OpenAPI document moves to 1.6.0; every response the platform answers names it in X-Tale-Api-Version. A refusal answered at the edge (the dot-segment 404, BODY_LENGTH_MISMATCH, UPSTREAM_UNAVAILABLE) carries a fresh requestId of its own and no version header: only the platform knows the contract it implements.
Changed
Idempotency-Key(run starts, chat sends) — a value outside printable ASCII (0x20–0x7E) answers 400INVALID_HEADER, the header named underdata.issues; nothing starts.POST /projects/{id}/tasks/{taskId}/start— aworkflowSlugthat names no automation answers 404AUTOMATION_NOT_FOUND; a saved-but-undeployed one 409AUTOMATION_NOT_DEPLOYED(was 200{started: false, reason: "not_started"}), judged before the execute budget is charged.POST /projects/{id}/uploadsandPOST /projects/{id}/files— afileNamewithout an allowlisted extension answers 400UNSUPPORTED_FILE_TYPEwhatevercontentTypesays;fileNameis stored trimmed and NFC-normalized.POST /projects/{id}/folders—nameis stored trimmed and NFC-normalized; 400FOLDER_NAME_INVALIDcarriesdata.issuesnamingnameand a sentence naming the rule.POST /websites/{id}/search—limitoutside 1..100 answers 400INVALID_BODY(was clamped).POST …/messages—maxOutputTokensbounds the whole turn;finishReason: "length"whenever any round was cut; a cut call'stool-resultpart readsstatus: "invalid_args";localeis a directive on the system prompt and on the message;costEstimateCentsis rounded to a millionth of a cent.PUT /skills/{slug}— a composedSKILL.mdbyte-identical to the stored one writes nothing: 200 with the storedetagandupdatedAt, no history entry;If-MatchandIf-None-Match: *are still evaluated first.GET /documents/{id}/content(content-only document) —If-Modified-Sinceis honoured againstLast-Modifiedat whole-second precision;If-None-Matchdecides alone when both travel.HEAD— never compressed through the edge (uncompressedContent-Length); on/api/health,/status,/status.jsonand/openapi.jsonit carries theGET'sContent-Length.POST /api/automations/webhook/{token}and the project twin — every response carriesX-Tale-Api-Version. Thex-api-key401 and the NUL-in-URL 400 carryCache-Control: no-store.GET /automations/{name}/versions—testsPassedreadsfalseafter the deploy gate refused a version (wasnull); the latest verdict wins.- Raw dot-segments — a URI whose dots fold into the API prefix (
/x/../api/v1/me) is refused at the edge with 404NOT_FOUNDlike one whose dots fold out of it.
Added
POST /projects/{id}/files/{documentId}/retry-indexing— 200{status: "indexing"}(lifting the bind-timeskipRagIndexingopt-out) or{status: "skipped", reason}, under the same 10-per-user-per-minute budget as the Hub document's retry (429RATE_LIMITED).AutomationVersion.testsCheckedAtandAutomation.testsCheckedAt— epoch milliseconds whentestsPassedwas judged,nullwith it.RunSummary.id— the run id besiderunId, on the REST listings and MCPlist_runs.POST /projects/{id}/tasks—externalState: "open"reopens a task the mirror parked atin_review, or adoneone, tobacklog.- 400
BODY_LENGTH_MISMATCH— answered at the edge for an HTTP/2 body that ended before its declaredContent-Length. - 502, 503, 504
UPSTREAM_UNAVAILABLE— answered at the edge on every machine door while the platform cannot be reached, withRetry-After: 5. GET /documents/{id}/contentdeclaresIf-None-Match,If-Modified-Sinceand its 304.Error.codegainsINVALID_HEADER,BODY_LENGTH_MISMATCHandUPSTREAM_UNAVAILABLE.
Documented, unchanged on the wire (en, de, fr): the 64 KiB header budget allows a few KiB of slack on HTTP/1.1 (a 66 KiB URL still answers 414); a never-bound automation runs in any project the caller can edit; an automation's runs outlive its deletion; GET …/triggers answers triggers, a list of at most one; the duplicate 409 codes are named (CONTACT_DUPLICATE_EMAIL, CONTACT_DUPLICATE_EXTERNAL_ID, DUPLICATE_PRODUCT_NAME, DUPLICATE_PRODUCT_EXTERNAL_ID, KNOWLEDGE_ENTRY_DUPLICATE, PROJECT_DUPLICATE_EXTERNAL_ID); a %00 in the URL is INVALID_URL ahead of every route; 422 SKILL_MALFORMED is about the bundle already stored, never the JSON body you send; KNOWLEDGE_ENTRY_STORE_TIMEOUT; a keyless request to an unknown /api/v1 path answers 401 before 404; the webhook door takes POST only and answers its 404 to every other verb; GET /models omits maxOutputTokens when the catalog declares no ceiling; a model failure's error is the provider's own answer prefixed with its HTTP status, so branch on errorCode; similarity is a scale, not a calibrated confidence; the Own Compose page explains IPv6 behind a Docker-published port and trustedProxies.
Known issues
- Unchanged from v0.5.20, where each is described in full: the
es/co-ccColombian cédula detector still ships switched off and a locale-agnostic PII toggle still widens national-ID matching to every locale (the PII library is untouched in this range); thinking-block replay on the native Anthropic connector is not done and the live Max-plus-tool-call check is still owed;rag_searchembedding calls inside a harness turn are unmetered; the product edit dialog cannot clear a field; the app's skill editor still carries the retiredprivatevisibility. - The
x-tale-paginationextension is a declaration on the OpenAPI document; generated clients that do not read vendor extensions still branch on the two cursor names untilcursoris retired. - The proxy changes were exercised through the real entrypoint in a local Caddy 2.11 container (a short HTTP/2 body over and under the cap, the platform down,
HEADwith gzip, the dot-segment spellings), not on a running deployment's edge. - A reply-language directive is a directive: a model may still answer in the prompt's language (most often a reasoning model on a short prompt), and nothing on the wire marks a slip.
- There is no single-file read on
/projects/{id}/files: a poller waiting for one file'sindexingafterretry-indexingwalks the folder listing. - The app's zip upload of a skill bundle rewrites the bundle and moves
updatedAteven when the zip is byte-identical, wherePUT /skills/{slug}writes nothing. - A tool call the reply cap cut keeps
input: {}on the storedtool-callpart; the raw text the model emitted is not on the transcript, so the timeline cannot show what was asked. - Folder names written before this release keep their bytes; a sync engine's hub-path lookup can create an NFC twin beside a legacy NFD folder. No backfill ships.
- Behind a Docker-published port, every IPv6 client arrives as the bridge gateway's address and shares one per-address rate-limit bucket and one audit address until the daemon runs with
ip6tablesand the proxy's network is IPv6-enabled — an operator item, documented on the Own Compose page.
Migration notes
- Two platform migrations apply on the first boot of the new images, each an additive nullable column with no backfill that the previous image neither reads nor writes:
0100_automations_tests_checked_ataddstests_checked_at_mstoapp.automations— when a version's test verdict was reached;nullfor a version saved without one, and an existing verdict has no known time.0101_tasks_external_closed_ataddsexternal_closed_at_mstoapp.tasks— the stamp of a park the mirror made; an existing park reads as a person's until the next external close stamps it.
- No knowledge-database migration in this range.
- The
task_labels_project_id_name_keyconstraint 0.5.22 kept for its rolling deploy is still in place; dropping it is a follow-up migration, not part of this release. - The proxy image changes (the error routes, the encoder skipping
HEAD, the widened dot-segment rule in the entrypoint). The proxy is in the stop-gated tier: a plaintale deployleaves a running proxy untouched and names it in a hint, so pass--stopto take the new edge rules (a brief downtime whiledb,object-storeandproxyrecreate); until then the edge keeps 0.5.23's rules while the platform's own changes apply. An own-Compose deployment pulls the newtale-proxytag. - A managed bundle now carries
cli/tale.mjs, the interpreted CLI, besidecli/tale.deploy prepareneeds that file beside the executable it runs as, which thesetup-cliaction andbun run --filter @tale/cli buildproduce at this commit; the executable from the release page has no interpreted bundle beside it and cannot prepare a managed bundle — build the CLI from the pinned commit, as the CLI install page describes. - The upload allowlist keys on the file name alone from now on; a client that relied on a declared MIME type for an extension-less name must name the file with its extension. Files already stored are untouched.
- No configuration-file change and no new environment variable;
.env.exampleis untouched. The db, web, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images have no source change in this range; the docs image carries the updated pages in all three languages. - Operators of the hosted platform: the IPv6 item above is a Docker daemon setting on the host (
ip6tables, no userland proxy, an IPv6-enabled proxy network), not a platform release.
Upgrading
-
On the 0.5 line (0.5.0 – 0.5.23):
tale update tale deploy --stop
--stoprecreatesdb,object-storeandproxyso the new edge rules take effect (a brief downtime); a plaintale deployapplies everything else and leaves the running proxy on 0.5.23's rules. The migrations above apply on the first boot of the new images. -
Managed deployments move by pinning the CLI and the runtime to this release's commit, preparing a new bundle and applying it with the pinned CLI — see Managed deployments on the CLI install page. Pin the CLI at this commit, not an older one: the bundle's backend-local phases run under the interpreted CLI that #3333 ships. On a Linux x64 host whose CPU lacks AVX2, pass
linux-baseline: 'true'to thesetup-cliaction so the bundle embeds the baseline executable. -
New install:
curl -fsSL https://raw.githubusercontent.com/tale-project/tale/main/scripts/install-cli.sh | bash mkdir tale-05 && cd tale-05 tale init tale deploy
On a CPU without AVX2 the downloaded executable aborts with
Illegal instruction; build it from source withbun run build:linux-baselineintools/cliinstead.
What's Changed
- fix(cli): repair on-prem deploy provision and credential path by @yannickmonney in #3333
- fix(cli): run backend-local client export under interpreted bun by @yannickmonney in #3336
- fix(platform): close the 2026-09-13 API evaluation's fifth-pass findings by @larryro in #3335
Full Changelog: v0.5.23...v0.5.24