Skip to content

Releases: phoxmor/phoxtail

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 13:24
v0.5.0
fa4b2d7

This release stops a missing icon from breaking a page: a block variant that
names an icon removed in an earlier release now renders without it and logs
a warning, instead of answering with a server error.

Upgrading from 0.4.0 — read this first:

  • dashboard_icon_path is gone (see Removed). Templates of your own that
    use it, or pass icon to phoxtail_core/copy_key_button.html, must switch
    to {% icon %}.

Removed

  • The dashboard_icon_path template tag is removed, and so is the icon
    parameter of phoxtail_core/copy_key_button.html. Every icon is now drawn
    with {% icon "<name>" %}, so a missing name renders nothing and logs a
    warning everywhere instead of failing the page in the dashboard navigation.
    Templates that used {% dashboard_icon_path name as path %}{% include path %}
    should use {% icon name %}.

Fixed

  • A page whose block variant names an icon that no longer exists now renders
    without that icon instead of failing with a server error. The {% icon %}
    tag logs a warning naming the missing icon (Icon '<name>' not found), so
    stored variants that still use a name removed in an earlier release can be
    found and fixed.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Oct 21:59
v0.4.0
1e5e3a4

This release fixes a leak: images built by phoxtail 0.2.0 and 0.3.0 (and by
installs from the git repository since 2026-06-10) carry the project's
production .env once the project has been deployed with
phoxtail server deploy
(GHSA-52f8-g7r4-m363).
It also names database backups after the site and environment they hold, and
lets phoxtail db backup back up a server.

Upgrading from 0.3.0 — read these first:

  • Every deployed project must act (see Security): regenerate
    .dockerignore, release, change the credentials in .env, and delete the
    old images. Until .dockerignore is regenerated, phoxtail docker push and
    phoxtail docker release refuse the project's image.
  • phoxtail db backup, db pull, db push and db restore stop without
    DJANGO_ENV
    in the .env next to the database they touch, and their files
    have new names.

Security

  • Production images no longer carry the project's secrets
    (GHSA-52f8-g7r4-m363).
    The generated Dockerfile copies the project folder into the image, and the
    generated .dockerignore excluded .env only at the project root, while
    phoxtail server deploy keeps the production .env (with SECRET_KEY and
    the database password) in .phoxtail/deploy/. Anyone able to pull an image
    built after a project's first server deploy could read those files, and
    other files the project folder held but git does not, such as uploads under
    .phoxtail/ and hidden files at the root. The .dockerignore template now
    excludes .env files at any depth,
    .phoxtail/, every hidden file or folder at the project root and database
    dumps (*.sql), as well as Python caches, node_modules and editor files at
    any depth, so bytecode compiled on the developer's machine stays out too.
    phoxtail docker push and phoxtail docker release now refuse to push an
    image with a .env, .env.*, .phoxtail or .sql file under /app.

    Existing projects:

    1. Upgrade to phoxtail 0.4.0 first: the refusal runs in the CLI that
      pushes.
    2. Run phoxtail docker create dockerignore --force, then re-apply any
      edits of your own.
    3. Run phoxtail docker release, then phoxtail server deploy <ip>.
    4. Change the credentials the .env holds, in both copies (on the server
      and in .phoxtail/deploy/), then run phoxtail server deploy <ip>
      again:
      • the email password first, since it works from anywhere: create a new
        one with your provider, deploy it, check that mail is sent, then
        revoke the old one;
      • SECRET_KEY. Everyone is logged out, and links already emailed stop
        working. Do not keep the old key in SECRET_KEY_FALLBACKS;
      • any key you added yourself;
      • the database password only if its port was ever reachable from
        outside the server (the generated compose file publishes none).
        Change it in the database first, then in .env, or the site cannot
        connect.
    5. Delete the old image versions from your registry, keeping the one the
      server now runs.

Added

  • phoxtail db backup <ip> backs up a server's database: it dumps it into the
    server's db-backups/, keeps it there, and copies it into the local
    db-backups/. Without an IP it backs up the local database as before.

Changed

  • phoxtail db backup now names its files <project-slug>_<DJANGO_ENV>_<UTC time>.sql, for example mysite_production_2026-10-28_10-19-28.sql, so a
    backup says which site and which environment it holds even after it is
    copied elsewhere. The environment is read from the .env next to the
    database that is dumped, and the command stops with an error if
    DJANGO_ENV is missing. A failed dump leaves no file behind. Existing
    backups keep their names and db restore still accepts them.
  • The copy that db pull, db restore and db push take of a database
    before replacing it is now named <project-slug>_<DJANGO_ENV>_before-<step>.sql
    (for example mysite_production_before-push.sql) instead of
    pre_<step>_safety.sql, so it says which site and environment it holds.
    Each step still keeps one copy, overwritten by its next run, and the copy
    is written whole or not at all. The commands stop before changing anything
    if DJANGO_ENV is missing from the .env next to the database they
    replace. db restore refuses its own before-restore copy, since taking
    the copy first would overwrite it. Old pre_*_safety.sql files are no
    longer written and can be deleted.
  • A hatched project now commits its uv.lock: the template's .gitignore no
    longer ignores it, so the exact version of every installed package is in the
    project's history and each upgrade shows as a diff. An existing project picks
    this up by removing the uv.lock line from its own .gitignore.

Fixed

  • The MCP server and API token checks no longer fail in production images that
    carry no .phoxtail folder, such as those built with the new .dockerignore.
    Importing the MCP server tried to create .phoxtail/mcp/uploads under /app,
    which the app user cannot write there, and every API token check imports it.
    The screenshot tools now still return the image when they cannot also save a
    copy under .phoxtail/vision/.
  • phoxtail server ssl now replaces an outdated certificate-renewal cron line
    instead of keeping it: it shows the server's line next to the current one and
    asks before replacing it, leaving every other cron entry as it is. On a
    server set up before 0.3.0, running phoxtail server ssl <ip> is now how the
    --no-deps renewal line arrives; no crontab -e edit is needed.
  • phoxtail db pull, db push and the other server commands now read a piped
    answer: the ssh call that checks the server's identity used to swallow it,
    so echo y | phoxtail db push <ip> aborted at the prompt.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 20:09
v0.3.0
7459221

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 with guarded(...), 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 run collectstatic.
  • Tokens issued with "*" stop working wherever a scope is checked; re-issue
    them as unrestricted or with the scopes they need.
  • request.auth is an AuthorizationContext, not a User.
  • An app declares its API and MCP tools one way only, by convention; the
    api_version_router attribute, the phoxtail.mcp_modules entry point,
    [mcp] extra_modules and page_schema_contributors are gone.
  • Field templates moved to phoxtail_core/forms/fields/, several fields were
    folded into input, choices and select, and the fw- 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
    engine extra), declared as a surface of phoxtail.tokens: the RFC 8414
    discovery document at /.well-known/oauth-authorization-server and the
    endpoints under /o/. A hatched project picks it up on upgrade with nothing
    edited — the dependency, the URL mount and the OAUTH2_PROVIDER defaults
    arrive through the app protocol, and migrate runs on every start as it
    already does. Its issuer is the exact name the MCP server advertises.

    • Outside clients ask for bundles, never permission codenames: one
      <app>:read / <app>:write pair 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 placeholder read, a
      codename, or nothing at all — is refused as invalid_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 — its client_id is an https URL 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
      none among the token-endpoint authentication methods (a phone app or a CLI
      proves itself with PKCE), a client listening on http://localhost:<any port> matches one that registered another loopback port (RFC 8252), and
      every authorization response carries iss (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 is http://localhost: an http redirect 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 sweeptokens removes dead token rows of both families
      after 90 days (--retention-days); nothing schedules it yet.
    • System checks E004–E007 fail 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.
  • 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. One Authorization: Bearer header is routed by the key's
    shape (phoxtail's own keys carry the phxt_ prefix), and either kind is read
    into one credential. Bundles are valid on phoxtail's own keys too —
    cms:read beside 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 and http://mcp.<slug>.localhost/mcp locally. Its 401 points
    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. The mcp service joins the production compose;
    phoxtail nginx create production writes a server block that passes requests
    through exactly as sent; phoxtail ssl obtain and phoxtail server ssl add 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 production lists web in ALLOWED_HOSTS. An
    already-deployed project needs web added to ALLOWED_HOSTS by hand, the DNS
    record for mcp.<domain>, a regenerated nginx.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, and authenticated() 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 at GET /api/whoami/ before anything runs and answers a request with
    no credential 401 with the standard challenge; a local (stdio) session is
    offered only the tools its stored token covers. phoxtail_whoami reports 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, which wire_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.W001 names the apps in the way. It also holds
    phoxtail's wording for sentences allauth and Django write
    (packages/allauth/el.po). make translations gathers and compiles both.

    Model fields and models carry translatable labels. Each app gains a
    *_translatable_labels migration that changes no table; a package with its
    own models on UUIDMixin or TimestampMixin sees a pending migration of the
    same kind until it generates one.

  • {% languages %} (in phoxtail_core_tags) lists the languages a page can be
    read in, each with that page's address, and
    phoxtail_core/language_menu.html draws 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...

Read more

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 00:07
v0.2.0
f3e29e1

Upgrade notes below — a dependency that moved, renamed CSS classes, an app that
must change position in INSTALLED_APPS — address projects built against an
earlier version of the package. A fresh install needs none of them.

Added

  • Shared blocks can now be pinned to a site-wide slot (Block.site_slot: head/body
    anchors in the page template) and render automatically on every page of a site that
    fills in their shared content — no per-page placement needed; combined with
    page-type restrictions it scopes to every page of those types. A page that places
    the block itself takes over (its own position and variant, or a "hide on this page"
    toggle), variants cascade page → site (SharedBlock.variant) → block default,
    missing locales fall back to the default locale's content, and
    Block.render_in_preview keeps tracking scripts out of previews and screenshots.
    The streams v1 API, the MCP studio tools, and the cross-project push envelope all
    carry the new fields.
  • Platform menus: an editor writes a site's navigation in the Wagtail admin, scoped
    to a site and a language, under a new Platform section. Entries point at a page, a
    named route, an address elsewhere, or a group of those. The menu renders along the
    top of the dashboard and inside the small-screen drawer, with dropdowns,
    separators, and the entry for the page being read marked as current; unpublished
    pages are shown only to editors, linked to their draft. A site with no menu written
    for it shows no bar and no drawer section. Menus are also readable and writable
    over /api/dashboard/v1/menus/ and through the phoxtail_dashboard_* MCP tools,
    so an agent can list, create, update and delete them — updates take an If-Match
    ETag, and an entry type the menu does not define is refused rather than silently
    dropped.
  • Dashboard widgets are declarative: give add_widget() a title, description, icon
    and url_name and phoxtail renders the card, so an app no longer ships a template
    and a context function for a link. Cards are grouped under a heading per app
    (DashboardModule(verbose_name=...)), and the Account group opens with a card for
    the Wagtail admin, shown only to users who may reach it. A site can amend what
    other apps contribute — registry.get_module(), unregister(),
    DashboardModule.remove_widget() and remove_nav_item() — override the index view
    itself through the new PHOXTAIL_DASHBOARD_VIEW setting, and shadow any phoxtail
    template from a project-level templates/ directory, which hatched projects now
    get.
  • The dashboard is served under a language prefix (/en/dashboard/) and carries a
    language switcher offering every language the project serves. A language with no
    menu written for it still opens the platform — the menu bar is simply absent, and
    the dashboard's own screens are translated regardless.
  • UrlMount accepts i18n=True, mounting an app's URLs under a language prefix
    (/en/shop/). Set it for anything a person reads; machine endpoints stay
    unprefixed so their callers need not learn a locale. When at least one app opts in,
    Django's set_language view is published at i18n/ alongside it.
  • phoxtail hatch and phoxtail install take a --source option that says where a
    package is resolved from, using uv's own locator syntax: git+ssh://…@ref or
    git+https://…@tag for a repository, a directory for a local checkout (added as an
    editable path), or a .whl/.tar.gz URL for a built artefact. PyPI stays the
    default and nothing changes without the flag. Both commands check a non-default
    source before writing anything, and neither treats an unanswered check as a
    failure: with no network the check is skipped and uv, which can resolve from its
    own cache, has the final say.
  • phoxtail upgrade phoxtail now compares the project's phoxtail-owned declarations
    in pyproject.toml against the current project template and offers to apply the
    difference before locking. Previously it only advanced the lockfile, so a project
    kept declaring whatever extras it was hatched with even after the template moved a
    dependency to a different one. Projects hatched before the dependency-groups split
    get the [dependency-groups] table created for them.
  • TRADEMARK.md sets out the permitted use of the Phoxtail name, wordmark and logo,
    which the BSD licence does not grant. Saying a product is built with Phoxtail needs
    no permission; naming a product after it does.
  • SECURITY.md — how to report a vulnerability privately.

Changed

  • Wagtail 8.0 is now the supported version, raising the engine extra from
    wagtail>=7.4.2,<8.0 to wagtail>=8.0,<9.0. Django stays at <6.0 and the Python
    floor stays at 3.11: Wagtail 8 runs on Django 5.2, so neither has to move yet.
    wagtailmedia is raised to >=0.18.1, which carries a permission fix for the
    media chooser — it previously exposed a media item's id, title and edit URL to
    users without access to it.

    Two Wagtail 8 changes are worth knowing about. Image renditions no longer convert
    AVIF and WebP to PNG, so sites serving those formats will now emit them directly;
    set WAGTAILIMAGES_FORMAT_CONVERSIONS to restore the old behaviour. And
    menu_order is now respected between viewsets inside a ViewSetGroup, which
    reorders some admin menus.

  • Breaking for projects built against an earlier version. playwright and
    Pillow are no longer installed by default. They moved to a new studio extra,
    which carries the screenshot tools in phoxtail.mcp.studio and nothing else — a
    base install no longer pulls a browser stack it may never use.

    Newly hatched projects get phoxtail[studio] in their dev dependency group, and
    projects still on the phoxtail[dev] alias keep it because that alias now includes
    studio. Projects that already moved to the dev-tools + dependency-group layout
    must add it themselves — phoxtail upgrade advances the lockfile without rewriting
    pyproject.toml:

    [dependency-groups]
    dev = [
        "phoxtail[dev-tools]",
        "phoxtail[studio]",
    ]

    Then run uv lock; the Dockerfile syncs with --frozen and refuses a lockfile
    that no longer matches. Without the entry the development image fails at
    playwright install. Production images are unaffected, and lose a browser stack
    they were never using.

  • htmx is no longer bundled with phoxtail. It now comes from django-htmx, which
    phoxtail already depends on, via its {% htmx_script %} tag — one copy instead of
    two, at 2.0.10 instead of 2.0.8. Any project template that loads htmx with
    <script src="{% static 'phoxtail_core/js/htmx.min.js' %}"> must switch to
    {% htmx_script %}; the static file is gone, and under
    ManifestStaticFilesStorage a stale reference raises at render time.

  • Every declared dependency floor now names a version phoxtail is tested against,
    replacing inherited minimums that had never been installed — several of which did
    not work. Installing phoxtail alongside an older pinned dependency will now fail to
    resolve rather than fail at import.

  • phoxtail install no longer decides on its own whether a package belongs in
    INSTALLED_APPS. It previously treated any importable top-level module as a Django
    app, so installing something like httpx or redis registered it and left Django
    importing a non-app at startup. A package now has to carry an AppConfig, models,
    or migrations to be considered one — and because no inspection can rule out a
    Django app carrying none of those, the result is offered as the default answer
    rather than acted on. Answer no and nothing is registered; answer yes for a package
    that was not detected and you are asked for the module name. --app <module> and
    --no-app answer ahead of time, and a non-interactive run acts on the detection
    result without prompting. The module name is read from the installed
    distribution's metadata rather than guessed from the package name, so apps whose
    names differ — django-taggit installs taggit — are registered instead of
    silently skipped. The stream-population step follows the same answer: declining
    registration skips it instead of running it against an app Django does not know
    about.

  • The profile view is borderless: every control — the content panel, the
    account-settings links, the personal-information rows, the Edit button, the avatar
    and the file field — is now a shape defined by its fill against the fill behind it,
    in both colour schemes. The information rows are tiles rather than lines, the two
    columns share one fill, the page heading has lost its panel and the icon beside its
    subtitle, buttons are pills on the core button's metrics, and the row icons are the
    dashboard's widget icon. The stylesheet that dressed it moved with it — see
    Removed.

  • The annotations shipped under py.typed are now verified rather than asserted —
    the package type-checks clean, and CI fails on any error. Two signatures were
    corrected against their callers in the process: collection_id on the studio
    client's create_variant accepts None, and Provider subclasses now declare
    their one-argument constructor on the base class.

  • The agent app's migration 0003 was folded into 0001: it set a model's
    verbose_name and nothing else, so it emitted no SQL. No action required — a
    database that already applied it is unchanged, and Django ignores the leftover
    history row.

  • LICENSE names the copyright holder as a person rather than a trade name.

Removed

  • The dashboard stylesheet phoxtail_dashboard/css/ui.css is gone. Only the profile
    view rendered its components, yet every dashboard page linked it. Its rules now
    live in users/css/profile.css under a usr- prefix, and its two genuinely shared
    rules, .dash-page and .dash-page-inner, moved into
    phoxtail_dashboard/css/dashboard.css. A site using any of th...
Read more