Skip to content

Tale v0.5.33

Choose a tag to compare

@larryro larryro released this 18 Sep 08:58
155dbd9

0.5.33 carries 5 merged pull requests. Three of them are about who may hold an account on a deployment and what that account is worth: the account-creation route now closes the moment a deployment holds an account, an account an administrator provisions counts as a verified one everywhere at once, and a managed deployment can rename its deploy operator in place and declare a break-glass administrator whose password never reaches the deployment. The fourth closes the 2026-09-18 external API evaluation — the backend no longer stamps Sentry trace headers onto outbound requests, the crawler honours the robots group written for it by name, a webhook re-posted across scopes is refused instead of run twice, and a third concurrent site scan waits for a render slot instead of failing — and moves the contract to 1.15.0. The fifth reconnects two governance model pickers that had been empty since the AI-backend rewrite. One migration (application database 0107, nullable column and index, rolling-safe), one new environment variable for throwaway test stacks only, and nothing in the stop-gated tier: the upgrade is tale update followed by a plain tale deploy.

Highlights

Account creation closes once a deployment has an account (#3407)

POST /api/auth/sign-up/email answered anyone who could reach the backend. The proxy in front of a deployment refuses that route, but backend-api is also attached to each box's sandbox network so the in-sandbox connectors bridge can call its host door — and the proxy never sees a request that arrives that way. Measured on a test deployment, from a container on that network: 200, with a live session token for a brand-new account. Code running inside an agent session — a prompt-injected agent, or an automation body — could create accounts on a deployment whose edge refuses exactly that.

The auth before-hook now refuses the route with 403 SIGN_UP_CLOSED once the deployment holds an account, and keeps the two paths that exist: the first account over HTTP (the setup flow in the browser, or the managed CLI's bootstrap), and every later one server-side — an administrator's Settings > Members door and the deploy's own provisioning reach Better Auth as calls that carry no request and bring their own authorization. The refusal is logged ([sign-up] refused: this deployment has an account), because nothing else records the attempt. The login page already offered setup only while the deployment was empty; the backend now agrees with it, through one shared hasAnyUsers query so the page can never offer a screen the backend refuses.

A throwaway test stack that mints its own accounts over HTTP reopens the route with TALE_ALLOW_OPEN_SIGN_UP=true. The local dev orchestrator, the dev compose overlay and the backend integration check set it for themselves; a real deployment leaves it unset. The managed CLI learned the refusal too: a deploy whose bootstrap sign-up is refused takes back the journal marker it wrote a moment earlier — nothing was created — and says what to do instead of reporting an authentication failure: sign in as the break-glass administrator and set the operator password back to the one the deploy carries.

A provisioned account is a verified account (#3408)

Tale sends no mail and has no self-service sign-up, so an address could never become verified by anything a person or an administrator could do. The setup wizard's owner and every colleague added under Settings > Members started unverified and stayed that way; only enterprise SSO, SCIM, trusted headers or the CLI's operator-attested declaration ever set the flag. Everything that requires a vouched-for address therefore refused those people: identity issuance answered 403 IDENTITY_NOT_ELIGIBLE — after Tale's own login page had already sent them back to the application, so the relying party showed a generic failure — and conversation synchronization and the notification mirror skipped them.

Whoever provisions an account names its address, and on this platform that assertion is the only verification there can be — the same one the CLI records as operator-attested. Every account Better Auth creates here now arrives verified: the setup wizard's owner, a member added under Settings > Members, the deploy's own accounts. A directory-provisioned account (SSO, SCIM, trusted headers) is written straight to the table and keeps its provider's verdict. Connected applications read this as the email_verified claim on the identity Tale issues, so a colleague an administrator just added can sign in to them immediately.

Existing deployments heal themselves. After the numbered migrations and Better Auth's own, boot catches up the accounts this instance provisioned — a credential row is the proof that it issued the password — and logs how many it verified. It is not a numbered migration, because the application's .sql files run before Better Auth's tables exist and do not own them; it runs on every boot, so a rolling deploy cannot leave behind an account the previous image created while the new one was already up. A directory account has no credential row and is left alone. Both halves are proved against real Postgres in the backend integration check.

Five minutes to sign in. The identity provider signs the login and consent continuation with the same lifetime it gives an authorization code, so codeExpiresIn: 60 was also the window a person had to find a password and answer a second factor before the request expired and the provider's error page took over. It is now 300 seconds — still a single-use, PKCE-bound code, within the conventional ceiling. The CLI pins the backend's provider policy before it provisions a managed client, so its expectation moves with the platform: a deploy needs a CLI at least as new as the platform it deploys, which the managed bundle already requires.

A machine address for the deploy operator, and a break-glass administrator (#3406)

A managed deployment (tale deploy --bundle) signs in as its identity operator on every run, with the password alone. That had two consequences. An organization could not enforce two_factor_policy without breaking its own deploys: a password sign-in of a TOTP-enabled operator is answered with a challenge, and an operator with neither factor is held at the enrolment wall once its grace period ends — and the CLI stopped at both with one generic message. And the operator's user id anchors the retained state — the bootstrap and email-attestation journals, managed native client intents, operator-owned skills (which have no ownership transfer) and API keys — so a new account could not take over an existing deployment's deploys.

The way out keeps the account and gives it a machine address, so that people sign in with accounts of their own and choose either factor.

  • identity.migrateEmailFrom — the retained operator's previous sign-in address. Requires bootstrap: "fresh" and must differ from identity.email. The CLI reads the retained account's current address backend-locally (it must be the declared or the previous one, and no other account may hold the declared one), signs in with it and proves the retained user id, journals migratingEmailFrom, renames the account through Better Auth's internal adapter in a write scoped so that only this exact update passes — guarded by email = previous, so a concurrent change is refused rather than overwritten — ends every session of the account, signs in again with the new address and proves the same user id. A declared operator attestation re-verifies the new address, admitting the previous address's completed journal exactly once. A replay after an interruption finds the address already moved and finishes without a second rename; leaving migrateEmailFrom declared afterwards is harmless. Renaming an operator that holds a single sign-on link is refused: the link belongs to the person who signed in with it.
  • identity.breakGlass: { email, passwordHash } — one administrator for when the operator is unavailable. email is a literal or a required environment reference, distinct from the operator's current and previous address; passwordHash is a required environment reference to a Better Auth hash (<32 hex>:<128 hex>). The host resolves both into private provisioning stdin only; the bundle carries the variable names, and the password never reaches the deployment. After the managed organization is selected, every deployment converges on the declaration: an absent account is created with a verified address, and break-glass.json binds its id as soon as the user row exists, before its single credential account is linked — the platform's adapter runs without transactions, so this binding is what makes a partial creation resumable. A credential write (first link or replacement) is journaled as pending beforehand and cleared only after the account's sessions are proven ended, so a run that dies between the write and the sweep still ends the old sessions on replay. An unbound account at the address is adopted only when it is this deployment's own interrupted creation; any other account is refused, and so are the operator and a different account holding a bound address. Membership converges through the organization-scoped member doors — add as admin, or set admin; an owner is never touched. The deployment never signs in as this account, and the ready proof must name it.
  • tale auth hash-password prints that hash from stdin, or from a hidden prompt with confirmation in an interactive terminal. It refuses a password that fails the platform's default password policy and prints nothing but the hash. It is a local helper that manages no instance, so it is excluded from the CLI's version alignment — a re-exec would hand its stdin password to another binary.
  • Two named refusals replace the generic MFA message: The deploy operator has TOTP enabled; a managed deploy signs in with its password alone, so the operator must hold a passkey and no TOTP, and The deploy operator has no second factor and its two-factor enrolment grace period has ended; register a passkey for it. Operator-address and break-glass refusals name the conflict — another holder, a missing account, a retained binding — and never echo credentials.

The new suites drive real Better Auth 1.6.30 — its internal adapter, adapter scoping and transactions — over its memory adapter, and an independent adversarial read before merge found four defects that are fixed in the same change: the role door needs orgId, an interrupted rotation could leave old sessions, a foreign account could be adopted as administrator, and account creation is not transactional on the platform adapter. All of it is documented under Rename the deploy operator's address, Declare a break-glass administrator and Enforced two-factor sign-in on the CLI install page, in English, German and French, and in the CLI readme.

The 2026-09-18 API evaluation findings are closed (#3410)

The tenth external black-box evaluation of the platform API ran against 0.5.32 — about 2,700 requests, zero 5xx, no data loss, no cross-tenant leak, no auth bypass, verdict mature — and rated the website crawler its weakest surface. Every finding was re-confirmed against the code before it was fixed. The four serious ones:

  • Sentry trace headers leaked from the backend — and not only from the crawler. The empty tracePropagationTargets list from the previous round is correct and present, but the deployment's shared environment file carries SENTRY_TRACES_SAMPLE_RATE into the backend containers (the CLI writes it, '0' by default). Any value switches span recording on; the http and fetch integrations then register OpenTelemetry's request instrumentation beside their own, and its propagator derives the target URL from the active span — an unsampled span records no URL, so the target list is never consulted and sentry-trace and baggage (release, public key, environment) went out on every outbound request. The two integrations are now registered with spans: false, which keeps the request lanes on the Sentry-native hooks that honour the target list. Proved on the wire with the sample-rate variable set, with a positive control.
  • The User-agent: TaleBot robots group was ignored. Only the * group was ever read, so a site that followed the documentation and addressed TaleBot by name to refuse or throttle it was crawled under the rules it wrote for everyone else. robots.txt is now parsed for the product token per RFC 9309 — the named group binds, else the * group — with Allow, longest-match precedence, $ end-anchors (a Disallow: /*.pdf$ used to fail open) and Crawl-delay (capped at 60 seconds, paced on both fetch legs and on sitemap reads), honoured by discovery, every continuation link, the rendered-page admission, the retirement pass and the registration-time homepage probe — which also identifies as TaleBot now instead of a bare node user agent. A Disallow: / site no longer has its /sitemap.xml guessed or its homepage walked for links, and a scan that stored no page because robots refused everything lands on the documented error state with that reason instead of reading active with zero pages.
  • A cross-scope webhook redelivery ran twice, silently. The delivery key folded the requested project into its hash, so the same delivery id at the organization door and at a project door produced two keys, two ledger rows and two runs — an operator who moved an automation between organization and project scope had the sender's redelivery run again, and the documented 409 AUTOMATION_DELIVERY_SCOPE_MISMATCH never fired. Migration 0107 adds a scope-free identity_hash; the accept path reads the live rows that share it and answers the 409 when one belongs to the organization door and the other to a project door. Per-project fan-out — the same id starting one run in each installed project — stays legitimate.
  • A third concurrent scan failed the whole scan. An organization's render sessions are a shared budget (two by default), and a third scan that found it spent ended in error with everything it had fetched discarded, and no retry until the next scan interval. The refusal is now a wait: the link polls for a slot every 15 seconds while its window allows, and when none comes it leaves the batch unmarked for the next link, which follows after 60 seconds instead of the usual five. Only infrastructure failures other than capacity still fail the scan.

And the rest of the round, each a small honest thing: Run.failureCode listed budget_exceeded twice (the chat and agent families both name it), which made openapi.json fail OAS 3.0.3's uniqueItems and aborted openapi-python-client by default — deduplicated, with a guard; the MCP cancel_run tool answered already finished for an id that did not exist, which a cancel-until-refusal loop read as success — now RUN_NOT_FOUND, like get_run and REST; a task create's 201 carries Location; a redirect off the registered site is reported as its own host_not_allowed kind instead of wearing the private_ip label (with a Websites-dialog label in English, German and French); a registration with a non-default port is refused instead of silently crawling the wrong origin on 443; website search's total is a real match count rather than the page size; Product.currency accepts any case, as the door always did; /openapi.json carries an ETag and answers 304; and the chat send documents that its 100,000-character cap counts UTF-16 code units.

The governance model pickers are filled from the live catalog (#3411)

On Settings → Governance → Default models, the Add default model rule dialog's Provider dropdown showed No providers found even with provider credentials configured, and the Model access editor's pickers were empty for the same reason. Both read their options through two hooks that the 2026-08 AI-backend rewrite had replaced with stubs returning nothing while the catalog was offline; the catalog came back and already powers the Providers page, but these editors were never reconnected.

Both hooks now read the live provider catalog. The provider list is scoped to the providers the organization holds an active credential for, so a default rule can never point at a provider that could not serve it and the set matches what the Providers page shows; the model-info popover gets the catalog's context window, cost, reasoning, tools and vision.

Behaviour changes

  • POST /api/auth/sign-up/email answers 403 SIGN_UP_CLOSED once the deployment holds any account, unless TALE_ALLOW_OPEN_SIGN_UP=true. The setup flow and the managed CLI's bootstrap still create the first account; Settings > Members and the deploy's provisioning are server-side and unaffected. A refused attempt is logged.
  • Every account Better Auth creates on this platform arrives with emailVerified: true; directory-provisioned accounts keep their provider's verdict. At boot, accounts this deployment provisioned earlier (those with a credential row) are marked verified, every boot, with a log line naming the count. The email_verified claim on an issued identity, conversation synchronization and the notification mirror follow.
  • The identity provider's authorization code — and with it the signed login-and-consent continuation — lives 300 seconds instead of 60.
  • A managed deployment stopped by an enforced two-factor policy names whether it met a TOTP challenge or the enrolment wall.
  • The backend sends no sentry-trace or baggage header on any outbound request, whatever SENTRY_TRACES_SAMPLE_RATE says.
  • The crawler obeys the robots.txt group that names TaleBot over *, honours Allow with longest-match precedence, $ end-anchors and Crawl-delay up to 60 seconds, never fetches a guessed /sitemap.xml or walks the homepage when robots covers them, and fails a scan that robots left with no page to store. The rules persist on the site row as an object rather than a bare list; rows written by earlier scans are still read.
  • A webhook delivery re-posted across the organization/project boundary within its identity window answers 409 AUTOMATION_DELIVERY_SCOPE_MISMATCH; the same id across two installed projects still starts one run in each.
  • A site scan that finds the organization's render sessions spent waits for one (15-second polls within its window) and otherwise defers the batch to its next link 60 seconds later; it no longer ends in error.
  • MCP cancel_run on an unknown run id answers RUN_NOT_FOUND. POST …/projects/{id}/tasks answers 201 with a Location header on a create (an upsert that matched an existing task is a 200 without one). /openapi.json carries an ETag and answers 304 to a matching If-None-Match.
  • A crawl redirect that leaves the registered site is recorded as host_not_allowed, with a message that says so; registering a site with a non-default port answers 400 WEBSITE_DOMAIN_INVALID.
  • POST /api/v1/websites/{id}/search answers a total that counts every matching chunk on the site — it can exceed results.length — instead of the page size.
  • The governance Default models and Model access editors list the providers the organization has an active credential for, with their models and capabilities.

API contract changes

  • 1.14.0 → 1.15.0. The surface is unchanged at 80 paths, 127 operations, 58 schemas and 150 Error.code values; the version moves because a generated client sees the difference. Regenerating the document from this commit reproduces the committed file and its contract fingerprint.
  • Run.failureCode, RunProjection.failureCode and RunSummary.failureCode list budget_exceeded once. The document now validates as OAS 3.0.3, and openapi-python-client generates from it with its default settings. A spec test refuses a repeated enum value from here on.
  • WebsitePage.lastErrorKind gains host_not_allowed — a redirect off the registered site, which the crawler does not follow; it used to be reported as private_ip, whose description now says what it means.
  • GET /api/v1/skills/{slug}/files/{path} declares the conditional GET the door already answered: If-None-Match and If-Modified-Since request headers, ETag, Last-Modified and Cache-Control on the 200, and a 304 response.
  • Product.currency on create and update is ^[A-Za-z]{3}$ — any case in, stored uppercase, as the door always behaved; the old ^[A-Z]{3}$ made a generated client refuse "eur" the door accepts.
  • WebsiteSearchResults.total is documented as the count of all matches across the site, not the size of the answer; this door has no pagination.
  • Automation.createdBy and AutomationVersion.createdBy document system:provisioning for the built-in connector automations (split on the first :; a value without one is a user id).
  • The chat send's content documents that its 100,000 cap counts UTF-16 code units, so an astral character costs two.
  • Not in the schema but on the wire: Location on a task create's 201, ETag/304 on /openapi.json, RUN_NOT_FOUND from MCP cancel_run, and AUTOMATION_DELIVERY_SCOPE_MISMATCH — already in the enum — actually answered.
  • SIGN_UP_CLOSED (403) is a code of the Better Auth door, not of /api/v1, whose surface mounts no sign-up route; the REST registry records it as app-only.

Security

  • Unauthenticated account creation from inside the deployment's own network is closed (#3407). The sign-up route answered whoever could reach the backend, and the sandbox network could. The measured result was a new account with a live session token; that account belonged to no organization, so it read no tenant data, but it was a foothold on the deployment, and an address created that way is one an operator can no longer declare for a machine account. The route now refuses once an account exists, and the refusal is logged. One limit is stated in the code rather than hidden: on an empty deployment the probe and the insert are separate statements, so simultaneous first sign-ups are all admitted.
  • The backend no longer leaks its Sentry release, public key and environment to third parties (#3410). The previous round's fix held on the render leg only; with SENTRY_TRACES_SAMPLE_RATE in the environment — which every CLI-written deployment has — the plain fetch leg, and every other outbound request the backend makes, carried sentry-trace and baggage. Registering the http and fetch integrations with spans: false closes it for the whole backend, and the wire test now runs with the variable set.
  • What email_verified means on this platform (#3408). A relying party reading the claim from a Tale-issued identity should know it now says an administrator or the operator named this address, not the person clicked a link — Tale has no link to click. Accounts from a directory (SSO, SCIM, trusted headers) carry their directory's verdict, unchanged.
  • The break-glass administrator's password never reaches the deployment (#3406). Only its Better Auth hash travels, resolved on the host into private provisioning stdin; the deployment never signs in as that account; any credential change ends the account's sessions, also when a run finishes an interrupted one; a foreign account at the declared address is refused, never adopted.
  • The authorization-code lifetime is 300 seconds (#3408), up from 60, for a single-use, PKCE-bound code that also gates the login and consent continuation — within the conventional ceiling.
  • A crawl redirect that leaves the registered site is refused under its own name (#3410), and a non-default port is refused at registration; neither is a new refusal, but a public off-site redirect is no longer reported as a private-network attack, and a site can no longer be registered at host:8443 and crawled on 443.
  • A cross-scope webhook replay is refused (#3410): the sender's redelivery no longer runs an event a second time after an automation moved between organization and project scope.
  • No dependency advisory is fixed in this release, and no dependency changes in this range. One advisory is open against a runtime dependency: @better-auth/oauth-provider 1.6.30, the identity provider behind Continue with Tale, is affected by CVE-2026-67332 / GHSA-p2fr-6hmx-4528 (CVSS 6.4, medium) — a client could request an access token for a different allow-listed audience than its authorization covered. The platform already applies the advisory's own workaround and has since the provider shipped: validAudiences is empty, so no resource outside the deployment's own can be minted, and only the authorization_code grant is enabled, so the refresh-grant path does not exist. The upgrade to 1.7.0 is a breaking change with a schema migration and is tracked as a separate dependency pull request, not carried here.

Known issues

  • The sign-up gate has a first-boot race. On a deployment with no account at all, simultaneous first sign-ups are all admitted; the gate closes once one exists. The setup flow is the only intended caller of that window.
  • A managed deploy that cannot authenticate its operator can no longer mint a replacement administrator — that was the hole. The recovery is the break-glass administrator: sign in as it and set the operator password back to the one the deploy carries, as the CLI's refusal says.
  • The boot catch-up asks nobody. Every account with a credential row that this deployment holds becomes verified at the first boot of 0.5.33 (and any it missed at every later boot). There was no state on this platform in which an unverified local account was intended, but an operator who relied on the flag being false for such accounts should read the Security note above.
  • Break-glass limits, each documented on the CLI install page: the deployment records no password-change time for the break-glass account, so an organization password rotation policy may ask it for a new password, which the next deployment sets back; renaming an operator that holds a single sign-on link is refused until the link is removed; the backend-local suites drive real Better Auth over its memory adapter, not the platform's Postgres adapter.
  • The cross-scope webhook guard governs deliveries from here on. Rows written before the upgrade carry no identity_hash and never match, so a delivery first taken on 0.5.32 and re-posted across scopes on 0.5.33 still runs; the guard applies within the delivery's identity window — 24 hours for a header-named id, two minutes for a body-hashed one.
  • A site's robots policy upgrades at its next scan. Until then the row holds the bare Disallow list an earlier scan stored, read as no Allow rules and no delay. Crawl-delay is honoured up to 60 seconds; a site that asks for more gets 60.
  • A scan waiting on render capacity takes longer, by design, and can still end in error if its continuation budget runs out with pages waiting — the reason then says so (N page(s) were still waiting when the scan's continuation budget ran out). The organization's render-session budget itself is unchanged.
  • The Sentry fix is proved by a reproduction and the wire test, not yet on a fleet host — no deployment runs this release yet. The check is the same as the evaluation's: a logging origin behind the crawler should see no sentry-trace or baggage header on either fetch leg.
  • The governance pickers list only providers with an active credential. A provider whose credential is missing, expired or disabled does not appear in Add default model rule or Model access, even though the catalog ships it. That is the intended reading; the alternative (every shipped provider) is a one-line filter change if it turns out wrong.
  • One dependency advisory is open (@better-auth/oauth-provider, above); the workaround is in place, the upgrade is not.
  • A page is still fetched three to four times per scan — the plain probe, the render pass and, for the homepage, the create-time metadata probe. The robots $-anchor and Allow half of that ledger item is paid down by this release.
  • Unchanged from v0.5.32, where each is described in full: the embedding pacing is proved against a controlled server, its bound is per Tale process, and minTokensPerSecond is a statement nothing verifies; the Kubernetes page's verified scope is one kind cluster, config-data needs RWX or a single node, and Tale ships no Helm chart.
  • Unchanged from v0.5.31, where each is described in full: a managed deployment picks up that release's proxy policy only when a newly prepared bundle is applied; the transcription setting is only as good as the organization's credentials; the six agent-turn fixes are bounded by the pinned Claude Code build they were read from; the 0.5.29 proxy change has been exercised live in TLS_MODE=letsencrypt only; the web tier's backend-URL default lives in the image, not in the generated compose; the scheduled-pack fix does not reach an automation an organization already has; a budget hold covers a turn's first round only; a run still carries no usage or cost; nothing backfills a task timeline.
  • 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; 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.
  • Cloud sync, left for later: there is still no Sync now action — the cadence is the fifteen-minute scan, so a reconnected account waits for the next run. A config whose owner leaves the organization is still deactivated silently by a different door, and a source-deleted item is still a status stamp with no bell.
  • Documents indexed before 0.5.27 keep one vector per repeated passage until they are re-indexed; the content hash is unchanged, so only an explicit retry-indexing (or a content change) re-embeds them.
  • The rail's navigation memory has had part of its manual round: the R5 round drove six EN/DE/FR desktop and phone cases covering parts of NAV-F16–NAV-F19; the remaining section, the second-account cases and NAV-B6–NAV-B9 are still unrun.
  • A reply-language directive is a directive: a model may still answer in the prompt's language and nothing on the wire marks a slip.
  • No image input on the REST chat send. A vision model reads an image over REST only on a thread the app continued with an image attachment; the design of an attachments field on the send is recorded as contract debt.
  • No REST door authors or deploys an automation — POST /automations answers 405 by design. Build and deploy in the app, or over the MCP endpoint's save_automation and deploy_automation; the REST key lists, reads, runs and wires triggers.
  • 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.
  • The app's zip upload of a skill bundle rewrites the bundle and moves updatedAt even when the zip is byte-identical, where PUT /skills/{slug} writes nothing.
  • A tool call the reply cap cut keeps input: {} on the stored tool-call part; the raw text the model emitted is still not on the transcript.
  • Folder names written before 0.5.24 keep their bytes; a sync engine's hub-path lookup can create an NFC twin beside a legacy NFD folder. No backfill ships.
  • Two bounded document readers still filter after their cut; both report an honest truncated, so a caller can tell the answer was cut.
  • 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 ip6tables and the reverse proxy's network is IPv6-enabled — an operator item, documented on the Own Compose page.
  • Recorded as contract debt, each with its design in the ledger: a queued send is invisible on the message list until a worker opens it; a webhook delivery the deployed inputs schema refuses moves no trigger stamp; the MCP run_deployed tool keys its idempotency apart from start_run and REST; a cancelled run answers trace: null and effects: null where a failed run answers both; approvals and asks have no REST twins; a task cannot be archived or deleted over REST; a webhook bind does not say whether the deployed inputs schema admits a delivery; an exhausted repeatUntil is only a trace note; Website carries no scanStartedAt and the crawler has no page cap, path filter or stop verb of the caller's; website search has no dense leg and its substring fallback stamps score: 0 (its total is now honest); no Idempotency-Key on the task start; no queue position on a queued send; a corrupt Office document still fails as indexer_error and is retried five times where a PDF lands malformed; no /.well-known/security.txt; no changelog feed on tale.dev; no SDK, collection or per-code table beyond the Error.code enum; GET /notifications rows carry type as a free string and nothing pushes them to a machine caller; a skill keeps no version history on the machine door; the per-task circuit breaker is not built; the messages a conversation snapshot applied are readable only in the app.

Migration notes

  • One migration, forward-only and rolling-safe. 0107 automation_webhook_delivery_identity on the application database, applied by the backend at boot inside the advisory lock while the previous image keeps serving: ALTER TABLE app.automation_webhook_deliveries ADD COLUMN IF NOT EXISTS identity_hash text and an index on (trigger_id, identity_hash, expires_at_ms). The column is nullable, the previous image neither writes nor reads it (its rows simply do not take part in the new guard), and no backfill is possible or needed. The application database moves from 0106 to 0107; the knowledge database is unchanged. There is no down migration by design.
  • One boot-time catch-up that is not a numbered migration. After the migrations, the backend marks verified every account that holds a credential row and is not yet verified, and logs verified N provisioned account(s) when it changed any. It runs on every boot and matches nothing once they are all verified.
  • One new environment variable: TALE_ALLOW_OPEN_SIGN_UP. Unset on a real deployment. Set to exactly true only on a throwaway test stack that mints its own accounts over HTTP; the local dev orchestrator, the dev compose overlay and the backend integration check set it for themselves. Documented in .env.example and in the environment reference in English, German and French.
  • Two optional additions to the managed deployment specification, identity.migrateEmailFrom and identity.breakGlass, and one new CLI command, tale auth hash-password. A specification that declares neither behaves as before.
  • One new error code, SIGN_UP_CLOSED (403), on the Better Auth door only.
  • No image in the stop-gated tier changes. The proxy and db images carry no source change, and neither does the managed proxy policy the CLI renders. A plain tale deploy is the whole upgrade: no --stop, no downtime window.
  • The platform image (the sign-up gate, the verified-account rule and its boot catch-up, the provider lifetime, the evaluation fixes, the governance pickers) and the docs image (five pages in each of English, German and French: the CLI install page, first administrator, authentication, the environment reference and members and roles) carry source changes. web and ui-docs rebuild with no change of their own. The db, proxy, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images carry no source change.
  • The CLI has source changes in this range — the operator rename, the break-glass administrator, auth hash-password, the sign-up refusal and the provider-policy pin — so a managed deployment must move its pinned CLI reference as well as its platform reference. A CLI from before this release refuses to provision a managed client against this platform: its expectation of the provider's code lifetime moved with the platform. The release executables report 0.5.33.
  • @tale/ui and @tale/marketing-ui are pinned by this release as the ui-v0.5.33 and marketing-ui-v0.5.33 tags on their snapshot branches; a consumer outside the monorepo installs "@tale/ui": "github:tale-project/tale#ui-v0.5.33". Neither package has a source change in this range, so both tags are content-identical to their 0.5.32 predecessors.

Upgrading

  • On the 0.5 line (0.5.0 – 0.5.32):

    tale update
    tale deploy

    The migration and the account catch-up run at boot. Nothing in this release needs --stop. A deployment crossing from a version older than 0.5.29 should read that release's notes, which do: its proxy image change is only applied by a --stop deploy.

  • 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 first: a CLI older than this release refuses to provision a managed client against a 0.5.33 platform. The bundle's backend-local phases run under the interpreted CLI (cli/tale.mjs) that the setup-cli action and bun run --filter @tale/cli build produce beside the executable; the executable from the release page has no interpreted bundle beside it and cannot prepare a managed bundle. 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.

  • Enforcing two-factor sign-in on a managed organization? Give the deploy operator a passkey and no authenticator app before you enforce the policy, or the next deploy stops at the challenge — the refusal names which wall it met. Then consider giving the operator a machine address with identity.migrateEmailFrom, and declaring identity.breakGlass with a hash from tale auth hash-password, so people sign in with their own accounts and a person can still recover the deployment. All three are under Managed deployments on the CLI install page.

  • 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.

  • Running a test stack that creates its own accounts over HTTP? Set TALE_ALLOW_OPEN_SIGN_UP=true on it, or every account after the first is refused. Never set it on a real deployment.

What's Changed

  • feat(cli): rename the deploy operator and declare a break-glass administrator by @yannickmonney in #3406
  • fix(platform): close sign-up once a deployment has an account by @yannickmonney in #3407
  • fix(platform): treat a provisioned account as a verified account by @yannickmonney in #3408
  • fix(platform): close the 2026-09-18 round-J API evaluation findings by @larryro in #3410
  • fix(platform): fill the governance model pickers from the live catalog by @larryro in #3411

Full Changelog: v0.5.32...v0.5.33