Expose selected agent skills as slash commands and pass invocation
arguments to their entry points.
Update the devenv OpenCode CLI to v2.0.16 with checksums for both
supported architectures.
AI-assisted-by: space-bunny-free
Add the User-Agent values used by Mattermost, OpenGraph.xyz and the
Twitter Card Validator to the production and development
link-preview routing maps.
This lets those crawlers reach the dynamic Open Graph response instead
of receiving the generic Penpot preview.
AI-assisted-by: space-bunny-free
Bump the OpenCode V2 binary in the devenv image from 2.0.12 to the
current npm latest tag (2.0.15) and refresh the per-architecture
SHA-256 checksums for both arm64 and amd64 tarballs.
AI-assisted-by: deepseek-v4.1-flash
* ⬆️ Upgrade MCP SDK to v2 and remove HTTP sessions
MCP's per-request protocol removes the need to retain HTTP sessions.
Use the v2 handler to manage each request's transport and lifecycle,
so requests can reach any server instance without session affinity
or the workaround that adopts sessions through private SDK fields.
Keep legacy SSE support and the shared plugin and Redis bridges.
Remove the shared expiry checker, including legacy SSE idle expiry;
SSE connections now remain until disconnection or server shutdown.
Verify stateless requests, token isolation, and legacy compatibility.
Resolves#11827
AI-assisted-by: gpt-6-astra
* 🔥 Remove legacy MCP SSE support
Use Streamable HTTP as the sole MCP client transport so the server no
longer needs a separate SSE connection registry or lifecycle.
Remove /sse and /messages, their nginx routes, and the server-legacy
dependency. Legacy SSE clients must switch to /mcp; older Streamable
HTTP clients remain supported. Document the migration and verify that
the removed endpoints return 404.
Resolves#11846
AI-assisted-by: gpt-6-astra
A compose port mapping delivers traffic to the container address,
never to loopback, so `listen 127.0.0.1:8082` made
`ports: <host>:8082` fail from the host. Both configs (image
template and devenv) now use `listen 8082`, which binds every
interface and matches the implicit bind of the public
`listen 8080 default_server`.
Rewrite both block comments to state the new bind and who decides
access from outside the host. The scrape URI stays on
127.0.0.1:8082: it still reaches the socket.
AI-assisted-by: mimo-v2.6-flash-free
* 🐳 Add upstream diagnostics to nginx access log
Enrich every access-log line with the internal journey of the request:
the status the backend answered (us), the time spent connecting to it
(uct), the time spent waiting for its answer (urt) and the internal
address that served the request (ua).
A plain 502 line used to say nothing about where the request died. With
this format, the tail of the line classifies the failure: connection
rejected, backend accepted and hung (uct + urt under 1s), or backend
stuck until read timeout. This was the missing witness in the Sep 20
incident, where nginx received connection resets with zero timeouts and
zero rejections.
Applied both to the production image template and the devenv config.
With proxy_pass on variables there is no upstream keepalive, so uct
measures one real TCP connection per request.
Parsing the new fields (us, uct, urt, ua) on the log shipper is left to
ops, so they can be filtered in Loki.
AI-assisted-by: glm-5.3-flash
* 🐳 Add stub_status endpoint for nginx metrics
Add a dedicated localhost-only server (listen 127.0.0.1:8082) exposing
/stub_status next to every other location of the public server. Ops can
run the official nginx-prometheus-exporter as a sidecar against
http://127.0.0.1:8082/stub_status and get nginx_connections_active,
accepted vs handled, reading/writing/waiting and request rates in
Prometheus.
Binding it to localhost and its own server keeps it unreachable from
outside the host and out of the public surface, and access_log off
avoids polluting Loki with one line per Prometheus scrape. The base
image already ships stub_status compiled in, so no image rebuild is
needed.
Applied both to the production image template and the devenv config.
AI-assisted-by: glm-5.3-flash
* ✨ Expose http server gate metrics (worker and connector)
The backend already measured dispatch latency but nothing reported the
state of the "house door": the xnio worker queue and threads, and the
monitor-level listener counters. This was the exact blind spot of the
Sep 20 incident, where the server kept answering health checks while it
accepted connections and dropped them without response.
Add a periodic metrics sampler that lives and dies with the http
server (single daemon thread, 15s interval, each sample guarded so an
unexpected error does not cancel subsequent runs) and publishes:
- worker (xnio MXBean gauges): penpot_http_worker_queue_size,
busy_threads, pool_size and max_pool_size. Negative samples are
discarded: the MXBean transiently reports -1 on the busy thread
count (verified live), and a stale negative would read as zero.
- listener (Undertow connector statistics, enabled via the new
:server/statistics yetti option): penpot_http_connector_active*
_connections gauge and requests_total / errors_total counters.
Undertow exposes absolute totals, so the sampler keeps a watermark
atom and publishes deltas, skipping (and moving forward past) a
counter reset.
The connector-level part depends on yetti v11.11, which now accepts
a :server/statistics server option (patch authored and released
upstream; before it, ListenerInfo#getConnectorStatistics always
returned nil).
New tests cover the samplers with fake MXBean/collector statistics
against real prometheus collectors, including the negative-sample
filter, the delta/watermark logic and the sampler lifecycle.
AI-assisted-by: glm-5.3-flash
* 🐛 Include jdk.management in the backend runtime JRE
The production image builds a trimmed JRE with jlink and omitted
jdk.management. Without that module the OS MXBean is
sun.management.BaseOperatingSystemImpl, which has no
getProcessCpuTime, getOpenFileDescriptorCount nor
getMaxFileDescriptorCount. The prometheus client StandardExports
reads those getters reflectively and collect() swallows the
NoSuchMethodException, so process_open_fds, process_max_fds and
process_cpu_seconds_total silently disappeared from /metrics while
the other process_* families kept flowing.
Verified against Prometheus: the app job only ever exposed
process_start_time_seconds, process_virtual_memory_bytes and
process_resident_memory_bytes; the fd and cpu families were absent.
Reproduced locally by running the backend metrics registry on a JRE
built with the same jlink module list (false/false/false) and on one
with jdk.management added (true/true/true).
Add the module to --add-modules and pin the metric contract with
backend-tests.metrics-test.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Build the http metrics sampler on promesa.exec
Replace the hand-rolled ScheduledThreadPoolExecutor and ThreadFactory
with promesa.exec primitives: px/scheduled-executor with a daemon
thread factory, and a px/schedule chain that reschedules the next
sample when the current one finishes.
Beyond fitting the existing periodic-task pattern (worker/cron,
rpc/rlimit), the chained schedule makes the docstring promise real:
with scheduleAtFixedRate an exception escaping the runnable cancelled
the following executions, while the reschedule now happens in a
finally block.
The sampler shutdown uses px/shutdown-now (shutdown! is deprecated in
promesa 12.0.0) to cancel the pending sample, keeping the previous
halt semantics.
The lifecycle test moves to the promesa predicates and a new test
covers the error-resilience promise: the first sample runs, throws,
and the next one is still scheduled.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Tighten the http metrics samplers
The samplers are leaf functions: they receive what they need and
publish it. Drop the internal nil guards (if there is no metrics
instance or no mxbean there is nothing to call them for) and move the
checks to the boundary, where the optional data is resolved:
sample-http-metrics now short-circuits with some-> and when-let.
Write the four worker gauges as four static operations instead of a
vector of pairs walked by doseq: the set is fixed, so the collection
only adds an allocation and hides each operation.
Drop the ! suffix from the sample-*-metrics family: ! marks a function
whose contract is to mutate state, while these report, and the mutation
happens in the mtx/run! they call. The constant true return, which only
existed so the removed guard tests could assert it, goes away too.
Tests follow the move: the internal-guard tests are replaced by one
boundary test (a nil server publishes nothing).
AI-assisted-by: deepseek-v4.1-flash
* 📚 Add the function design rules memory
Document the rules that came out of the http metrics sampler review:
preconditions are checked at the boundary instead of re-checked in the
core, optional-by-design data is guarded where the optionality is born,
a fixed set of operations is written statically, ! marks mutation and
not reporting, and production code is not shaped for tests.
Also state in the memory maintenance guide that memories must not use
manual line wrapping.
Linked from critical-info so it is read when designing a solution or
an API, not only when touching the samplers.
AI-assisted-by: deepseek-v4.1-flash
* 📚 Unwrap the critical-info memory lines
The memory maintenance guide forbids manual line wrapping, so rewrite
critical-info with one line per bullet and paragraph. A stray `*` at
the start of one continuation line is dropped.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Drop the redundant guard in the http server halt
create-metrics-sampler always returns the scheduler, so the sampler is
always present when integrant calls halt-key!; the nil check was dead
code, same as the yt/stop! call next to it.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add srepl helper to delete profiles by email
Add `delete-profiles-by-email!` to app.srepl.main. It accepts a
single email, a comma separated list of emails or a coll of emails,
resolves each profile, logs it to audit and enqueues the
delete-object task. The deleted-at is backdated with the configured
deletion-delay so profiles and their owned teams are purged on the
next gc pass.
Extract the per-email deletion logic into a private fn and reuse it
from `delete-profiles-in-bulk!`. Add tests for the new
`parse-emails` helper.
AI-assisted-by: glm-5.3-flash
* 🌐 Complete Catalan translations in frontend
Complete the Catalan (ca.po) locale to 100% coverage against en.po,
using es.po as support reference. Adds the 1439 missing entries
across workspace, dashboard, labels, shortcuts, subscription,
errors, modals and onboarding, keeping vosaltres treatment and
IEC/Termcat terminology consistent with the existing strings.
Normalizes placeholders and plural forms, drops the 14 stale
obsolete entries and canonicalizes the file with the repo
translations script.
Closes#11739
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Add frontend translations memory with Catalan criteria
Record the PO workflow, the sync fuzzy-flag gotcha and the
Catalan glossary and tone agreed upon while completing ca.po,
and link the new memory from the frontend core routing.
AI-assisted-by: muse-spark-1.3-contributor
* 🔧 Add gettext to devenv image
Provide msgfmt and msgattrib in the dev environment for
checking PO translation files.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Fix Catalan translations and add PO checker
Review of the missing-whitespace pattern found ~90 glued words
across 75 entries, plus 4 lost plural forms and 2 placeholder
mismatches verified against tr call sites. All fixed in ca.po.
Adds frontend/scripts/check-translations.js (vocabulary-free PO
QA: glued words, punctuation, placeholders, plurals) with
--self-test, wired as pnpm run check-translations and
documented in mem:frontend/translations.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Multi-locale PO checker with word catalogs
Split the checker engine from its word lists: ca/es catalogs now
live in scripts/check-translations/words.<locale>.txt and all
messages are in English. Adds an es seed (calibrated to zero
errors) and fixes 7 typos it found in es.po. Universal checks
(placeholders, plurals, punctuation) run without a catalog.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Merge PO checker into translations.js
Fold check-translations.js into translations.js as a check
subcommand reusing its locale helpers; word lists stay in
scripts/check-translations/words.<locale>.txt. Also fixes the
getopts stopEarly bug that made -l useless after the command
(sync -l ca synced every locale), drops dead lodash import
and code, unifies help and exit codes. Removes the
check-translations package alias; use translations.js
check -l <locale> with explicit -l.
AI-assisted-by: muse-spark-1.3-contributor
* 🌐 Keep unused placeholders out of the gate
Reverts the %s-stripping on unused auth.terms-privacy-agreement:
the links mirror its markdown sibling and a reactivation may
need them. Placeholder mismatches on #, unused keys now warn
instead of failing, and the rule is recorded in
mem:frontend/translations.
AI-assisted-by: muse-spark-1.3-contributor
* ⬆️ Update devenv dependencies
Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to Java 27
Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MinIO with RustFS in devenv
Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.
Keep the old MinIO volume untouched and use a new RustFS volume.
AI-assisted-by: gpt-5.6-sol
* 🐳 Replace MailCatcher with persistent Mailpit
Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.
Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update Node.js to 24.21.0
Align the host NVM version with the Node.js version used by devenv.
AI-assisted-by: gpt-5.6-sol
* ⬆️ Update devenv to PostgreSQL 18.6
Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.
Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.
AI-assisted-by: gpt-5.6-sol
* 🐳 Expose RustFS ports in devenv
Publish the RustFS S3 API and management console on localhost port 9000
and 9001.
Keep both bindings on loopback so object storage is not exposed to the local
network.
AI-assisted-by: gpt-5.6-sol
* 🐳 Install standalone pnpm in devenv
Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.
Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.
AI-assisted-by: gpt-5.6-sol
* 🔥 Remove corepack, use system pnpm everywhere
Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.
Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix exporter watch missing render-wasm build step
The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.
AI-assisted-by: muse-spark-1.3-contributor
* 🔧 Add opencode V2 support and adapt plugins
Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.
Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.
AI-assisted-by: muse-spark-1.3-contributor
* ⬆️ Update pnpm and opencode
* ⬆️ Update pnpm and its deps
* ⬆️ Update JVM dependencies in backend and common
Update several JVM dependencies across backend and common:
- passay 1.6.6 -> 2.0.0 (package reorg, ctor-based rules)
- siphash 2.0.0 -> 3.0.0 (SipHasher* renamed to SipHash*)
- lettuce-core, guava, sqlite-jdbc, jsoup, lz4-java, markdown-clj,
awssdk s3/sts, selmer, jackson-core/databind, shadow-cljs
Adapt passay validation to the new API (moved packages, constructor
configuration) and siphash to the renamed classes. Add tests for
password validation and UUID advisory-lock hashing.
AI-assisted-by: deepseek-v4-flash
* ⬆️ Update node on docker images
* 📎 Minor fixes related to pnpm12 compatibility
Remove the guard in stop-devenv that refused to stop ws0 while any
ws1+ instance was running. Each workspace is now fully independent
and can be started/stopped in any order. Shared infra shuts down
only when no instances remain running.
Updated docs (devenv.md, agentic-devenv.md) and devenv memory to
reflect the new behavior.
AI-assisted-by: mimo-v2.5-pro
Move docker/imagemagick/Dockerfile and docker/devenv/Dockerfile from
ubuntu:26.04 to Docker Hardened Images (Debian 13 / trixie).
imagemagick gets a true non-dev runtime with its shared libraries
vendored via ldd; devenv keeps the -dev tag as its final image since
it's an interactive development container, not a production
artifact.
Local-only builds by default for build-devenv and
build-imagemagick-docker-image; --push is now required to build
multi-platform and push to the registry. All release-image build
commands (frontend, backend, exporter, mcp, storybook) now accept
--tag to override the image tag. Adds a shared DEVENV_TAG variable
threaded through pull-devenv, the production build function and
docker-compose.main.yml so a custom devenv tag can be used
end-to-end.
When nginx follows a backend 307 redirect to a presigned S3 URL, it was
forwarding the client's Authorization header to S3. Production S3 rejects
this because it sees two auth mechanisms (presigned URL signature +
Authorization header). MinIO in devenv is more lenient and ignores the
extra header.
Fix: add proxy_set_header Authorization "" in the @handle_redirect block.
Fixes#10776
AI-assisted-by: mimo-v2.5-pro
* ⬆️ Updgrade base image for penpot docker images to ubuntu 26.04
* ⬆️ Update playwright
* 🐳 Use dist-upgrade to update all system packages
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 🐳 Split devenv compose for parallel workspaces
Move shared services into an infra compose file and keep the main devenv container plus Valkey in a separate compose file driven by defaults.env. Parameterize host-side ports, container names, source path, and runtime env while keeping container-internal ports fixed for same-origin proxying.
Make tmux startup idempotent, add attach-devenv for the live instance, move shared MinIO user setup to infra startup, and let exporter scripts load backend _env.local overrides.
Co-authored-by: Codex <codex@openai.com>
* 🐳 Run parallel devenv instances against shared infra
Add support for running N parallel devenv instances under separate compose
projects sharing Postgres, MinIO, mailer, and LDAP. Each instance has its
own main container, Valkey, source checkout, tmux session, and host port
range offset by 10000 (3449 -> 13449 -> 23449, etc.).
./manage.sh run-devenv-agentic --n-instances N reconciles the running set
to exactly {ws0..ws(N-1)}: missing instances are created (workspace sync
from the live repo via git ls-files + per-instance env-file generation
under docker/devenv/instances/ + detached tmux startup), surplus instances
are stopped highest-first via compose down (never -v), already-running
instances are left untouched. ws0 binds the live repo at PWD; ws1+ are
scratch clones under ~/.penpot/penpot_workspaces/.
Backend workers (enable-backend-worker) are gated on PENPOT_BACKEND_WORKER
in backend/scripts/_env; ws1+ overlays disable them so async-task
notifications stay bound to a single Valkey Pub/Sub instance.
Compose helpers wrap docker compose with env -i so per-instance overlay
--env-file actually overrides defaults.env -- without the strip, the shell
env from sourcing defaults.env at startup would shadow the overlay (Compose
gives shell precedence over --env-file).
Other:
- Drop network aliases (- main, - redis); use container_name for
cross-container DNS so multiple instances on the shared network don't
fight over the same DNS name.
- Pin volume names via name: (PENPOT_*_VOLUME) so volumes survive project
renames; ws0 keeps the pre-existing physical names (penpotdev_*).
- Remove cross-project depends_on from main.yml (postgres/minio-setup now
live in penpotdev-infra); manage.sh ensure-infra-up docker-waits on the
minio-setup one-shot.
- Strict arg parsing in run-devenv / run-devenv-agentic; --n-instances 0
rejected.
- Remove unused Host-matched server block from the Caddyfile.
Memory mem:devenv/core and developer docs updated.
Co-authored-by: Codex <codex@openai.com>
* ✨ Document and stabilise the parallel-workspace CLI; wire AI agents
Improve parallel-workspaces developer CLI,
and add an opt-in layer that lets four AI
coding agents (Claude Code, opencode, VS Code Copilot, OpenAI Codex CLI)
drive a specific workspace through a single launcher command.
Parallel-workspace semantics
----------------------------
each run-devenv-agentic call brings up one wsN;
--ws N (integer; default 0) targets a specific workspace and auto-starts
ws0 first when N>=1 so the worker invariant holds. --sync is forbidden on
ws0 and re-seeds the workspace from the live repo for ws1+. Stop semantics
mirror the start invariant -- ws0 is the last to stop, shared infra stops
with it, --all walks every instance highest-first. The worker policy
section explains why workers run only on ws0 (Postgres FOR UPDATE
SKIP LOCKED is safe across many workers but the cron dedup primitive is
best-effort, and :telemetry / :audit-log-archive are not idempotent).
Per-instance Valkey Pub/Sub isolation, msgbus topology, and the
"async task notifications miss ws1+ tabs" caveat are stated explicitly.
The mem:prod-infra/core memory captures the same external-services and
task-queue / Pub-Sub topology in agent-readable form, and
mem:backend/core and mem:critical-info now cross-link it so backend work
surfaces the horizontal-scaling constraints from the start.
AI coding agent integration
---------------------------
New top-level .devenv/ directory holds committed templates
(templates/{claude-code,opencode,vscode}.json and templates/codex.toml,
each with \${PENPOT_MCP_PORT} and \${SERENA_MCP_PORT} placeholders) plus
committed shared entries (matching shared/* files for Playwright, the
only workspace-independent server we ship today).
./manage.sh start-coding-agent <claude|opencode|vscode|codex> [--ws N]
launches the chosen client against one workspace. It cd's into the
target's directory (the live repo for ws0; workspace-path "wsN" for ws1+)
and refuses to launch unless (a) the binary is on PATH, (b) the
workspace directory exists for ws1+, and (c) the instance is up
(devenv-main-running) -- the MCP servers only exist while the devenv is
running. The agentic-devenv guide is restructured around this Quick
start path, with a per-client table and a Manual configuration fallback
for clients we don't cover.
Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ Scope the shadow devtools to the dev build
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add SERENA_UPDATE_VERSION env var (in devenv docker-compose.yml)
to dynamically update Serena on agentic devenv without requiring
an image rebuild.
Apply for update to v1.5.0 (also changing initial installation
in Dockerfile to this version).
Serena provides useful tools for the agentic workflow for penpot.
The following additional extensions are added:
1. uv and Serena installation, including a suitable serena_config.yml, are added to the devenv docker image
2. Serena configuration options are set via env vars and flags in manage.sh
3. run-devenv can now take -e flags which it forwards to docker exec
GitHub #9315
New tool to evaluate ClojureScript expressions by connecting to the
nREPL service already provided in devenv.
Add dependency 'nrepl-client' and a corresponding client class
as well as types to support this.
Add a new environment variable for 'devenv mode', which enables
the new tool (PENPOT_MCP_DEVENV).