Skip to content

Tale v0.5.36

Choose a tag to compare

@larryro larryro released this 19 Sep 06:31
c1b3f90

0.5.36 carries 1 merged pull request. A team becomes what it always claimed to be: a label on work that says who may see it, and a queue a conversation can wait in — never a workspace to switch into. A document, folder or project carries the teams that may see it; empty means the whole organization, and Owners and Admins see everything. One rule states this once and every door reads it, including the doors that ignored teams until now — WebDAV, folder creation, the cloud imports, the document retag lane. The Team switcher in the account menu is gone; in its place a read-only Teams row opens Settings > Account > Your teams, and the document, project and inbox lists gain Teams and queue filters that live in the page address. Deleting a team happens in one transaction with a preview of what it touches, a team keeps at least one member, and a team an identity provider provisions shows Synced and is not edited by hand. Governance rules for a member of several teams combine one way — a limit to the strictest, a permission list as a union with a block winning, a single choice by list order — and the chat budget banner reads the same standing the gate enforces. REST carries the audience as teamIds on projects and documents (API contract 1.17.0). One migration (0109); no image in the stop-gated tier changes, so the upgrade is tale update followed by a plain tale deploy.

Highlights

A team is an audience, not a workspace (#3422)

A member of two teams could "switch team" in the account menu and nothing changed: the row was a client-side filter dressed as a context switch — it redirected to chat, hid nothing organization-wide and showed the chosen team nowhere — while the backend used "team" for six different jobs whose rules disagreed with each other. A document carrying a team_id beside an empty tag list read as organization-wide on one door and as team-only on the next.

The model is now one sentence, stated once in core/lib/audience.ts and embedded by every list door: a document, folder or project carries teamIds, the teams that may see it; empty means every member of the organization; Owners and Admins see everything regardless. A team folder owns the audience of everything inside it — a document filed there takes the folder's teams and cannot name a team outside them (TEAM_INHERITED_FROM_FOLDER). Naming a team is bounded too: an id that is not one of the organization's teams is refused (TEAM_NOT_IN_ORG), and a member who is not an Owner or Admin can only restrict work to teams they belong to (TEAM_ACCESS_DENIED). Projects fold their owning team and shared teams into one team_ids set; a conversation keeps a team as its queue. Knowledge retrieval carries the same rule, so an Owner's or Admin's search reaches every team library, and the org_<id> pseudo-team that stood in for "organization-wide" is gone.

The switcher and its provider are deleted. The account menu's Teams row names the teams you are in (two names, then +n; No teams for an account in none) and opens the account page's new Your teams section, which says what teams decide and links Owners and Admins to Settings > Teams. Documents and projects gain a Teams filter — Organization-wide, My teams, and every team of the organization by name — kept in the page address as ?teams=… so a filtered list can be bookmarked; the inbox shows each conversation's queue as a chip and gains Filter by queue (?queue=…) with All queues, My teams, Unassigned for administrators, and each team by name. A project shows its audience by name in the list's Sharing column, is given one under Who can see it on create and under Audience on General, where removing a team asks for confirmation because members outside the remaining teams lose access; clearing every team widens the project to the organization and saves at once. Every surface resolves names through one read, GET /api/app/teams/directory, which names every team of the organization to any member — nobody looks at blanks or raw ids for teams they are not in any more.

Every door reads the one rule (#3422)

Several doors ignored teams altogether. WebDAV resolved paths and listed collections for the credential's organization without asking which teams the credential's owner was in, so a team library was one PROPFIND away for any member holding a WebDAV credential. The handlers now resolve the owner's audience (fail-closed: no membership, no team rows), filter listings in the SQL statement, stamp a PUT with the landing folder's audience, and refuse a DELETE, COPY or MOVE whose tree holds a team folder or document the caller cannot see — refused whole with 403 rather than done half-way, so a hidden subtree can never be re-homed under a wider audience; an Admin's copy under an organization-wide destination keeps each row's own audience. Folder creation, the OneDrive and Google Drive import doors and the document Assign team lane accepted any team id, including another organization's; each now validates through the same assignment rule an upload obeys, and a team skill's teams list is validated the same way when it changes. Two team-membership lookups — the mention directory's and the team doors' — read memberships without joining the organization's teams, so a membership in another tenant's team could count; both join team on the organization now. A cloud-sync refresh used to re-stamp a synced document with the pipeline's single team, lifting a restriction an administrator had set by hand; it now leaves the stored audience alone unless the landing folder owns one, and an import into a team folder takes the folder's whole team list.

Deleting a team is one transaction, with a preview (#3422)

Better Auth's team endpoint deleted the team row and its memberships and nothing else; the projects, folders, documents, queues and sync configurations the team scoped have no foreign key to it, so they stayed pointed at a ghost nobody could satisfy until a daily sweep ran — and a door that failed half-way left the same ghost. DELETE /api/app/teams/:id now retires every scope, the identity-provider provenance, the memberships and the row in one serializable transaction, audited as team.deleted with the counts of what it retired; GET /api/app/teams/:id/impact previews what a deletion touches — members, projects, folders, documents, queued conversations — and how many of those items have no other team and become visible to everyone. The confirmation dialog shows those numbers. SCIM's group deletion and the single-sign-on group reaper retire the team's scopes inside the transaction that deletes the team. The daily teams.repair_scopes sweep is removed and unscheduled at boot, and migration 0109 cleans the ghosts one last time. The administrator doors never take a team's last member (409 TEAM_LAST_MEMBER; delete the team instead), and a team an identity provider provisions shows Synced with a read-only edit dialog, because a local change would be undone by the next synchronization — deleting it locally still works.

How rules combine for a member of several teams (#3422)

Each governance policy combined a member's team rules in its own way; the budget rule took the most permissive team cap, so joining a lenient team raised a member's personal cap. rule_precedence.ts now states the rule once and every policy reads it: the most specific scope wins — user, then team, then role, then default — and among a member's team rules a limit combines to the strictest value, a permission list to the union of allowed models with a block anywhere winning for that model, and a single choice such as the default model to the first matching rule in the table's order. The chat budget banner derives its warnings from the same standing buckets the admission gate walks — the personal cap against the reader's own usage, each team's shared cap against that team's aggregate at the team rule's own threshold, the organization's against the organization's — so it can never announce a standing the gate would not enforce, and a team's shared cap reads Team name: … left. The usage ledger no longer records a team on a row (a team's usage is read through its membership), and the per-user usage table drops its Team column. The Policies and limits page documents the combination in a new How rules combine section, in English, German and French.

Projects and documents carry their audience over REST (#3422)

Project.teamIds joins the project payload, always present (empty = organization-wide), and teamIds is accepted on POST /api/v1/projects (teams of the organization; for a key holder who is not an admin, their own) and on PATCH /api/v1/projects/{id}, where it replaces the audience whole and is an organization-admin verb like archived. Document.teamIds joins the document payload beside the single teamId, now marked deprecated as the pre-1.17.0 spelling; teamIds is accepted on the document create and patch bodies, teamId still is, and inside a team folder the folder's audience applies. Three codes join the machine contract: TEAM_NOT_IN_ORG (400, data.teamIds names the strangers), TEAM_INHERITED_FROM_FOLDER (400) and PROJECT_SHARING_INVALID (400: more than the allowed number of teams, a duplicate or blank id, data.unknownTeamIds). The OpenAPI document names each on the operations that answer it, and the manual error-code register carries their rows.

Behaviour changes

  • A member of several teams gets the strictest personal budget and context cap among their team rules; it was the most permissive. Model access combines as the union with a block winning; the default model follows the table's order.
  • Owners and Admins see every hub document and folder, in the library, over WebDAV and in knowledge retrieval; there was no administrator bypass on the hub before. Nothing changes for other members: a document or folder is visible to the members of any of its teams, or to everyone when it carries none.
  • A document or folder stamped only through the single team_id column, with an empty tag list, is team-scoped after migration 0109; it read as organization-wide on one door and as team-only on the next.
  • A member who is not an Owner or Admin can restrict a document, folder, project or team skill only to teams they belong to; an id that is not one of the organization's teams is refused everywhere it can be named.
  • A document inside a team folder cannot name a team outside the folder's audience; moving one in applies the folder's teams, as an upload already did.
  • A WebDAV DELETE, COPY or MOVE whose tree holds a team folder or document the caller cannot see is refused whole with 403; a WebDAV listing shows only what the credential's owner may see.
  • The Team switcher in the account menu is gone, and with it the per-browser remembered selection; the Teams row is read-only and opens Settings > Account > Your teams. Lists narrow by team through Teams filters kept in the page address; the inbox filters by queue, and a member sees no Unassigned option and no unassigned conversations.
  • Every member can read every team's name through GET /api/app/teams/directory; the team list under Settings > Teams and a team's members keep their rule.
  • Deleting a team is atomic and previews its impact; the daily ghost-team sweep is gone. A team keeps at least one member: the member-removal doors answer 409 TEAM_LAST_MEMBER.
  • A team an identity provider provisions shows Synced and opens a read-only edit dialog.
  • Clearing every team on a project saves without a confirmation; removing a team while others remain confirms first.
  • A cloud-sync refresh no longer lifts a team restriction set by hand; an import or refresh into a team folder takes the folder's whole team list.
  • The chat budget banner reads every cap that binds the reader and adds a Team name line for a team's shared cap; the per-user usage table no longer shows a Team column, and the usage ledger stops recording a team.
  • Over REST, Project.teamIds is always present and teamIds is accepted on project and document writes; PATCH …/projects/{id} with teamIds needs an organization admin.

API contract changes

  • The OpenAPI document moves from 1.16.0 to 1.17.0: still 85 paths, 133 operations and 62 schemas; no path or operation is added or removed. Four request bodies change: POST /api/v1/projects and PATCH /api/v1/projects/{id} accept teamIds (at most 21 ids of at most 128 characters each); POST /api/v1/documents and PATCH /api/v1/documents/{id} accept teamIds (at most 64) beside teamId. Every operation that answers a Project or a Document answers teamIds.
  • Project gains teamIds (required, array of strings). Document gains teamIds (array of strings), and its teamId is marked deprecated — the first team of teamIds, or null. DocumentInput and DocumentPatch gain teamIds; on the patch it replaces the audience whole and [] makes the document organization-wide.
  • The Error.code enum grows from 164 to 167 values: PROJECT_SHARING_INVALID, TEAM_INHERITED_FROM_FOLDER, TEAM_NOT_IN_ORG. The first two were app-only codes before and are on the machine contract now; TEAM_NOT_IN_ORG is new.
  • X-Tale-Api-Version answers 1.17.0. Every change is additive — new optional request fields, one new response field on two schemas, three new codes — so a client generated from 1.16.0 keeps working; a client that rejects unknown response fields has to regenerate.
  • One new code stays app-only, on /api/app/teams: TEAM_LAST_MEMBER (409, removing a team's only member). FOLDER_TEAM_FORBIDDEN keeps its place on the folder doors.

Security

  • Several doors enforced the team audience inconsistently or not at all (#3422). The WebDAV handlers listed and resolved every hub row of the organization for any member holding a WebDAV credential, and a COPY or MOVE could re-home a hidden team subtree under a wider audience; folder creation, the two cloud-import doors and the document retag lane accepted a team id without asking whether it was the organization's or the caller's. Every one of them now reads core/lib/audience.ts: the viewer's audience is resolved fail-closed, list doors filter in the statement, and every write validates its team ids. Two membership lookups that could count a team granted by another tenant now join the organization's teams.
  • Owners and Admins gain read access to every team library, in the hub, over WebDAV and in retrieval, matching what they already had for projects. This is a deliberate widening: a team is documented as never being a way to hide work from administrators.
  • Every team's name is readable by every member through the directory endpoint — names only, never members — so filters and audience chips can show names. Membership and the Settings > Teams list keep their admin-or-own rule.
  • Ghost team ids are gone. The scope columns have no foreign key to Better Auth's team table; migration 0109 removes every id that is not one of the row's organization's teams from documents, folders, projects, conversation queues and sync configurations, and from here on team deletion is one transaction and every write validates its ids, so the daily repair sweep is retired rather than kept as a safety net.
  • No dependency changes in this range, and no advisory is fixed. The @better-auth/oauth-provider advisory noted in 0.5.33 (CVE-2026-67332 / GHSA-p2fr-6hmx-4528, medium) remains open with its workaround in place; the 1.7.0 upgrade is still a separate dependency pull request.

Known issues

  • Automation files: mounts and workflow document.* steps do not apply the team audience yet; they read documents as they did before this release. A single-sign-on sign-in that arrives with an empty group list still revokes nothing, and a SCIM group replace still overwrites members an administrator added by hand without saying so; both are left for a later release.
  • The legacy mirror columns stay — team_id on documents and folders, team_id and shared_with_team_ids on projects, team_id on the usage ledger. The first four are still written as derived mirrors so the previous image keeps working mid-roll, and the project readers fall back to the legacy pair while team_ids is still empty on a row the previous image wrote; the columns are dropped in a later release once nothing reads them.
  • The three GIN indexes of migration 0109 are built inside the migration transaction, without CONCURRENTLY; a large tenant holds a share lock on app.documents, app.folders and app.projects for the seconds the build takes.
  • The browser rounds for the Teams row, the account section, the three filters, the project audience pickers, the synced-team lock and the atomic delete (NAV-F6, SET-F18, SET-F19, SET-F42, KNOW-F20, PROJ-F23, PROJ-F24, CONV-F12) are manual; the audience rule, the delete preview and the three deletion lanes, the last-member rule, the assignment and inheritance refusals, the WebDAV refusals and the migration's idempotence are proved against real Postgres in the backend integration check.
  • A team skill's teams list is validated only when it changes, so an edit that leaves the list alone does not fail on a team the organization has since deleted; the deleted id simply matches nobody.
  • The REST Document.teamId stays as the deprecated single-team spelling; nothing removes it in this release.
  • Unchanged from v0.5.35, where each is described in full: a frame carries the signed-in session only from a same-site host page and the shell's embedding policy is the union across organizations; revoking a trusted-header key or turning the card off ends no session; the AUTH-F21–AUTH-F24, AUTH-B10 and SET-F41 rounds are manual; approvals have no REST twin; moving a folder has no door and documents already at the root stay there; the auto-retry resumes only a turn that announced its conversation handle; the Google Drive row counts a deployment app from either lane.
  • Unchanged from v0.5.34, where each is described in full: a managed deployment gets the organization-creator behaviour only once its specification declares organizations.creators and a new bundle is applied; the AUTH-B9 and AUTH-F20 rounds are manual; the creator list is matched against sign-in addresses.
  • Unchanged from v0.5.33, where each is described in full: the sign-up gate's first-boot race; the boot catch-up that marks provisioned accounts verified asks nobody; the break-glass administrator's password-rotation, single-sign-on-link and memory-adapter limits; the cross-scope webhook guard governs deliveries from that release on; a site's robots policy upgrades at its next scan; a scan waiting on render capacity takes longer by design; the governance pickers list only providers with an active credential; one dependency advisory is open.
  • 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, answers asks 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 page is fetched three to four times per scan; a cancelled run answers trace: null and effects: null where a failed run answers both; approvals have no REST twin; 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; 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, 0109 (0109_team_audience.sql): adds team_ids to app.projects and backfills it from the owning and shared teams; makes the tag array authoritative on app.documents and app.folders (a row stamped only through team_id gets team_tags = [team_id]); re-derives the mirror columns from the arrays; removes, once, every team id that is not one of the row's organization's teams from documents, folders, projects, conversation queues and the OneDrive and Google Drive sync configurations (guarded so a fresh database, where the application migrations run before Better Auth creates its tables, skips the sweep); and creates three GIN indexes for the per-team lanes. Every statement is IF NOT EXISTS or an UPDATE whose WHERE matches nothing on a second run, no updated_at_ms moves, and the previous image keeps serving while it applies. The application database moves from 0108 to 0109; the knowledge database is unchanged. Better Auth adds no column this time.
  • One scheduled job is retired: teams.repair_scopes, the daily ghost-team sweep. The schedule is removed at boot, so a deployment upgrading in place stops running it without an operator step.
  • No environment variable is added or removed; .env.example is unchanged. No organization configuration file changes; the seed catalog is untouched.
  • Three error codes join the machine contract and one app-only code appears; see API contract changes. New audit action: team.deleted, carrying the counts of what the deletion retired.
  • No image in the stop-gated tier changes. The proxy and db images carry no source change, and the managed proxy policy the CLI renders is unchanged. A plain tale deploy is the whole upgrade: no --stop, no downtime window.
  • The platform image (the audience rule, the doors, the atomic delete, the governance precedence, the frontend) and the docs image (six pages in each of English, German and French: Teams, Documents, Manage your account and preferences, Project concepts, Policies and limits, Models) carry source changes. The web, ui-docs, db, proxy, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images carry no source change.
  • The CLI has no change of its own in this range, but the reference tree it embeds — the platform's shared modules and its core, and the shared project schema — does, so the release executables are rebuilt and differ from 0.5.35; they report 0.5.36. Nothing in the range is new for an older CLI to refuse. A managed deployment should move its pinned CLI reference together with its platform reference, as always.
  • @tale/ui and @tale/marketing-ui are pinned by this release as the ui-v0.5.36 and marketing-ui-v0.5.36 tags on their snapshot branches; a consumer outside the monorepo installs "@tale/ui": "github:tale-project/tale#ui-v0.5.36". Neither package changes in this range, so both tags are content-identical to their 0.5.35 predecessors.

Upgrading

  • On the 0.5 line (0.5.0 – 0.5.35):

    tale update
    tale deploy

    Migration 0109 is applied at boot. Nothing in this release needs --stop. A deployment crossing 0.5.35 runs that release's migration 0108 and Better Auth's session column at boot as well; one crossing 0.5.33 runs migration 0107. 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.

  • After the upgrade, review who sees what. A document or folder that carried a single team stamp without a tag list is team-scoped now; a member of several teams with budget or context rules is held to the strictest; Owners and Admins can open every team library. Under Settings > Teams, a delete previews its impact before anything is removed. Members find their teams under Settings > Account > Your teams and narrow lists with the Teams filters; nobody has to "switch team" any more.

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

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

What's Changed

  • feat(platform): teams as audiences: one visibility rule, atomic delete, honest UI by @larryro in #3422

Full Changelog: v0.5.35...v0.5.36