Repository navigation
v0.3.0
This release puts a permission on every door: each API endpoint and MCP tool now
names the Django permission its act needs, tokens are held to their scopes, and
the site becomes an OAuth 2.1 authorization server so outside clients can act
for a person with their consent. It also brings a shared component layer
(surfaces, buttons, fields, drawers, list pages), Greek, and the authentication
pages on django-allauth's own templates.
Upgrading from 0.2.0 — read these first:
- An endpoint that declares no
auth=now refuses everyone (see Security).
Annotate each of your own endpoints withguarded(...),scoped(...)or
authenticated(). - Every list the API serves answers
{"items": [...], "total": N}one page at
a time, and several API paths moved (/api/content/v1/is now/api/cms/v1/).
Update clients and runcollectstatic. - Tokens issued with
"*"stop working wherever a scope is checked; re-issue
them as unrestricted or with the scopes they need. request.authis anAuthorizationContext, not aUser.- An app declares its API and MCP tools one way only, by convention; the
api_version_routerattribute, thephoxtail.mcp_modulesentry point,
[mcp] extra_modulesandpage_schema_contributorsare gone. - Field templates moved to
phoxtail_core/forms/fields/, several fields were
folded intoinput,choicesandselect, and thefw-classes are now
phx-. - Deployed sites: regenerate the Dockerfile (uv 0.13.5) and the production
compose (pinned nginx and certbot), and replace the certificate-renewal cron
line (see Fixed).
Added
-
The site is now an OAuth 2.1 authorization server (django-oauth-toolkit, in the
engineextra), declared as a surface ofphoxtail.tokens: the RFC 8414
discovery document at/.well-known/oauth-authorization-serverand the
endpoints under/o/. A hatched project picks it up on upgrade with nothing
edited — the dependency, the URL mount and theOAUTH2_PROVIDERdefaults
arrive through the app protocol, andmigrateruns on every start as it
already does. Itsissueris the exact name the MCP server advertises.- Outside clients ask for bundles, never permission codenames: one
<app>:read/<app>:writepair per phoxtail app, derived from the
codenames the app's endpoints and tools already name (write includes read),
so a renamed model changes no issued credential's meaning. The discovery
document and the MCP server's protected-resource document list every
bundle; a request for anything else — the library's placeholderread, a
codename, or nothing at all — is refused asinvalid_scope. The consent
screen is phoxtail's: it names the client by the host of its metadata URL
when it has one, names the host the browser will be sent to, says so when
that is this device, and lists what the client would be able to do.
Approval takes the requested set whole. - A client the site has never met can introduce itself, granting nothing
either way: by document — itsclient_idis anhttpsURL the site fetches
through the library's SSRF-hardened fetcher, and the consent screen names
that host; grant types the server does not offer are narrowed away, as
RFC 7591 allows — or anonymously at/o/register/(RFC 7591, with RFC 7592
read-back). Both create a public client that still sends every person to
the consent screen. An operator who wants only listed hosts to register by
document sets the library's allowlist class and hosts in settings. - Public clients and loopback callbacks. The discovery document lists
noneamong the token-endpoint authentication methods (a phone app or a CLI
proves itself with PKCE), a client listening onhttp://localhost:<any port>matches one that registered another loopback port (RFC 8252), and
every authorization response carriesiss(RFC 9207). - Every RFC 9700 gate the library defines is decided. The implicit grant,
the password grant and plain PKCE challenges are refused; a key in the query
string is refused; replay protection, exact redirect matching and required
PKCE are deploy-check errors. A plaintext redirect scheme stays allowed only
because a CLI's callback ishttp://localhost: anhttpredirect to any
other host is refused whatever a client registered. A test fails if a
future library release adds a gate nobody has decided. - Tokens live short and are stored hashed: an hour for access, a week for
refresh (renewed on every use), and a replayed refresh token revokes
everything descended from its grant. There is no grace window for a retried
refresh.manage.py sweeptokensremoves dead token rows of both families
after 90 days (--retention-days); nothing schedules it yet. - System checks
E004–E007fail startup if a project's own settings
displace any of this: the scope backend or validator, the resource check,
either registration door, or the kept redirect gate.
- Outside clients ask for bundles, never permission codenames: one
-
The API reads a key the authorization server issued after a person's consent,
so a client holding one can act within the bundles approved and the person's
own permissions. OneAuthorization: Bearerheader is routed by the key's
shape (phoxtail's own keys carry thephxt_prefix), and either kind is read
into one credential. Bundles are valid on phoxtail's own keys too —
cms:readbeside a codename in the admin or the CLI. A key issued for the MCP
server (resource=) is honoured at the API behind it, compared canonically, so
a key for another host is refused. -
The MCP server is reachable from the internet, at
https://mcp.<domain>/mcp
in production andhttp://mcp.<slug>.localhost/mcplocally. Its401points
at its protected-resource metadata (RFC 9728), which names the site as the
authorization server, so a remote client finds where to authenticate from the
MCP URL alone. Themcpservice joins the production compose;
phoxtail nginx create productionwrites a server block that passes requests
through exactly as sent;phoxtail ssl obtainandphoxtail server ssladd the name to the
site's certificate (--expand)./o/register/and/o/authorize/are
rate-limited per client address at the edge (ten a minute, burst twenty).
phoxtail env create productionlistswebinALLOWED_HOSTS. An
already-deployed project needswebadded toALLOWED_HOSTSby hand, the DNS
record formcp.<domain>, a regeneratednginx.conf, the certificate
obtained again, and both containers restarted. -
Token scopes are enforced. An endpoint names the codename of its act once with
auth=guarded("wagtailcore.publish_page"), which asks both halves: whether
the person holds the permission, and whether their credential covers it.
scoped(...)asks only the credential, andauthenticated()admits any
caller whatever their credential's ceiling, for an endpoint that genuinely
asks for no permission (an anonymous caller still gets 401). A narrowed
credential and a missing permission are refused with different messages.
Whether the person may act is still also decided where it already was —
Wagtail resolves publishing per page subtree, and that check is unchanged. -
A token's scopes are validated when it is issued: a scope names a real Django
permission or a bundle, and a typo is refused with the correct spelling. The
API token create form offers an Unrestricted checkbox. -
MCP tools are held to the same rule:
phoxtail.mcp.authorization.scoped()
names a tool's codename, and a tool its caller's credential does not cover is
left out of that caller's catalogue. Over HTTP the MCP server introspects the
bearer atGET /api/whoami/before anything runs and answers a request with
no credential401with the standard challenge; a local (stdio) session is
offered only the tools its stored token covers.phoxtail_whoamireports who a
local session acts as and which tools were withheld, with the permission each
would need. -
GET /api/whoami/reports the identity and ceiling of the credential making
the request — email, user uuid, superuser flag, whether the token is
unrestricted, its scopes and bundles, and its expiry. Every credential can
reach it, so a caller can discover its own limits. -
Phoxtail speaks Greek. The dashboard, the authentication pages, the profile,
the admin's snippets and settings, and the page bar and chatbot follow the
active language, with one Greek catalogue; text the bar's and chatbot's scripts
build in the browser is still English. Every password field reads
"Συνθηματικό", the Wagtail admin's included, and Django's password rules read in
better Greek.Translations live in a new app,
phoxtail.translations, whichwire_apps
installs first; a project that lists its apps itself must put it above every
app with translations of its own (allauth, Wagtail, Django's), or
phoxtail_translations.W001names the apps in the way. It also holds
phoxtail's wording for sentences allauth and Django write
(packages/allauth/el.po).make translationsgathers and compiles both.Model fields and models carry translatable labels. Each app gains a
*_translatable_labelsmigration that changes no table; a package with its
own models onUUIDMixinorTimestampMixinsees a pending migration of the
same kind until it generates one. -
{% languages %}(inphoxtail_core_tags) lists the languages a page can be
read in, each with that page's address, and
phoxtail_core/language_menu.htmldraws them as a menu; both are empty on a
site that serves one language. -
One surface model for every page: the page or drawer is the ground
(phx-ground: grey, near-black in dark mode,--phx-ground-background), what
stands on it rises (phx-card, white with a light shadow, with
--clickable,--flush,__title,__textand__icon), an edge-to-edge
bar takes a card's colour withphx-raised, and what floats above
everything is aphx-popover. Each box hands the controls it holds their fill
and shadow (--phx-control-background,--phx-control-shadow), so a button
is white and lifted on the page and a quiet tint inside a card. An admin view
opts in with{% block bodyclass %}phx-ground{% endblock %}. Drawers read
core values for their motion, edge shadow and scrim (--phx-drawer-*,
--phx-scrim-*), and--phx-floating-shadow/-upshade a bar floating
over the page. -
.phx-buttonis the one button:--primary,--secondary,--danger,
--full, and sizes--xsmall,--smalland--largearound the default. A
button markedaria-currentwears the brand colour; a disabled one fades to
0.38. Buttons and small controls are 44px tall from one variable,
--phx-size-small, with 18px icons (--phx-size-small-icon). -
Indicators that show a state and never act:
phx-badge(a count),
phx-tag(a status in a word;--small,--overlay) andphx-notice(a
status in a sentence). Tags and notices take a tone —--success,
--warning,--destructiveor--info— from the colour family of that
name. -
Core draws a list page:
components/lists.css(the page column, the card
grid, the empty state, filters),{% paginator %}(First, Previous, five page
numbers, Next and Last, keeping its width), and
phoxtail_core/page_header.html, the one header every page extends (icon,
title, subtitle, controls, a row below). The API Tokens, Remotes and Streams
sync admin pages use them. -
phoxtail_core/modal_drawer.htmlgives the core modals a ready drawer at
either level: a content view fills its title, body and actions, sets its width
and where it sits (side: right, left or center), and how it closes
(dismiss="button"for a secret shown once).closeModal(element)closes the
modal holding that element, at whichever level, and
phoxtail.core.htmx.replace_modal()answers with a modal in place of the one
the request came from. -
An action announces what it changed under the model's own name, and what shows
that model redraws itself:phoxtail.core.htmx.change_event(),
changed(Model, message)(204, the change,closeModal, a toast) and
{% changed_event 'app_label.Model' %}. -
A template draws a form field with
{% field "input" form.email %}(from
phoxtail_core_tags), or with{% field form.email %}to let the widget pick
its template, and a view with
phoxtail.core.fields.render_field("input", form["email"]); both use Django's
own field rendering. Options are passed by name; a misspelled form field, an
option the field does not take or a template name with a folder fails with an
error, andmanage.py checkreports a bad{% field %}call with its file and
line before its page is drawn.{% widget field class="…" %}draws a control
with phoxtail's attributes on top; anyhx_*option becomes anhx-*
attribute when the control makes a request. -
New fields:
search(a list's search bar, with a clear button and a result
count),date_stepper(‹ date ›for a page showing one day or week),
week_stepper(a week's seven days on one row, with arrows, a calendar and
Today),copy=Trueon an input (a button that copies its value), the
choicesfield'sphx-choices--1col, and thefilled/smalllook of
segmented_controlandsingle_select_searchfor toolbars. -
A page loads everything phoxtail's shared styles and scripts need with
{% include "phoxtail_core/stylesheets.html" %}and
{% include "phoxtail_core/scripts.html" %}; the first also applies the site's
colours and fonts. -
phoxtail.core.clocksasks questions in each place's own clock time:
localize(value, zone)reads a datetime given for a place and refuses one its
clocks skip or show twice (the API answers 422 naming the offset to send);
on_local_clocks(zone_path, zones, condition)asks a condition of each row in
its own zone; andfilter_on_local_clocks(queryset, zone_path, zones, **lookups)filters with bounds read on each row's clocks, which Django's
__dateandTrunc(one zone per query) cannot say. -
phoxtail.core.dates.display_datewrites a date for people in the page's
language ("Mon, Oct 5, 2026"), besideiso_datefor machines. A datetime
counts as its date, so turn it to the zone it should be read in first. -
phoxtail.core.paging.every_item(fetch, **filters)reads a whole paged list
and raisesListChangedif the list moves while it reads; it needs no Django. -
phoxtail.api.schema_names.clashing_schemas(api)lists schema names two apps
use for different shapes, which the OpenAPI document would otherwise silently
merge. -
phoxtail.core.testingis a supported module: Django settings configured the
way a hatched project is, wired throughwire_apps(), with phoxtail's apps
first, for packages that add an app to phoxtail and need a test environment.
A package that appends its own app toINSTALLED_APPSafter importing it
callswire_apps(globals())once more. -
phoxtail net up,net downandnet detachtake a slug to act on one
attached project from anywhere. -
translate,done_all,person_off,first_pageandlast_pagejoin the
bundled Material Symbols.
Changed
- Breaking: every list the API serves answers one page at a time, as
{"items": [...], "total": N}instead of naming the list after the resource
({"users": [...]},{"pages": [...]},{"palettes": [...]},
{"variants": [...]},{"menus": [...]},{"providers": [...]}and so on).
Passlimit(default 50, at most 500) andoffsetto read on;totalcounts
every match, and the matching MCP tools take the same two arguments. Lists
that used to arrive whole (collections, locales, sites, site-setting
assignments, block categories) now arrive 50 at a time, and a search returns
matches in the list's own order rather than ranked by relevance. Timestamps
use one API-wide format (2026-08-12T06:10:00.446Z).PUT /blocks/{id}/categories/answers with the resulting list itself.phoxtail studio dumpandload, the admin sync screens andphoxtail content list
read every page, and stop with a message if a list changes while it is read or
a remote runs a phoxtail from before lists were paged. Apps build their API onphoxtail.api.pagination.Router,
which pages every list it serves. - Breaking: API paths moved to the app that owns the data. Everything under
/api/content/v1/is now/api/cms/v1/, beside/api/cms/v1/site-settings/;
images, documents, video and audio are at/api/media/v1/; internal links are
at/api/core/v1/internal-links/. Update any client calling the old paths and
runcollectstaticso the shipped JS follows. The MCP tools and the
phoxtail contentCLI keep their names. - Breaking: API endpoints receive an
AuthorizationContextas
request.authinstead of aUser:request.auth.useris the person,
request.auth.tokenthe credential (Nonefor a browser session).
Predicates passed toAuthorizereceive the context, and
phoxtail.tokens.auth.authenticate()returns theAccessTokenrather than
its user. - Breaking: access tokens say whether they are unrestricted. A token carries
scopes or is markedunrestricted, and issuing one requires choosing. The
"*"wildcard is gone, because installing an app silently widened every token
already in circulation. Tokens issued with"*"are not converted: they are
refused wherever a scope is checked, until re-issued. - Breaking: an app declares its HTTP API and MCP tools one way only, by
convention: subclassPhoxtailAppConfigand shipapi/declaringversions
(andpage_schemas) andmcp/. Theapi_version_routerattribute, the
phoxtail.mcp_modulesentry-point group,[mcp] extra_modulesin
phoxtail.tomlandPhoxtailAppConfig.page_schema_contributorsare gone.
Every phoxtail app now declares its own this way; theoperationIdvalues in
the OpenAPI schema change with the module paths. The agent app's
providers,artifactsandsettingsendpoints no longer disappear when
thechatbotextra is missing. A new project's own app subclasses
PhoxtailAppConfig, so it can grow a surface the same way. - The users and genders endpoints ask for the Django permission each act needs
(phoxtail_users.view_userand its siblings) instead of a superuser account,
so a person granted exactly that permission, through a group or directly, can
be given exactly that much. Superusers are unaffected. Every endpoint's refusal
names the permission missing, and the MCP tools name the same codenames as
their endpoints, so a token narrowed to less than an act is not offered the
tool that performs it. - The MCP server is built on
fastmcpand speaks the stateless MCP protocol
(2026-07-28), so it can be restarted or replicated without dropping clients.
mcp>=1.29,<2is replaced byfastmcp-slim[server]. The variant-editing
session tools, which write files to the server's own disk, are offered only
over a local connection. - API requests made with an access token are answered in English unless they
sendAccept-Language; a logged-in browser still follows the language the
person chose. - The authentication pages — login, signup, password reset, login codes, email
verification, reauthentication, sign out, email addresses, change and set
password — are django-allauth's own pages drawn with phoxtail's look, so they
follow allauth's behaviour and wording as it is upgraded. The private-page
login, the page password prompt and the consent page share the same frame. The
card sits in the middle of the page with the site's logo top left and, on a
multilingual site, a language menu top right; the pages use the site's
favicon (light and dark) and fonts; the orbs and grid behind the card are gone.
Signup asks for the person first, then how to reach them, then the password,
and the extra buttons phoxtail used to add are gone, as allauth draws none. A
site that overrides one of allauth'saccount/templates keeps its copy. - phoxtail needs django-allauth 65.20.1 or later, for allauth's fixes since
65.18: rate limits that hold under concurrent requests, safer redirects when
ALLOWED_HOSTSallows every host, and a cancelled login by code that no longer
carries over. Relock withuv lock --upgrade-package django-allauth. - The engine extra declares python-dateutil (2.8.2 or later, below 4), which
phoxtail.usersuses to count ages; installed versions do not change. - Field templates moved from
phoxtail_core/forms/widgets/to
phoxtail_core/forms/fields/, and thehtmx/copies are gone: the fields take
hx_get,hx_target,hx_swap,hx_trigger,hx_includeandhx_vals
themselves. A template that includes the old paths switches to
{% field "select" form.status … %}, a view torender_field(…). The field
stylesheet moved tophoxtail_core/css/forms/fields.css, and the field
classes are renamed fromfw-tophx-(phx-field,phx-field__box,
phx-toggle__track,phx-choice,--phx-field-gap, …). - Every field lets Django draw its control:
input,select,textarea,
checkbox,toggle,phoneandsingle_select_searchcarry what the form
declares (type,required,maxlength,autocomplete, …), screen readers hear
each field's help text and errors, and a field template refuses a widget it
cannot draw instead of drawing a broken field. A decimal in a language that
writes it with a comma (Greek, German) now appears in its box,maxlength
stops typing at the limit, and a password is never written back into the page.
Thephonefield keeps its country and number on one row, its countries in
the page's language and in dictionary order.SingleSelectSearchField
declaresHiddenInput, and its script lives once injs/fields.js, as do the
password toggle and the picker buttons: a page drawing such a field includes
phoxtail_core/scripts.html. A disabled field is drawn filled at full
contrast instead of faded. Choice rows are 48px tall, and a toggle's row
highlights on hover like a checkbox's. A search matches the start of a word,
as the Streams Sync remote picker now does too. - Country names come from the same source as the phone prefixes, in the page's
language, everywhere a country is shown. Some English names change ("United
States", "Trinidad & Tobago"); stored codes do not. - The dashboard's navigation is one system with its pages. On a computer the
sidebar is a floating card and the top bar is gone: the site's own pages sit
behind a Website row and the language behind a row of its own. On a phone the
menu opens as a drawer, the top bar floats in from the screen's edges with
rounded corners, the dock marks the page you are on in the brand colour, both
take a card's colour, and their ☰ and language buttons look like the dock's
items. The top bar's site menu template
(phoxtail_dashboard/navigation/menu.html) and itsdash-menuclasses are
removed. The dashboard home's cards are core's clickable cards, and the profile
page is a bento of four cards with an Edit drawer. A custom widget template that
copied the old card markup moves to core's card classes (dash-widget__*and
the profile'susr-*classes are gone), and a project overriding the profile's
user_info.htmlorindex.htmlmoves its buttons intouser_info.html. The
content pane draws a slim, theme-aware scrollbar. DashboardModule.remove_nav_item()takes the item'surl_nameinstead of its
translated label.- Drawers are solid instead of glass, parted from the page by their shadow. The
API Tokens, Remotes and Streams Sync admin pages use core's list page, cards,
search, paginator and drawers; a token is shown once in a dialog only its own
buttons close. Their own stylesheets are gone, and empty lists show their title
and text without an icon. - Messages (toasts) are popovers with the modals' round close button, and their
classes are renamed fromauth-toast*tophx-toast*. A toast no longer
closes the open modal; send thecloseModalclient event beside it to close
the modal an action came from. Each core modal is named for assistive
technology by its first heading. The modal close button and other round
buttons are 44px. - Every bundled icon is a Material Symbol; the block editor's field blocks and
the dashboard menu editor use them too. - A freshly hatched project's homepage stands on phoxtail's surfaces, with the
wordmark at the size the authentication pages give a logo, and its phoenix
respects reduced motion. - Generated production compose files pin
nginx:1.31and
certbot/certbot:v5.8.0instead of taking the newest, so a deploy cannot move a
site to a new release line unasked; fixes within nginx 1.31 still arrive. A
deployed project regenerates its compose file with
phoxtail docker create compose production --registry --output .phoxtail/deploy/docker-compose.prod.yaml --force
and deploys again. phoxtail server deployshows the difference and asks before replacing a
server'sdocker-compose.yamlthat differs from the local one..envand
nginx.confare still uploaded only when the server has none.phoxtail docker releaseprints the commit each upgraded package moves to
when it refreshes the lockfile, so its output says which commits go into the
image.phoxtail net peersis a group:net peers listandnet peers get <slug>.
Scripts callingnet peers --jsonsaynet peers list --json.- The local swarm's router moves from Traefik 3.1 to 3.7.
Removed
- Fields folded into others:
date,datetime,time,numberand
passwordare drawn byinput;checkbox_multipleandradio_selectby
choices;choice_dropdownbyselect;null_boolean_selectbyselect
withphoxtail.core.fields.BooleanFilterSelect;segmented_control_choices
bysegmented_control. Replace{% field "<old>" … %}with the new name. The
label_slice_index,label_off,label_onandlockedoptions are gone: the
form field's choices (for weekdays, Django'sWEEKDAYS_ABBR) anddisabled
say it. Asegmented_controlwith no value ticks no segment, so give it an
initial value. - The
multi_select_chipsfield,MultiSelectChipsFieldand
MultiSelectChipsSearchView, which nothing drew. single_select_searchno longer takesis_active(usedisabled=True),
selected_item,available_itemsorwidget_id, and
SingleSelectSearchViewno longer takeswidget_id: both read them from the
form field.- The
add_classtemplate filter (use{% widget field class="…" %}) and the
dashboard'sdashboard_languagestag (use{% languages %}). - The
allauth/components/logo.htmlandsubmit_button.htmltemplates: the
frame draws the logo, and{% element button %}draws aphx-button. - The old admin paginator template and the
.phoxtail-btnand
.phoxtail-danger-sectionrules inmodal.css.
Fixed
-
Certificate renewal no longer depends on the registry. Renewal started certbot
with nginx and web, and web'spull_policy: alwaysmade it pull the site
image first, so once the server's registry token expired, renewal failed
silently and certificates lapsed. Certbot now runs with--no-deps. The cron
line is installed only once, so on each existing server replace the
# phoxtail-ssl-<project>line incrontab -ewith:0 3 1,15 * * cd $HOME/<project> && docker compose run --rm -T --no-deps certbot renew -q && docker compose exec -T nginx nginx -s reload # phoxtail-ssl-<project> -
File URLs the API hands out — images, documents, videos, audio, thumbnails and
font files — are built onWAGTAILADMIN_BASE_URLinstead of the hostname the
caller used, so the MCP server no longer relays URLs pointing at its own
container.phoxtail net attachwritesDOMAIN=<slug>.localhost; a project
already attached needs the line added to.envor a freshattach. -
Confirming your password on the "Confirm Access" page no longer always fails
for a user with a username, and a deactivated user sees "Account Inactive"
instead of a wrong-password error.phoxtail.usersadds allauth's
authentication backend; a project that sets its ownAUTHENTICATION_BACKENDS
should addallauth.account.auth_backends.AuthenticationBackend. -
The phoxtail bar appears for anyone who can use something in it, not only
superusers; grantingphoxtail_agent.access_chatbotnow shows the chat button. -
The chatbot acts as the person chatting: each turn mints a short-lived token
owned by that person, revoked when the turn ends, so every tool call is
attributed to them and the tool list is theirs. Its endpoints answer 503
naming thechatbotextra when it is not installed. -
The phone number on the profile edit form can be saved again, and the profile
drawer stays open when a save fails. -
Fetching an SVG image no longer answers 500.
-
Deleting a collection asks Wagtail what is inside it, so the API refuses
exactly what the admin refuses, including other apps' contents and
grandchildren. -
Asking for a block's context with reference variants no longer fails, and a
blank required field on a variant collection, block or block category is
refused with 422 naming it;phoxtail studio loadreports such a file as a
warning and carries on. -
The API documentation shows each schema once: page-body blocks are
BodyBlock*and streams collectionsVariantCollection*, and the two ways in
(Bearer token, session cookie) are listed once each. -
phoxtail.usersbringssorl.thumbnailintoINSTALLED_APPSwhen settings
are wired. -
The CLI starts several times faster: heavy imports load only in the commands
that use them. -
Pages say which language they are in (
<html lang>), and the dashboard's
labels, its "Log in" button and "Last Name" are translated in every language
Wagtail translates them into. -
On an iPhone or iPad, a date or time field's calendar or clock button opens
the picker. -
Typing in a
single_select_searchfield no longer fails with a
FilterFieldErrorwhen the form narrows the field's choices. A model searched
this way declaresindex.FilterField("id")in itssearch_fields, and
manage.py checknames any that does not. -
The seven-column choice grid switches on its own width, so its labels are not
cut in a narrow drawer. -
The password toggle and a message's close button are announced properly to
screen readers.
Security
-
An API endpoint that declares no
auth=, under a router that declares none,
now refuses every caller: 401 for anonymous, 403 for everyone else. Before, it
admitted any logged-in user and any unrestricted token, so an app whose
endpoints were not annotated let any logged-in user read and change its data.
Such endpoints stop answering until they declareauth=guarded(...),
scoped(...)orauthenticated()./api/ping/admits any valid credential. -
Endpoints that asked for nothing beyond being logged in now ask for the
permission their act needs, so any account could no longer:- read every page as its latest draft, including unpublished edits
(/api/cms/v1/pages/), or photograph one through the screenshot views;
reads narrow to the pages the caller holds a Wagtail grant on, and others
answer 404; - list, download, move or delete every image, document, video and audio file
(/api/media/v1/); permissions are asked per collection, as Wagtail's admin
asks; - rewrite every block and variant, or push and pull them across projects
(/api/streams/v1/); - rewrite the palettes and fonts every page renders with (
/api/design/v1/),
or a site's branding, font and palette settings, now asked per site; - add an inference provider or repoint a site's default model
(/api/agent/v1/); - read or change a site's menu, or rename or delete an internal link;
- list every site and locale (
view_site,view_locale); - read a block's field schema and every reference variant's code through
POST /api/streams/v1/context/(nowview_blockandview_blockvariant).
The studio tools that reach past their app name what they reach
(phoxtail_studio_render_blockandphoxtail_studio_screenshot_pagename
access_chatbot), so they are not offered to tokens narrowed to something
else.Collection endpoints now admit the people Wagtail permits per branch, which
has_permnever saw. The menu, media and collection pickers require an
operator instead of chat access. - read every page as its latest draft, including unpublished edits
-
A font's name or fallback list, or a font or palette role's identifier, could
close the<style>element every page carries and run a script for every
visitor, the authentication pages included. Each value is escaped as the CSS
is built, and the admin, the API and the MCP tools refuse values that are not
font names or identifiers. -
Generated Dockerfiles install packages with uv 0.13.5, which no longer trusts
the distrusted emSign certificate authority
(GHSA-hq85-5cpj-mr62).
An existing project regenerates its Dockerfile with
phoxtail docker create dockerfile --force, which overwrites local edits, so
diff it against a copy first.