Skip to content

Tale v0.5.23

Choose a tag to compare

@larryro larryro released this 13 Sep 08:16
7f8f950

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 requestId and X-Tale-Api-Version like every other refusal — the x-api-key 401 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 408 REQUEST_TIMEOUT in 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 answers Cache-Control: no-store.
  • Pagination. Every keyset list now answers continueCursor; GET /projects and GET /projects/{id}/files keep their cursor as a deprecated twin, and the website pages list gains isDone, continueCursor and ?cursor= beside its offset window. Every list schema declares its family as x-tale-pagination: keyset | offset | none, the spec's own guard holds the families together, and INVALID_LIMIT / INVALID_CURSOR name their parameter under data.issues.
  • Validation. A blank project key, a blank folder parentId, a folder name carrying a control character or a backslash, and an empty contacts/bulk batch answer 400; the folder rule is one shared character class the file-name and WebDAV checks use too. A malformed or foreign storageId on a conversation sync answers 400 ATTACHMENT_NOT_STAGED instead of an undeclared 403.
  • Chat. A locale on 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 slices reasoning with ?reasoningSince= the way it slices text; DELETE …/generation stops a send that is still queued (202 cancelling, settling as a cancelled reply) and names the last reply on its 404; the tutorial's Python sample keeps the reply id and checks status (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, lastErrorAt on the page, failedPageCount on 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 REST retry-indexing door shares the app's guards: an in-flight or unsupported file is skipped with 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 by GET /projects?externalItemId= under either spelling) and encodes the canonical rule as unique indexes on projects and tasks. A task whose automationSlug names a saved-but-undeployed automation answers 409 AUTOMATION_NOT_DEPLOYED (it was a 404 that said "not deployed"). The automation summary carries its trigger's health (lastFiredAt, lastSkippedAt, lastSkipReason) and PUT …/triggers says whether the automation is deployed; project files carry size and indexing, the bind answers size and mimeType, and a task carries archivedAt.

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, project and indexing state, so an unindexed file is visibly unreadable by search.
  • rag_fetch reads 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 locale answers 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 or POST /documents/{id}/retry-indexing — opts the file into indexing; before, the request was refused as rag-opt-out.
  • A page the crawler failed on keeps its status and 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's externalSystem/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 — a key that is blank once trimmed answers 400 INVALID_BODY (it was derived from the name; the reference had promised the 400).
  • POST /projects/{id}/folders — a blank parentId answers 400 INVALID_BODY (was an opaque 404); a name carrying a control character or \ answers 400 FOLDER_NAME_INVALID.
  • POST /contacts/bulk — contacts: [] answers 400 INVALID_BODY (was 201 with nothing created).
  • POST /conversations/sync (and the native-reply path) — a malformed or foreign-organization storageId answers 400 ATTACHMENT_NOT_STAGED (was 403 BLOB_REF_INVALID, a code outside the enum).
  • POST /projects/{id}/tasks — an automationSlug naming a saved-but-undeployed automation answers 409 AUTOMATION_NOT_DEPLOYED; an unknown one stays 404 AUTOMATION_NOT_FOUND.
  • PATCH /knowledge-entries/{id} — a body that repeats the active row's topic and content writes no version and answers the active row's own id.
  • POST /documents/{id}/retry-indexing — reason is one of content-only, untracked-blob, unsupported, in-progress; rag-opt-out is retired (an opted-out file now queues and answers indexing); the per-user retry budget answers the door-wide 429.
  • POST …/messages — locale pins the reply language; omitted, the assistant answers in the prompt's language.
  • GET /skills/{slug} — the 304 carries Cache-Control: private, no-cache (was no-store) and matches the edge's -gzip/-zstd tags.
  • 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, with requestId in the envelope and the door headers; the proxy rule is removed. The x-api-key 401 and the NUL 400 carry X-Tale-Api-Version and Cache-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 deprecated cursor; a lookup answers isDone: true, continueCursor: ''.
  • GET /websites/{id}/pages — isDone, continueCursor and ?cursor= beside total/offset/hasMore; cursor and offset together answer 400 INVALID_QUERY. WebsitePage gains failCount, lastError, lastErrorKind, lastErrorAt, and its status is the declared enum discovered | active; Website gains failedPageCount.
  • Every list envelope carries the vendor extension x-tale-pagination (keyset, offset or none); INVALID_LIMIT and INVALID_CURSOR carry data.issues; Error.data documents providers, lastMessageId and lastStatus.
  • GET …/generation — ?reasoningSince=, reasoningOffset, reasoningLength. DELETE …/generation — 202 {status: 'cancelling', messageId} for a queued send; its 404 CHAT_TURN_NOT_RUNNING carries data.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}/files 201 file — 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) answers Cache-Control: no-store on every response.

Known issues

  • Unchanged from v0.5.20, where each is described in full: the es/co-cc Colombian 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_search embedding calls inside a harness turn are unmetered; the product edit dialog cannot clear a field; the app's skill editor still carries the retired private visibility.
  • 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-F36 covers 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-pagination extension is a declaration on the OpenAPI document; generated clients that do not read vendor extensions still branch on the two cursor names until cursor is 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_twins groups app.projects by (org_id, canonical external_item_id) and app.tasks by (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 to NULL (a task keeps its external_url), brings a surviving non-canonical key to canonical form, and adds the unique indexes projects_org_external_item_canonical and tasks_project_external_canonical beside 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_count adds the nullable failed_page_count column to app.websites, filled by the next corpus-to-row sync.
    • Knowledge database public_web migration 10 adds last_error, last_error_kind and last_error_at to website_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_key constraint 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 new tale-proxy tag.
  • 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.example is 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 the setup-cli action 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 with bun run build:linux-baseline in tools/cli instead.

What's Changed

  • fix(platform): close the round-d API findings and pin project chat scope by @larryro in #3334

Full Changelog: v0.5.22...v0.5.23