Tale v0.5.23
0.5.23 is a fix release on the 0.5 line that carries more than its fixes: the fourth set of REST contract corrections from an external API evaluation (contract 1.4.0 → 1.5.0), a repaired and now enforced rule for caller-owned external keys, observable crawl failures, an honest 408 at the door, and a server-pinned scope for the chat assistant inside a project. Two platform migrations (0098, 0099) and one knowledge-database migration apply themselves on tale deploy, each written to run while the previous image is still serving. No configuration-file change and no new environment variable. The Known issues from 0.5.22 are unchanged; two verification gaps are listed with them.
Highlights
The fourth API evaluation pass (#3334)
The fourth external black-box evaluation of the REST and MCP API ran against 0.5.22 (~600 requests, no 5xx) and reported 0 S1, 3 S2, ~18 S3 and ~15 S4 findings. Every item was second-confirmed from source before a fix was designed. Several of the report's headline findings did not hold: the "SSRF through a 302 redirect" is not one (the fetch client validates every hop before it dials, now proven again by an integration lane with a loopback listener that receives nothing), the listed-but-unusable model is the documented "catalog, not an account promise" and belongs to the operator's model allowlist, the "unknown 409" was in the OpenAPI document, the OIDC endpoints were documented, and the slow inline document was the tester's uplink. What did hold is fixed here; every wire change is listed under API contract changes.
- Door and edge. The proxy no longer answers 414 itself: the backend guard is the one producer, and its envelope carries
requestIdandX-Tale-Api-Versionlike every other refusal — thex-api-key401 and the NUL 400 carry the door headers too. A request must finish arriving within 15 minutes (Node's default was five, answered as a bare 408 with no body); a slower one answers 408REQUEST_TIMEOUTin the envelope.PUT /skills/{slug}answers 201 when it creates; the skills 304 is decided by the validated-read lane like every other JSON read. The webhook door answersCache-Control: no-store. - Pagination. Every keyset list now answers
continueCursor;GET /projectsandGET /projects/{id}/fileskeep theircursoras a deprecated twin, and the website pages list gainsisDone,continueCursorand?cursor=beside its offset window. Every list schema declares its family asx-tale-pagination: keyset | offset | none, the spec's own guard holds the families together, andINVALID_LIMIT/INVALID_CURSORname their parameter underdata.issues. - Validation. A blank project
key, a blank folderparentId, a folder name carrying a control character or a backslash, and an emptycontacts/bulkbatch answer 400; the folder rule is one shared character class the file-name and WebDAV checks use too. A malformed or foreignstorageIdon a conversation sync answers 400ATTACHMENT_NOT_STAGEDinstead of an undeclared 403. - Chat. A
localeon the REST send now pins the reply language (the field was wired, but the runtime directive told the model to follow the user's language, the opposite of what the reference promised). The generation poll slicesreasoningwith?reasoningSince=the way it slicestext;DELETE …/generationstops a send that is still queued (202cancelling, settling as acancelledreply) and names the last reply on its 404; the tutorial's Python sample keeps the reply id and checksstatus(it used to print an empty string on any failed turn and read the wrong row on a long thread). - Knowledge and websites. Superseding a knowledge entry with an identical body writes no version. A page the crawler could not store now says why —
failCount,lastError,lastErrorKind,lastErrorAton the page,failedPageCounton the website — and a listed page is never dead forever (it used to leave the crawl after five failed attempts with no way back). Every operation with its own body cap declares it in its 413 text. The RESTretry-indexingdoor shares the app's guards: an in-flight or unsupported file isskippedwith its reason, and an explicit retry opts a bind-time-skipped file back in. - Projects, tasks, automations. Migration 0098 repairs the external-key twins migration 0093 left behind (a project stored with a decomposed
ébeside a newer composed twin could not be found byGET /projects?externalItemId=under either spelling) and encodes the canonical rule as unique indexes on projects and tasks. A task whoseautomationSlugnames a saved-but-undeployed automation answers 409AUTOMATION_NOT_DEPLOYED(it was a 404 that said "not deployed"). The automation summary carries its trigger's health (lastFiredAt,lastSkippedAt,lastSkipReason) andPUT …/triggerssays whether the automation isdeployed; project files carrysizeandindexing, the bind answerssizeandmimeType, and a task carriesarchivedAt.
The chat assistant's scope inside a project (#3334)
A chat started inside a project listed only the organization's hub documents, had to list every project to find its own, and could not read a project file that had never been indexed. The documentation already promised the opposite, so the platform now keeps that promise on the server, not by trusting the model: the thread's project is the boundary of rag_search and rag_fetch. A project chat reaches its project's files plus the organization's knowledge hub as the person sees it; the organization chat reaches the hub only; the assistant's task and project questions inside a project stay on that project's board, and another project's id is refused. The model in one paragraph, now in the docs, the prompt and the tool descriptions: a document lives in exactly one place, the hub or one project; team tags decide who sees a hub document; project access alone decides who sees a project file, which never carries team tags.
- The document listing carries each row's
scope,projectandindexingstate, so an unindexed file is visibly unreadable by search. rag_fetchreads a never-indexed plain-text file of up to 4 MiB on request and otherwise names the file and its true state (skipped by the uploader, pending, failed with its error, unsupported) instead of "may not be indexed yet". One shared reader serves the chat and the sandbox bridge.- The project's Knowledge tab shows Not indexed for a file bound without indexing and offers Index now; the timeline step never shows a raw blob reference.
Behaviour changes
- The organization chat no longer reaches project files through the assistant's tools; project files are read from the project's own chat. A project chat's task and project listings are that project's.
- A project chat's system prompt names the project and its key and states the boundary; the "prefer its material, but anything the user can read" clause is gone.
- A REST send with
localeanswers in that language whatever language the prompt is written in; a send without it answers in the prompt's language as before. The app's chat is unchanged. - An explicit re-index of a file bound with
skipRagIndexing: true— Index now on the Knowledge tab orPOST /documents/{id}/retry-indexing— opts the file into indexing; before, the request was refused asrag-opt-out. - A page the crawler failed on keeps its
statusand carries the failure beside it; a listed URL is probed once per scan even after five failures; an extraction failure counts as a failure, not a visit. - The backend accepts a request body for up to 15 minutes; a client slower than that gets a JSON 408 and a closed connection, where it used to get a bare status line after five minutes.
- The proxy's 32 KiB API-URL rule is gone; URLs between 32 and 64 KiB reach the backend and answer its 414, and above 64 KiB the edge's header budget still closes the connection as documented.
- A project or task whose external key was one of two normalisation twins has been released by migration 0098 (its
externalItemId, or the task'sexternalSystem/externalId, reads empty);PATCH /projects/{id}re-keys a project. - A hidden regenerate-sibling thread is no longer readable or editable by id through the REST door.
API contract changes
The OpenAPI document moves to 1.5.0; every /api/v1 response names it in X-Tale-Api-Version.
Changed
POST /projects— akeythat is blank once trimmed answers 400INVALID_BODY(it was derived from the name; the reference had promised the 400).POST /projects/{id}/folders— a blankparentIdanswers 400INVALID_BODY(was an opaque 404); anamecarrying a control character or\answers 400FOLDER_NAME_INVALID.POST /contacts/bulk—contacts: []answers 400INVALID_BODY(was 201 with nothing created).POST /conversations/sync(and the native-reply path) — a malformed or foreign-organizationstorageIdanswers 400ATTACHMENT_NOT_STAGED(was 403BLOB_REF_INVALID, a code outside the enum).POST /projects/{id}/tasks— anautomationSlugnaming a saved-but-undeployed automation answers 409AUTOMATION_NOT_DEPLOYED; an unknown one stays 404AUTOMATION_NOT_FOUND.PATCH /knowledge-entries/{id}— a body that repeats the active row'stopicandcontentwrites no version and answers the active row's own id.POST /documents/{id}/retry-indexing—reasonis one ofcontent-only,untracked-blob,unsupported,in-progress;rag-opt-outis retired (an opted-out file now queues and answersindexing); the per-user retry budget answers the door-wide 429.POST …/messages—localepins the reply language; omitted, the assistant answers in the prompt's language.GET /skills/{slug}— the 304 carriesCache-Control: private, no-cache(wasno-store) and matches the edge's-gzip/-zstdtags.GET …/threads/{id}by id — a hidden regenerate-sibling thread answers 404.- 414
URI_TOO_LONG— answered by the backend for every API URL above 32 KiB, withrequestIdin the envelope and the door headers; the proxy rule is removed. Thex-api-key401 and the NUL 400 carryX-Tale-Api-VersionandCache-Control: no-store. Idempotency-Key— the parameter declares a printable-ASCII pattern (a header value carrying a control character never reaches the platform); no length is enforced.
Added
GET /projects,GET /projects/{id}/files—continueCursor(required,''when done) beside the deprecatedcursor; a lookup answersisDone: true, continueCursor: ''.GET /websites/{id}/pages—isDone,continueCursorand?cursor=besidetotal/offset/hasMore;cursorandoffsettogether answer 400INVALID_QUERY.WebsitePagegainsfailCount,lastError,lastErrorKind,lastErrorAt, and itsstatusis the declared enumdiscovered | active;WebsitegainsfailedPageCount.- Every list envelope carries the vendor extension
x-tale-pagination(keyset,offsetornone);INVALID_LIMITandINVALID_CURSORcarrydata.issues;Error.datadocumentsproviders,lastMessageIdandlastStatus. GET …/generation—?reasoningSince=,reasoningOffset,reasoningLength.DELETE …/generation— 202{status: 'cancelling', messageId}for a queued send; its 404CHAT_TURN_NOT_RUNNINGcarriesdata.lastMessageId/data.lastStatus.PUT /skills/{slug}— 201 on create, 200 on update.- 408
REQUEST_TIMEOUT— door-wide, in the envelope, when a request does not finish arriving within 15 minutes. AutomationSummary.trigger—lastFiredAt,lastSkippedAt,lastSkipReason;PUT /automations/{name}/triggers—deployed.ProjectFile—size,indexing;POST /projects/{id}/files201file—size,mimeType;Task—archivedAt.- Every operation with its own body cap declares it in its 413 (documents 32 MiB, contacts bulk 8 MiB, conversation sync 8 MiB, staged upload 30 MiB, skill save 4 MiB, delivery claim/fail/ack 64 KiB).
- The webhook door (
/api/automations/webhook/{token}and the project twin) answersCache-Control: no-storeon every response.
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 project chat's new scope was proven by unit tests and the real-database integration lane, not by a live model turn in a browser; the manual box
CHAT-F36covers the live reading of the boundary sentence. - The proxy entrypoint change (the removed URL rule) was reviewed and built into the image, not exercised against a running edge; the backend's 414 lane is covered by tests.
- 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.
Migration notes
- Two platform migrations and one knowledge-database migration apply on the first boot of the new images, each written to run while the previous image is still serving:
0098_external_keys_canonical_twinsgroupsapp.projectsby(org_id, canonical external_item_id)andapp.tasksby(project_id, canonical external_system, canonical external_id)— canonical being NFC-normalised and trimmed — keeps one row per group (the row already holding the canonical bytes, else the oldest), sets the others' key columns toNULL(a task keeps itsexternal_url), brings a surviving non-canonical key to canonical form, and adds the unique indexesprojects_org_external_item_canonicalandtasks_project_external_canonicalbeside the byte-exact ones. Detached rows stay reachable by id, in the listing and on the board; the header of the file carries the detection query. Idempotent.0099_websites_failed_page_countadds the nullablefailed_page_countcolumn toapp.websites, filled by the next corpus-to-row sync.- Knowledge database
public_webmigration 10 addslast_error,last_error_kindandlast_error_attowebsite_urls; the database image applies it in its knowledge role at boot, and a fresh or bring-your-own corpus gets the columns from the converge path. It has no down migration by design.
- 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 entrypoint no longer renders the 32 KiB API-URL rule. It rolls with
tale deploy; an own-Compose deployment pulls the newtale-proxytag. - The backend listener now enforces a 15-minute request arrival budget with a JSON 408; nothing to configure.
- No configuration-file change and no new environment variable;
.env.exampleis untouched. The web, sandbox, sandbox-runtime, sandbox-buildkitd 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 model allowlist of a provider credential is where a model the provider's plan does not cover is excluded from
GET /models— the evaluation's "listed but not entitled" finding is closed there, not in code.
Upgrading
-
On the 0.5 line (0.5.0 – 0.5.22):
tale update tale deploy
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. 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
Full Changelog: v0.5.22...v0.5.23