Muppy 18.97.0¶
Preview
This page describes Muppy 18.97.0, which is not released yet. What it describes is
being delivered to the customers who asked for it as the release candidate 18.97.0-rc.1, and
may still change before the release.
This release brings a long-running development branch home in one step: everything built since 18.89.0, including the seven prereleases between the two, which were never published on their own. Four subjects carry it:
- Git access for App Servers — a box no longer holds a git token. Git asks Muppy, a human approves the operation, and the credential is released for that operation alone.
- Muppy Vault — the secrets Muppy stores are encrypted at rest, under a master key that lives in your password manager and nowhere else. A stolen dump, backup or disk image becomes worthless.
- Public URLs, custom domains and DNS — your applications are served through Cloudflare's proxy, under your tenants' names and now under your customers' own names, with the visitor's real address visible to your IP filters.
- What an AI agent may do — every infrastructure method is now gated by the environment it acts on, and every remote command an agent runs leaves a record.
Everything else — App Definition duplication, server plans, host readiness, Kubernetes upgrades, high availability, the version display and release candidates — follows below.
What this release asks of you
Nothing breaks on upgrade, but eight things need a decision or a gesture from you:
- Repository-limited git tokens must name their repositories, or they are never served — details.
- Cloudflare zones: run Check Connection once, or custom hostnames and the Declare button stay hidden — details.
- IP whitelists: an application that filtered visitors by IP needs its range re-entered and the reverse proxy reconfigured — details.
APP_PRIMARY_URLchanges meaning on App Servers that have a public URL — details.- 0.5 vCPU plans need Re-apply Plan Limits to become a real limit — details.
- Kubernetes: the Dashboard chart repository moved, Stop before upgrade on a package profile is retired, and a profile that receives release candidates needs its Upgrade Job Trigger on Any SEMVER change — details.
- The first generation of LXC packs is retired, with its data: export what you still read on those screens before upgrading — details.
- Muppy Vault is opt-in and encrypts nothing until you run the activation ceremony; keyring administration is now granted explicitly, never inherited — details.
Git access for App Servers¶
A git token written on a server is readable by anyone with a shell there — a colleague, a script, an agent working on the box — for as long as the box lives. This release removes the token from the box entirely.
The short version is in Git in Muppy in two minutes; the model and what each provider lets Muppy verify are in Git workflows in Muppy.
A git command asks for its own authorization¶
Git calls a credential helper, the helper asks Muppy, and Muppy answers for that one operation. When no window covers it, the git command blocks and waits while you approve it — it is the command itself that holds the request, and it is that command that spends the approval. Nothing is ever left armed on a box waiting for a taker, and two operations started at the same moment join one request instead of asking you twice.
What you gain. Nothing to steal on the box, one place to revoke a credential, and an approval prompt that names what is being approved: push to acme/app, not "a git session".
What you have to do. Nothing on a box already using the credential helper. A box on an embedded token keeps working exactly as before.
Logging in on a box¶
muppy-git-login <your-muppy-login> opens a session: it says who is working on this
box and sets git's author identity. It grants nothing — every operation still asks.
The command prints the approval URL and, on a line of its own, the Login Code in a form you can copy with a double-click. You type that code into Muppy to approve the login, and the field accepts it whatever spacing, case or line break came with the copy.
Why the code is asked for. Anyone can run muppy-git-login with your address on
their box, and it is Muppy — a system you trust — that then notifies you. The code never
travelled on their channel: it printed on their terminal. Asked for one you do not have,
the absence is the signal. The notification also says where the request came from, and the
login screen offers Not me to cut it off.
Three more things around sessions:
- One open session per box. A second person is told who holds it and how to claim it; claiming evicts the incumbent, who is notified. Logging in again as yourself is not a takeover and needs no flag.
- A session ends on its own. A nightly job closes a session once a chosen hour has
passed in the holder's own timezone — 4 a.m. by default,
-1disables it — so a box left open on a Friday does not stay held until somebody evicts its holder. muppy-git-statusasks Muppy, instead of reading a local file that cannot know it has gone stale. It also probes every path the box uses and reports any one your zero-trust edge is blocking, which used to surface as agit fetchfailing for no visible reason.
Approving an operation¶
The approval screen offers four answers, with the tightest one as the default action:
- Allow once — this operation and nothing else.
- Allow for N minutes, optionally sliding (each write pushes the window back). Reads never extend a sliding window, so a background fetch loop cannot hold one open.
- Reject — a change of mind.
- Not me — close the session — the answer to a prompt you cannot account for. No confirmation dialog: friction on a panic control is a design error.
The window length follows the capability of the token that would be served, not the operation asking: 15 minutes for a write-capable token, 8 hours for a read-only one. Both are settings.
The git command line the box reported is shown to you, labelled as reported by the box rather than established by Muppy — the box declares it, so it is evidence you read, not evidence Muppy vouches for.
Reads that ask nothing¶
A read is served with no session and no approval when the token that would be used is both read-only and limited to the repository being read. Such a token cannot write and cannot reach another repository, so serving it freely costs nothing.
Everything else — a push, or a read whose only token is account-wide or read-write — goes through an approval. This is what makes the one-token setup workable: a developer with a single read-write PAT can work immediately, approving as they go, and earns silent pulls later by adding a repository-limited read-only token.
If a qualifying token exists but has expired or been revoked, the owner is told, because that is a configuration to fix rather than a missing one.
A repository-limited token names its repositories¶
Fill in Repositories on your scoped tokens
A token declared as limited to a repository now carries the list of repositories it reaches, one per line. A scoped token that names none is never served. Existing tokens are not migrated: open each one and either run Introspect token (GitLab and Forgejo fill the list themselves) or type the paths by hand (GitHub and Azure DevOps cannot report them). The form warns while the list is empty.
Why it became necessary: two tokens for the same forge, one read-only and limited to one repository, the other read-write and account-wide, both flagged Default. Provisioning a server from a third repository was served the read-only one and the forge refused it. Every question Muppy asks now carries the repository, so a scoped token is offered only where it applies.
The Default flag now means "the account-wide token of this forge" and nothing else. It
is hidden on a scoped token, which no longer conflicts with anything. Token scope and
access level show as coloured badges in the lists, and a token's label says how far it
reaches — my-token (GitLab - Read Write - Wide?). The question mark marks an assumption
rather than a declaration: an unstated reach is treated as account-wide.
Your tokens are measured, not declared¶
Introspect token asks the provider what a credential really is, and writes back what it learns: the real permissions, the expiry, the username, the reach, and a Token status — active, expired or revoked — with the age of that measurement beside it. A verdict without its age is a half-truth.
- A token that cannot describe itself — fine-grained, or simply dead — is read through your provider connection (below). That is the case the feature exists for.
- A token that has vanished from your own listing is recorded as revoked, keeping the permissions the last check measured: what it could do is the answer to "what was revoked here".
- When a credential's name matches two of your tokens, or none of them, the message says which of the two situations you are in and what to do: rename the others at the provider, or link this one by hand. They need opposite fixes, and one sentence used to cover both.
- Link Git Token lists your tokens at the provider and lets you pick the one a vault entry holds. Providers never re-expose a token's value, so this is the only reliable way to bind the two.
A replaced credential no longer holds the short Code its replacement should have: a dead entry's code is retired with a marker, which frees it and can be read back. A Git Tokens screen, reachable from the infrastructure menu and from Manganese, lists exactly the columns a token owner needs.
Expired credentials are flagged, never hidden
A token past its declared expiry used to disappear from the dropdowns while staying silently selected on the records already using it. Both states are now shown as a prefix — the sand has run out, or it is running out — and nothing is filtered on a date you typed by hand. A token believed expired but still working stays usable; the automatic resolvers still skip a truly dead one.
Connecting a provider account¶
A provider connection (OAuth) lets Muppy act on your behalf on the provider's API: list your projects, resolve your identity, describe a token. It is never served to a box — your App Servers authenticate git with your own repository tokens.
- It is ephemeral: unused for longer than the window you chose at connection, it is revoked at the provider and forgotten here. The ceiling is set per provider now, because what a connection can do differs per provider (GitLab 2 hours, Codeberg 4 hours by default).
- Disconnecting revokes at the provider, by the route that provider actually documents. When a provider offers no way to revoke — Codeberg and other Forgejo instances do not — Muppy says so, records that an authorization it cannot cancel is still live, shows a red block on the connection and reminds you daily, with a link to revoke it by hand. Muppy's own idle sweep is not treated as such a case: it strands nothing.
- Connection Doctor and Check connection report both halves of a connection — the token refresh and a real API call — instead of a badge alone.
Connecting is reachable by Manganese users too, from Manganese → Git: a box resolves its owner's credentials, so the owner has to be able to connect.
The registration of the Muppy application at your provider is documented in Registering the Muppy OAuth Application, and its three buttons (Muppy's guide, the provider's instructions, the provider's application page) now sit in the application block itself — previously the button that opens the page where you obtain the Client ID only appeared once the Client ID was filled in.
GitHub, Forgejo / Codeberg and Azure DevOps¶
The credential helper is no longer a GitLab feature.
- GitHub describes a token as far as GitHub allows, and says where it stops: a classic token's reach can be corrected from what the API reports, a fine-grained one's cannot, and a block on the vault says which case you are in. GitHub cannot list your tokens, so Link Git Token is not offered there — and registering a Muppy application for GitHub is deliberately not offered either, since nothing would consume it.
- Forgejo / Codeberg tokens are described by listing the owner's tokens and matching on their last characters — the standard introspection endpoint answers "inactive" for every token, healthy or revoked alike, so a control built on it would have reported dead for everything.
- Azure DevOps comes under the credential helper. Until now an Azure box kept an authorization header that Muppy wrote at provisioning and never removed, so git never asked the helper and the whole model sat inert behind a correct-looking configuration. Switching an Azure box to the helper now clears that header, and removing the helper writes it back.
Azure DevOps and GitHub Enterprise
Azure DevOps stopped accepting OAuth application registrations in April 2025, and Muppy does not offer a registration form for it. GitHub Enterprise is declined explicitly rather than called at a guessed address. Azure DevOps has no repository-level token, so a read-only organisation token is the finest containment available there, and the refusal messages say so.
What happened, and when¶
Three records, side by side, each reachable from the one above:
| Record | What it holds |
|---|---|
| Git Session | Who is logged in on a box, since when, and why it closed |
| Git Authorization | What you approved, for which operation, for how long |
| Git Operations Log | What actually happened — every credential served, every refusal, every login, every introspection |
The operations log is the forensic one, and it keeps the rows the other two cannot: a login refused three times before being approved, a push that gave up waiting for an answer, a read served without anyone being asked. Each row introduces itself (Credential use — git ls-remote on acme/app), carries a form where the whole trace can be read, and never shows a credential: a token embedded in a URL cannot reach the title.
Retention is 90 days by default, with its own setting: this table is the only place that keeps a refused login or an unanswered request.
On the box¶
- The tool is installed on every App Server, whatever its qualifier, so a developer whose push fails learns the helper exists at the moment it fails.
- Every deploy re-converges the box's git tooling. Until now provisioning was the only moment Muppy wrote it, so a box that drifted stayed that way.
- Remove git helper now removes the capability, not just the wiring: the box is logged out, its live authorizations are revoked, and the retired pre-push hook is deleted. It refuses beforehand when the token it would have to write back to disk is not read-only and repository-limited, rather than leaving a box unable to do git.
- Reinstall / Repair is for a helper box; a separate developer-mode button refreshes the tool on a box that deliberately does not use the helper.
- A network glitch during a wait is reported as a network glitch — nothing was released, and nothing was refused — instead of a traceback or a false "timed out waiting for approval".
Zero-trust edge: the approval URL has changed
The page where a login or an operation is approved moved from /git-login/<code> to
/mpy/git_provider/decide/<code>. Nothing on a box holds that address — it is printed
fresh each time — but if you maintain the allowlist of a zero-trust front end
yourself, list the new path along with the box-facing endpoints. An unlisted path is
redirected to a login page, which a box can only read as "unreachable"; run
muppy-git-status on the box and it now names exactly which path is blocked.
What to configure¶
The git settings are grouped by subject, and each block says what is below it before saying why it matters: the two approval windows, the read-only token policy, the provider connection ceiling, the operations log retention, the nightly logout hour and the takeover cooldown. See Git Credential Settings and the read-only token policy — the switch that turns the read-write fallback into a refusal, off by default.
Manganese users manage their box's git helper from the App Server form itself: the mode it runs in, its repair, and the counters leading to their own sessions, authorizations and operations.
Muppy Vault — your secrets encrypted at rest¶
Muppy stores the credentials that let it act: SSH private keys, git and API tokens, database passwords, S3 keys — around thirty secret fields. Without encryption every copy of the database is a copy of those keys. Muppy Vault encrypts them at rest under a master key that exists in your password manager and in the memory of a small companion process, and nowhere else.
The full guide is the Muppy Vault section: what it protects, how to activate it, day-2 operations, certification, revealing secrets and troubleshooting.
How it works¶
A tiny key holder (mpy-masterkeyd) keeps the master key in memory. Muppy asks it to open
a keyring, and the keyring decrypts the secrets. The holder never fetches a key from
anywhere — a human enters it, or the vault stays sealed. It is shipped as a service on
new App Servers and as a Kubernetes part, so a platform upgrade no longer replaces the
holder and no longer forces a re-entry.
Activating is a deliberate act¶
Nothing is encrypted until you run the ceremony
Upgrading to this release encrypts nothing and changes no behaviour. Activation is a guided ceremony on Vault → Encryption, and converting the secrets you already hold is a second, explicit gesture. Read Activating Encryption first: it lists the five things to have in place, including the one people skip — every credential needed to reach a backup must be recoverable without a running Muppy.
Keyring administration is granted, never inherited
Being a Muppy administrator no longer makes you a custodian of the master key. Only the initial administrator and the main user are seeded; everybody else is a deliberate grant in Settings. Members who inherited it before this release keep it — review that list once.
The conversion verifies before it clears¶
The sweep that converts a family of secrets now reads each value back from the encrypted store, decrypts it and compares it with the plaintext in hand before clearing the old column. A value that fails to verify keeps its column, its encrypted copy is removed, and the case is recorded and raised. The conversion reports what it did in four numbers — encrypted, cleared, failed, unreadable — and the dialog asks you to take a backup immediately before, which is the undo a verified sweep leaves you.
Certification mode¶
A mode for whoever ships a release or declares a new family of secrets: the vault runs for real while the plaintext stays in its old column as a witness. Every read is served from the encrypted store and audited against that witness; a divergence serves the trusted value and raises an alarm rather than reaching the user. An exit protocol on the Encryption screen says which of its four steps you are on and what is holding it, and ends on a choice — commit, or roll the trial back and leave no trace. Export / Compare / Import produce and check the reference file the certification rests on; the file is built as it is sent and is stored nowhere on the server.
Revealing a secret asks who you are¶
A field that hands over an identity outside Muppy — an SSH private key that is not Muppy's own — is no longer revealed by a toggle. Verify to reveal asks for your password, and the confirmation covers that gesture on that record and nothing else. It expires by itself, and the secret hides again on the next page load. Approving a git push asks the same way.
See Revealing Secrets.
Living with a sealed vault¶
A vault is sealed when the holder has no key — after a machine reboot, or because you pressed Seal. In that state Muppy keeps running: reads return empty instead of failing, and anything that would use a secret refuses cleanly with a sentence naming the remedy, rather than acting with an empty credential. Jobs blocked by a seal are findable under a named filter and replayed in one gesture once you unseal.
Three signals report it: a banner on the Encryption screen, a notification to the keyring administrators, and a health check on the vault's monitoring endpoint that flows into the alerting you already run. The screen also carries a Coverage table naming every family and its fields, a self-test safe to press on a live instance, an audit trail of every ceremony, and a read-only window on the encrypted rows.
Stopping the key holder is not sealing
A restart of the holder — a systemd restart, a redeploy — restores the key by design, so it never demands a re-entry. Only the Seal button erases it, and only a machine reboot empties the memory it lives in.
Two-factor authentication
The legacy Muppy two-factor addon is removed. Odoo's native TOTP is the supported path.
Public URLs, custom domains and DNS¶
Your application behind Cloudflare's proxy¶
An App Server's public name can now be served through Cloudflare's proxy rather than pointing straight at the host: the record is created proxied when the domain's policy asks for it and the zone behind it can proxy. Every domain form shows a Proxied column read from the provider itself, and two App Servers can no longer be given the same public name — the attempt is refused and the provisioning wizard warns first.
What a zone can do is measured, not assumed¶
Run Check Connection on each Cloudflare zone
A zone now declares two capabilities read from Cloudflare itself: whether it can give certificates to names two labels deep (Advanced Certificate Manager), and whether it can serve custom hostnames (Cloudflare for SaaS with an active fallback origin). Every zone starts with both switches off and one Check Connection sets them. Until then, that zone's Custom Hostnames page and the Declare button stay hidden, and it cannot be picked as the backend of a Muppy Builtin DNS server.
The reading goes both ways: a feature turned off at Cloudflare is turned off here. A call that errors leaves the switch as you set it and says so — "we could not ask" is not "it is off".
Your customer's own name in front of your application¶
A customer's own host name — shop.mycompany.net in front of your App Server — is now
declared from Muppy, travels the same path a DNS record travels, and its state travels
back. You keep an inventory, a reconciliation target and a billing base instead of a list
of names declared by hand at the zone.
The gesture is a button on the Traefik Application, and it does things in the order that works: it reconfigures the application so the router knows the name, then declares it. Filling the name and declaring it without reconfiguring used to leave a healthy certificate and an active declaration pointing at a 404. A name already declared by hand at the provider is adopted, with its certificate intact, never duplicated.
The per-tenant quota starts at zero
Each custom hostname is an edge certificate you are billed for, so the quota per tenant defaults to 0. Raise it before a tenant can declare one.
Changing the name afterwards is refused while a declaration is live — one keystroke must not orphan a paid-for certificate. Full procedure: Custom Domains and Preparing a Cloudflare Zone.
Restricting visitors by IP address, behind the proxy¶
Trust the proxy before asking for the forwarded address
An IP whitelist on a proxied application used to compare the address that opened the connection — which is Cloudflare's edge, never a visitor's — so it admitted or refused everyone at once. A Traefik Server now carries Trusted forwarding proxies, and a Traefik Application a Client Address setting. Set the server's trusted proxies first: asking for the forwarded address before the proxy is trusted leaves the filter with no address at all and the application answers 403 to everyone, yourself included. Two banners on the form say so.
Applications already in service gain the capability without switching template: with Client Address left on the connection address, what is rendered is unchanged.
A whitelist admits several ranges¶
An application could admit exactly one CIDR range; it now admits several, as the Traefik server's trusted proxies already did. Extending a whitelist no longer means editing a range shared with other applications.
Re-enter the range, then reconfigure
There is no migration: an application that had a range must have it selected again. Until then its stored configuration fails loudly at render rather than quietly admitting nobody — press Reconfigure reverse proxy on the App Server after re-entering. An enabled whitelist with no range is now refused outright, because an empty one refuses every visitor.
Zero trust follows every name¶
Sunray protects an application, and an App Server carries several names: its private one, its public one, its Code Server's, and now its customers'. The protected-host inventory is built from the applications themselves, one entry per name served, so a custom domain reaches the zero-trust gate instead of being forwarded to a gate that had never heard of it. A name is matched however the visitor typed it — case and trailing dot are normalised.
A loopback web.base.url now stops the inventory, loudly
The inventory publishes an entry built from web.base.url. Sunray refuses a whole
response as soon as one entry names something that cannot designate a reachable host
(an IP literal, a bare label, localhost), so a misconfigured instance silently
stopped every one of its hosts from syncing. Muppy now refuses to publish such a name
and says which value is at fault. Set web.base.url to the public name of the
instance.
Muppy Builtin DNS¶
- An MBD domain reports its credential expiry instead of showing "Unknown" for life. An older server that reports nothing is read as "not reported", never as "never expires".
- An MBD server mints its own token from a button, which was impossible before: the record could not be saved without a token and the token could not be created without a saved record.
- The server URL must be https. The first request over plain http burns the token for
good, and the field's help used to offer an
http://example. - An MBD server's backend zone must be able to host sub-zones — a zone without Advanced Certificate Manager cannot certify a name two labels deep, so every client URL it published answered a TLS error and nothing refused the configuration.
See Muppy Builtin DNS.
Cloudflare API tokens are checked¶
A Check token button measures whether a Cloudflare API token is still alive, with the expiry the provider reports and the age of the measurement beside the verdict. An account-owned token — which the standard verification endpoint refuses — is checked against the account endpoints instead, and the account it belongs to is filled in. Every call behind the verdict is recorded on the credential, which is the half worth reading when a token comes back rejected: deleted at the provider and account-owned-but-unmatched are told apart by the refusal payloads alone.
The URLs of an App Server¶
Two URLs, decided by origin¶
An App Server's two URLs were decided by the order of a list, which a drag handle wrote. They are now decided by where each one comes from:
| Button | Opens |
|---|---|
| Direct URL | The application the server generates for itself, else its first non-public definition |
| Load Balancer URL | Your Default LoadBalancer URL if you set one, else the first public application |
Each button hides when its URL is empty, instead of offering a control that opens nothing. The Default LoadBalancer URL field is now written by nobody but you — an automatic write would destroy the override the field exists to carry — and setting it to a non-public application is allowed, with a warning saying what that means later.
APP_PRIMARY_URL and APP_LOADBALANCER_URL stop naming the same thing
On the box, APP_PRIMARY_URL is now the direct URL and APP_LOADBALANCER_URL the
resolved public one (empty when there is none). Scripts and application code that
read APP_PRIMARY_URL expecting the public address of a server that has one must read
APP_LOADBALANCER_URL, falling back to APP_PRIMARY_URL. The variables converge at
the App Server's next environment push.
The whole model is in How Muppy Exposes Your Application. The External App URL field, which held the URL of an application published by something other than Muppy's reverse proxy, is removed — a Traefik Application Definition covers that case, and no record was using it.
The health check watches a URL you choose¶
Which URL the App Server's health check probes is now a Health Check Target setting on
the App Definition and on the App Server, rather than a consequence of how applications
happen to be ordered. It also fixes a form that displayed //health where the probe used
/health, and an empty URL that raised instead of being left alone.
Host names are generated by form¶
The FQDN generators are named after the shape of name they produce rather than a use
case, so a customer on their own Cloudflare zone no longer has to pick a "Tenant" generator
with no tenant. Two shapes that existed in practice but had no generator gained one. A new
Traefik Application Definition arrives with a generator and a matching Host Name
Pattern, and is named after the URL it generates — [template] pattern.domain — instead
of after the generator, which said nothing about the names produced.
No URL moves
Changing what a generator produces would not rename the application it made, so a changed shape is always a new generator and the old one is retired rather than edited. The Use LXD as Sub-domain flag on a host is deprecated — it let two servers of the same name land on one URL — and the migration moves the servers that relied on it onto the generator that produces exactly the name they had. Verified name by name before and after: nothing moved.
Code Servers¶
- A Code Server created on its own, for a user on a machine with no App Server behind it,
is now named after its user — the name used to degenerate to the host, so two users
on one machine shared one name and the URL carried the
codr-prefix twice. - Its URL is resolved by the Public Exposure Definitions list, in link order, instead of by a drag handle on a list that every reconfiguration rewrote. Measured before and after: no Code Server changes the application it resolves to.
App Definitions¶
A duplicate carries the whole configuration¶
Duplicating an App Definition rebuilt six of its collections and silently dropped four — the systemd unit definitions, the Traefik application definitions, the Code Server links and the Kubernetes deployment tag map. The result looked complete: the tab was simply empty, and every App Server provisioned from it inherited the hole.
Every collection now declares how a duplication carries it, in one place, so a collection added later is either carried or deliberately left out. Four many-to-many links had the mirror defect and were copied when they should not have been — one of them mirrors a real PostgreSQL grant, so a copy displayed an access nobody had given.
Deployment profile pointers now stay empty on a duplicate rather than aiming at the original's releases. Consequence to know: a duplicate builds nothing until you pick its Default K8s Package Profile, which is required as soon as Kubernetes deployment is on. Base profiles — the recipes several App Definitions derive from — deliberately keep following the copy.
Only an App Definition can be duplicated¶
An App Server is a running instance bound to a host, a database, a reverse proxy and a PostgreSQL cluster. A second one is made by instantiating its App Definition, and the web client no longer offers Duplicate on an App Server. Deriving an App Definition from an App Server stays supported through the Create App. Definition button — and a definition made that way no longer claims its units were imported from a repository file, nor joins the high-availability pack of the server it came from.
(re)Sync All now carries the App Definition's environment files, which it used to leave behind silently while an adjacent button covered the same collection.
Creating an App Server¶
- The wizard keeps the App Definition's branch instead of realigning it onto the
repository's default branch — opening a definition pinned to
19.0-oshused to show18.0. - Duplicate detection keys on the repository you actually chose, so a generic App Definition no longer blocks every creation with "Server already exists".
- Typing a Project ID on a GitHub, Azure DevOps or Forgejo definition no longer sends a request to gitlab.com carrying your GitLab token.
- The token selector is narrowed to the owner's own tokens and browsing lands on the git list, where the columns that tell two tokens apart live.
- A class of shell-injection through branch names, fork sources, repository URLs and git author names is closed structurally: every untrusted value now reaches git as data, not as syntax. Author names containing apostrophes work as a side effect.
The Odoo and Muppy App Definitions¶
The Odoo (ikb v2 and osh) App Definitions now declare their systemd units in Muppy, as the
Muppy 18 definition already did, and the mpy_setup templates no longer generate a
services.yml at build time.
Commit .muppy/services.yml in your stack repository
Services belong in the repository, committed, not generated at build. If your stack
repository does not ship .muppy/services.yml and your setup script no longer writes
one, the App Server ends up with no services. Muppy's own stacks ship it already. See
the services.yml guide.
An App Server already provisioned from those definitions
Migrate it in this order, from its form: Sync from the App Definition first, so the unit definitions arrive, then Configure systemd units, so the units are deployed from them. Configuring first removes the units the old file declared before the new definitions exist.
Three more things on these definitions:
- Every App Server is now typed for the Odoo server-detection layer by its qualifier —
production, staging or development. A production or staging server used to boot as
undefined, which catches mail and disables scheduled jobs. - The setup script is uploaded from its record before every build, so a repository that ships none, or one that was re-cloned, no longer builds silently against nothing.
- Which App Definition owns a CI/CD webhook is decided per instance, on the record, instead of being re-applied from shipped data at every upgrade.
Server plans¶
A fractional CPU is now a real limit¶
Re-apply the limits of your 0.5 vCPU plans
A fractional plan emitted a soft CPU share alone, which only bites under load and restricts nothing visible: the container saw every core of the host and sized its workers on all of them. It now also emits a hard limit of one visible core, keeping the share for density. Existing fractional containers are not migrated — press Re-apply Plan Limits (developer mode) on each, which pushes the CPU and RAM of the plan current at that moment and leaves the disk alone.
Resizing between plans now unsets the keys the new plan does not use, so an upgrade drops
the share and a downgrade adds it back. A fractional plan on a virtual machine, and a
fraction above 1, are refused instead of being silently rounded. The plan form computes the
effective limits as you type, and the resize preview shows them, rendering 0 as
unlimited.
Names and codes proposed, ranges generated¶
A plan's size code is the key Manganese and the MCP pass to create and resize an App Server, and it was typed by hand: three naming conventions coexisted and nothing refused a duplicate, while the lookups that resolve it would silently take the first match. The form now proposes both the name and the code from the specifications, following the scheme its siblings use, and a duplicate code is refused.
A wizard generates a whole range in one pass — both machine types, a ladder that steps by one vCPU up to 10 then widens, and RAM following a ratio you set from your real hosts. The 36 lines are editable before anything is created, generated codes carry a prefix so an older hand-made range keeps working, and a line landing on an existing code arrives unchecked. See Server Plans.
Sizes are read in GiB¶
Memory and disk are written in GiB everywhere — the plan's name, the form labels, the resize
preview, the error messages, and what Manganese and the MCP show — because GiB is what LXD
reserves. The same quantity appeared as Go, Gb or GiB depending on the screen. Plan names
change accordingly (10.0 GiB RAM); the sizes themselves do not.
The container wizard sizes the way a plan does¶
In the LXC creation wizard, Machine Type, vCPU, RAM and Disk now generate the LXC launch flags by the rules a plan applies, so a container sized by hand and one sized by a plan receive the same limits. The flags stay editable, and your edits hold until one of the four fields changes again. Specifications a virtual machine cannot take — a fraction of a vCPU, no explicit RAM — are refused at launch rather than launched as a container. See Creating a Container or VM.
Hosts, tasks and files on the box¶
A powered-off host is not a failure¶
A stopped instance or container is a managed state, not a fault. Updating facts and the two temp-folder purges now end done, with the reason recorded, instead of failing with a timeout traceback and retrying every day. A stopped container that has lost its address reaches the same gate, the connectivity probe answers "unreachable" instead of crashing, starting and stopping an instance records its power state, and starting a container re-probes SSH so the reachability badge follows the guest back up.
A daily purge of the temp files¶
Nothing had ever removed anything from the scratch directories Muppy uses on a host, and
files from months back were still there — including copies of /etc/muppy.env left by the
Download button, which carried the database password and the git tokens.
One daily job now walks the managed hosts and purges what applies to each, with a retention
per family: 18 hours for scratch files, 48 hours for pg_dump folders, which are a
recovery point with their own policy. It replaces the old backup-purge job, whose cron entry
is removed by migration; that one only selected hosts owning a database cluster, while the
residues sit on every host Muppy has ever downloaded from.
The manual Purge temp folders button is unchanged and now says what it does: it empties everything regardless of age, an upload or dump in flight will fail, and the daily job is the one to prefer.
/etc/muppy.env is no longer world-readable¶
Existing App Servers converge at their next environment push
The file carries the database password and the repository tokens in clear, and was the
only secret-bearing file Muppy writes that anyone on the box could read. It is now
0640, owned by root and readable by the App Server's own user — the identity every
legitimate reader already runs as. Boxes provisioned before this release keep the
old mode until their environment variables are pushed again, and nothing reports
which. Push them.
Pre-existing leftovers in the scratch directories are cleaned by the daily purge above,
but a file downloaded long ago may be older than any retention: rm -f ~/.muppy/tmp/*
on the box settles it.
Two adjacent leaks close with it: uploaded secrets were staged in a world-writable directory under a predictable name, and the wrapper script that runs a remote command — which carries the command and any environment values passed with it — was world-readable. Both are now private to the user running them. An environment file that cannot be read now says so on standard error, naming the file, the user and the owner, instead of failing silently much later as a missing database connection.
Start / Stop / Restart All stop at your own units¶
The buttons above the systemd list acted on every unit registered on the host — the Code Server included, so Stop All took the user's IDE down. They now act on the App Server's own units only, the same set Deploy and Upgrade already filtered on. Refresh All still reaches every row, so no status goes stale.
Kubernetes¶
The Kubernetes Dashboard chart repository moved
The upstream project was archived and changed organisation, and its old index answers 404 — every Dashboard install and every chart sync against the four shipped packages was failing. The new address ships with this release, along with chart 7.14.0, the last one published. The default stays 7.13.0, the version tested here; 7.14.0 is available, not automatic.
Stop before upgrade on a package profile is retired
Stopping before a migration job is now a property of each part, declared in the Parts Config, rather than a switch on the profile that meant delete the release and reinstall it — which recreated every pod, re-issued the TLS certificate and replaced the master-key holder at every upgrade. An upgrade now scales only the parts that ask for it down to zero, waits for their pods to be gone, runs the job, then restores them and moves the image. A failed job leaves those parts stopped and a Resume gesture brings them back, from the form or over the MCP.
The profile field stays visible and read-only, labelled as no longer used, until 2027-01-01. The delete-then-install behaviour survives as reinstall before upgrade for packages that have no parts, and the migration carries it over for those.
Release candidates need Any SEMVER change as Upgrade Job Trigger
SEMVER Major change and SEMVER Minor change compare the three numbers of a version
and ignore what follows the hyphen, so between two candidates of the same release —
18.97.0-rc.1, then 18.97.0-rc.2 — they see no change and skip the migration job. On
the profiles that receive candidates, set the Upgrade Job Trigger to
Any SEMVER change, which compares the whole version, or to Always.
Also in this release:
- A Kubernetes pack upgrades in one call. Its members share one database, so they move together and in order, primary first: a failed primary leaves its followers waiting rather than upgrading them against a half-migrated database, and only the primary runs the migration job — a follower is wired to a standby database that refuses every write.
- The master-key holder runs as its own part, on its own pinned image, so a release upgrade leaves the holder and the key where they are. It authenticates its callers through the Kubernetes API, and its namespace is the boundary. See mpy-metapackage.
- Pinning a chart version works even before the chart is synced. Version lookups ranked on the version Muppy had observed rather than the one you declared, so a pinned but unfetched package was invisible — and on the "latest" path an unsynced package won over every properly synced one.
- The minimum metapackage version is enforced on the install wizard's path too. It was checked on one route only, and a release installed through the wizard could go live against a chart too old to honour its configuration, with every pod dying.
- A dashboard finds its Deployment by the chart's name when no label identifies it, so the link is filled and the wait before a migration job has something to watch.
- Download logs on a pod works again — every click used to end in an error.
High availability¶
The first generation of LXC packs is retired¶
Export what you still read on those screens before upgrading
The first generation of LXC packs — packs launched from a published LXC image, with their updates and rollbacks — is removed, together with the clusters that grouped their hosts: the menus Muppy Packs, Muppy Pack Updates and Muppy Pack Database Updates under Deprecated / Legacy Packs, Muppy Clusters and Muppy Cluster Resources under Hosts, the Muppy Pack and LXC Application blocks of the host form, and the cluster fields of the PostgreSQL cluster and Traefik server forms. The upgrade removes their data with them, and logs, for every field it removes, how many records still held a value.
Nothing the current packs use goes with them: Packs under High Availability — a pack of App Servers, its members, its pools and its meta clusters — is untouched. The first generation had nothing left to run on: no host was attached to one of its packs any more, and its Upgrade Application button could only end in an error.
Manganese¶
- The Application URL a tenant sees is resolved, not "whichever application sorted first": the public URL when the App Server has one, its direct URL otherwise. The form field, the card, the Open App button and the MCP all read the same value, and the MCP stops promising a public URL for servers that have none.
- The git helper is managed from the App Server form — its mode, its repair, and counters leading to your own git sessions, authorizations and operations. Those counters are computed as you, so a number always matches the list its button opens.
- The default container pool has a settings screen instead of an error message telling you to set a system parameter by hand.
- The App Server URL preview in the creation wizard resolves the App Definition's real name generator, rather than looking one up by a key that no longer exists.
Muppy and AI agents¶
The infrastructure MCP server now declares, for every domain it exposes, which methods an
agent may call freely and which become a permission request a human approves: freely on a
dev or test record, a request everywhere else. Each model says where its environment
comes from — its own qualifier, or the record it belongs to. Creating and writing records
is no longer granted silently.
Two exceptions are deliberate: deleting a Helm release always asks, whatever the environment, because it is the one call a reinstall does not undo — it takes the volumes and the database with it; deleting a pod stays on the domain policy, because that is how a pod is restarted.
Every remote command an agent runs leaves a permission request as its journal — approved by a human or auto-approved — carrying the command with secrets masked and its result. That now includes a command run inside a pod, which is what lets an agent finish setting up a development release it has just deployed without any secret leaving Muppy.
Two gestures that existed only as wizards are now methods an agent can call: creating a release and upgrading one to a profile. Probing a release goes through its health check rather than a bespoke call.
High availability is readable over the MCP. An agent reading an App Server that belongs to a pack now sees the pack — which member serves as primary, which wait as standbys, and that a standby is wired to a database that refuses every write — instead of one machine among namesakes. A new domain describes the meta clusters, their sites and the resources in them, read-only: activating another site moves every application of a cluster and its database primary with it, and stays a decision for the person responsible for it. Kubernetes packs are described the same way.
An App Server's database password is no longer returned over the MCP, including through the environment variables that carried it verbatim.
What an agent can and cannot do is documented in Muppy MCP.
A refused remote command has two possible refusers
A web application firewall between your agent and Muppy scores the text of a command, and the agent's own permission layer judges the call. Neither reaches Muppy, so the server log is empty either way, and they need opposite fixes. The MCP help now separates the two and says that the returned exit code and error output report nothing about a block.
Notifications and health checks¶
- A Pushover notification names the instance that sent it. Every Muppy instance pushes through the same application, so the icon and the title were identical for all of them and the body carried no clue about which server had spoken. The sender is now on the last line, labelled From:, on every path including the test wizard — which was the one notification with no indication of origin at all.
- The link back into Muppy no longer carries a double slash.
- Every toast names its instance too, in a footer band, with the arrival time on those that stay on screen. Several App Servers open side by side all day, and a toast otherwise said nothing about which one produced it.
See Health Checks.
The version you are running¶
Muppy is deployed from a git checkout, so the release it serves is the tag of that checkout. That version is now read once and shown everywhere it was missing: at the bottom of the user menu, under a separator, and in the instance's own health endpoint — which until now published the version of a single component under the name of the product.
How to read a Muppy version number, including what a prerelease or a bare hash means, is on the release notes index.
A release candidate — 18.97.0-rc.1 — links to the page of the release it precedes,
which it opens early under a preview banner. Customers who want a release before it is out
receive its candidates: their servers are qualified vNext. How a change travels from a
feature to a candidate, then to a release, is described in
Branches, releases and version numbers.
Two more entries in the user menu: Activate / Deactivate the developer mode, which reload the page you are on instead of sending you to the settings, and a Support link that reaches Muppy support rather than Odoo's shop. The whole menu had stopped being extensible under Odoo 18, so none of the Muppy entries had been appearing at all.
Fixes¶
App Servers. Opening an App Definition from a link shows the App Definition form, not
the App Server one. APP_LOADBALANCER_URL is published on Traefik-based App Servers, for
parity with the Kubernetes one. The deploy wizard reads a branch from the repository itself
rather than from an API a repository-scoped token cannot call, so an existing branch is no
longer reported as missing and blocked. A server derived from a packed App Server no longer
joins that pack. The legacy host-name fallback is marked deprecated and kept — removing it
would strip most existing App Servers of their direct URL.
Git. A credential is resolved for the host git asked for, not for the box's own repository, so a dependency on a second forge is served correctly and a push is approved under the right forge's name. A GitLab project access token is recognised as repository-scoped instead of account-wide. A GitHub token's expiry keeps its hour instead of being read as midnight, which made a live token unusable up to a day early. The provisioning clone of a private repository is routed through the helper and gets a session — it used to fail with nothing but a bare error. The welcome banner stays out of non-interactive shells, where it corrupted the output of commands Muppy parses. A login prompt is no longer recorded as a refused credential. Declining a credential no longer pops an IDE password dialog.
Vault. A fresh database no longer loses the private half of its Muppy key at initialisation. The ceremonies and the identity check survive the browser's save-then-click sequence, which made them impossible to complete from a browser. A ceremony launcher opens for a keyring administrator who is not a general administrator. Compare shows its report instead of closing its dialog. The export closes its dialog and a spent identity check says so, instead of reporting a wrong password for the right one. The self-test stops failing on processes that are merely idle. Chatter entries render instead of showing their markup.
Credentials. A credential is resolved by its code before its name, so a rotated entry no longer resolves to the retired one. One default read-only token and one default read-write token on the same forge is a correct setup and is no longer flagged as a conflict. The permission scope of a fine-grained token is shown as a readable block rather than squashed onto one line. A public key and its fingerprint are copied in one click — the three views that offered a copy button never actually had one. Two-factor fields and git debug fields no longer appear on credentials that have neither.
Hosts and containers. The container poll survives a guest whose address has not been allocated yet, which used to kill an unrelated provisioning job when two ran at once, and it repairs a control address left empty on a container created while it was still booting. Lookups on a record that has no identifier yet no longer take down every creation form. New on the LXC Profiles list opens the form again; it ended on an error as soon as a default SSH key existed.
Kubernetes. Writing a cluster resource no longer fails on a field that does not exist — switching a high-availability pack's primary hit it every time. An upgrade whose Upgrade Job Trigger finds no reason to run the migration job now skips it, instead of failing on an unsupported-trigger error.
High availability. The Refresh button of a pack acts again — it did nothing at all. A pack can be saved before its primary is chosen. An App Server whose health check passes reads healthy in its meta cluster, instead of Unchecked.
Forms. Ten form titles that rendered at a fraction of their width — a long name looked truncated while the stored value was intact — are back to full width. A message shown before a confirmation no longer arrives with the layout of its source file in it. A qualifier's colour is picked on the palette, as on its list, instead of typed as a number from 1 to 12.