157 Commits

Author SHA1 Message Date
Álvaro Tejero Cantero
1342b27f6f
Merge remote-tracking branch 'origin/develop' into iop-vc-preview
Merges develop at f6f9df9a1a. Twelve files conflicted; each keeps
both intents.

files_update.clj: develop's per-stage validation timings wrap the
branch save path. `update-file` keeps the branch's early timer and
passes it to `update-file*`, inside develop's try/catch timing log.
`process-changes-and-validate` times the changes application and
still returns the file, the fixed-up change vector and the media
index. The audit props carry develop's revn and modified-at plus the
branch props. The timing test destructures the new return shape.

v3.clj keeps the branch-file overwrite guard and takes develop's
`configure-import-transaction`. migrations.clj puts develop's 0155
to 0157 before the branch's 0800 to 0805, so the branch's stay the
tail of the vector.

errors.cljs checks the merged and archived branch refusals before
develop's retrying case. left_header.cljs keeps the branch wrapper,
the review badge and the branch bar on develop's markup: the status
dot without icons and the DS library icon on the shared badge.
palette.scss and assets.scss keep the branch clearance terms on
develop's logical properties. The test runners, requires and
deftests are unions.

AI-assisted-by: claude-opus-5-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
2026-10-28 06:28:08 +01:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +02:00
Andrey Antukh
ba78ccb8c9
✨ Harden file data offload job with lock, summary and metrics (#12403)
Take the file row with FOR UPDATE SKIP LOCKED when the offload job
starts and skip the run when the row is locked or gone, so the job
never runs while the file is being edited. Every live hot-storage
row of the file moves to cold storage inside one all-or-nothing
transaction, and the handler answers a summary map with the
offloaded rows, bytes and per-type stats.

Add bounded metrics for the job: rows and bytes counters labeled by
row type on successful runs only, and a timing histogram with the
ok, skip and error outcomes. The job definition now requires
metrics, the recorders live in app.jobs.metrics, and the existing
ephemeral job timing histogram pins its buckets.

Cover the job with a new test namespace: the happy path, the locked
row, missing and deleted files, the rows and bytes summary with its
no-op rerun, the whole-file abort on storage failure, the metric
definitions and emissions, and the submit-time params validation.
An end-to-end suite reads, snapshots, writes, exports, imports and
deletes offloaded files; concurrency tests pin the stale-revn skip,
the blocked-edit retry and the reclaimable orphans of a partial
failure. The file-gc scheduler test pins one job per eligible file,
and the scheduler private helper drops its bang.

Document the lock invariant, the deliberate no-pacing decision and
the metric label sets in the backend storage memory and the
configuration guide.

Related to #12398.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-22 15:31:16 +02:00
Elena Torró
05e1981acc
✨ Support running several exporter replicas (#12487)
* ✨ Support running several exporter replicas

* 📚 Document one exporter per machine instead of compose replicas

* 📚 Trim exporter replica docs to the essentials

* ✨ Cancel an exporter's jobs when it shuts down

* 📚 Replace exporter replica docs with a short beta note
2026-10-21 15:30:46 +02:00
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Andrey Antukh
028547e19b Merge remote-tracking branch 'origin/main' into staging 2026-10-21 10:09:59 +02:00
Andrey Antukh
021492a577 🐛 Make migration 0156 compatible with PostgreSQL 15
`SET STORAGE DEFAULT` needs PostgreSQL 16, but the official docker
images pin `postgres:15`, so migration 0156 failed on those
deployments. The migration now uses `SET STORAGE EXTENDED`, the
storage mode of the type of every column it touches (text, text[],
jsonb and bytea), which leaves the catalog in the same state on any
supported version.

The documented PostgreSQL floor goes back to >=15, the reason for
raising it was that syntax.

Verified on the test database: the 62 columns reset and no table
column keeps the legacy EXTERNAL storage.

The backend memory now records that migrations must stay within
PostgreSQL 15 syntax.

AI-assisted-by: minimax-m3.1-flash-free
2026-10-20 20:36:24 +00:00
Andrey Antukh
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +02:00
Andrey Antukh
fdd66978fd Merge branch 'main' into staging 2026-10-19 20:10:20 +02:00
Andrey Antukh
2d335e9e83 🔧 Reset explicit EXTERNAL storage to PG defaults
Drop the legacy TOAST STORAGE EXTERNAL overrides set when PostgreSQL
ran on ZFS with transparent compression. Migration 0156 restores the
type default (SET STORAGE DEFAULT) on 62 columns across 21 tables;
existing TOAST data keeps its strategy until rewritten.

Raise the documented PostgreSQL floor to >=16, required by the new
SET STORAGE DEFAULT syntax.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 15:37:13 +00:00
David Barragán Merino
d6e1cb23e0
🐳 Serve Penpot and the Admin Console from a subpath (#12358)
* 🐳 Serve the frontend under the path of PENPOT_PUBLIC_URI

Requests below the path are rewritten to the root, so a reverse proxy can
forward them unchanged. The Admin Console keeps receiving the original path.

AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>

* 🐳 Strip the public path before proxying to the Admin Console

The Admin Console is served at the root of its own server and applies the
public path at runtime, so it needs a matching Admin Console image.

AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>

* 📚 Document subpath deployments in docker-compose.yaml

AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>

* 📚 Describe subpath deployments without proxy rewrites

The frontend now serves the subpath itself, so the reverse proxy must
forward it unchanged.

AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>

---------

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-10-19 15:49:41 +02:00
Álvaro Tejero Cantero
6ab7b3ebd6
Merge branch 'develop' into iop-vc-preview
Both conflicts are tail appends onto regions develop rewrote. In
migrations.clj keep both appended blocks, develop's two entries
first so the branch's four stay the tail of the vector. In
climit.edn take develop's rewritten entries and re-append the
branch's provisional block, moving the map's closing brace to the
last line.

AI-assisted-by: glm-5.3-flash
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
2026-10-14 16:34:48 +02:00
Andrey Antukh
0c0336da42 Merge remote-tracking branch 'origin/staging' into develop 2026-10-13 21:04:58 +02:00
Andrey Antukh
eafa483a18 Merge remote-tracking branch 'origin/main' into staging 2026-10-13 14:56:45 +02:00
Andrey Antukh
dd6cc835ef 🎉 Add graceful instance drain via /readyz
Add a per-process availability flag (app.health) that /readyz
reports as 503 OFFLINE while offline, so the load balancer
drains the instance and in-flight requests finish untouched.

Operator surface is app.srepl.main/set-offline and
app.srepl.main/set-online; a restart comes back online.
Covered by backend-tests.health-test; HA docs updated.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-13 14:33:32 +02:00
Francis Santiago
6839b16735 📚 Document subpath configuration and migration 2026-10-09 12:26:57 +02:00
Andrey Antukh
c17eb2a804
📚 Document disable-email-verification team auto-add behavior (#11425)
The configuration docs claimed it is not possible to add someone to a
team without them accepting an invitation email, but the backend
auto-adds existing profiles to the team when the email verification
is disabled (create-invitation in app.rpc.commands.teams-invitations).

Replace the incorrect sentence with an accurate exception note, add
the missing disable-email-verification entry to the "Other flags"
reference, document the CLI-created profile auto-add behavior in the
docker getting-started guide, and describe the shortcut in the
developer authentication docs.

Closes #11424

Reported in #11376

AI-assisted-by: glm-5.3-flash
2026-10-08 12:44:39 +02:00
Álvaro Tejero Cantero
f6dc4bed2d
📚 Replace the branching guide page with a placeholder
The page described a feature that changes daily behind the `:branching`
product flag, and every change cost two descriptions: the page and the
design notes the review reads. The page now names the featureset it
will cover, branches, pull requests, reviews and merges, and promises
its description at the point the feature is proposed for merging into
develop, written against the code that the proposal ships.

The name moves with it, because the page covers version control for
files and not one of its mechanisms.

AI-assisted-by: mimo-v2.6-pro
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
0e51e5b3b8
🐛 Keep the conflict refusal's error page and fix the guide
The backend answers a :conflict refusal (:file-modified, :file-locked)
with HTTP 409 since the previous commit. On the frontend that type had
no ptk/handle-error method, so the version-restore flow would have
fallen to the generic toast instead of the error page the old 500
reached. errors.cljs now maps :conflict to the same exceptional-state
handler.

The technical guide's audit paragraph said rename, archive and delete
report nothing; they report a duration and an outcome. The merge step
list now records the re-validation after the repair and its refusal,
and the update section names the same refusal.

AI-assisted-by: mimo-v2.6-pro, claude-opus-5-5
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
16b7e64fbb
📚 Make the technical guide describe the op-log engine it ships with
The guide still opened by calling a branch an isolated copy of a file,
called the merge base a snapshot frozen at creation and the op log
append-only, and gave the creation transaction a reason for deferring
constraints that does not hold. It now says a branch copies no data,
that an update from main repositions the base and squashes or empties
the log, and that every row referencing the new file is inserted after
it.

The same pass corrects five more statements: base_branch_revn holds the
branch file's own revision; a branch kept after a merge keeps its pin;
op log rows survive a logical deletion and go with the cascade when the
file row is hard-deleted; a resolution may be a per-attribute map; and
the listing now runs through `audited`, while a refusal raised as an
error writes no audit event. The RPC table gains get-branching-limits,
the test lists name all five test namespaces, and the UI and event
lists name the limits, merge, and info dialogs.

AI-assisted-by: claude-opus-5-5
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
aad3719721
📚 Name the audited set, not every mutation on a branch
The sentence opened with every mutation and then listed three that are the
exception, which a reader has to reconcile. It now opens with the set that
does attach the three keys: the mutations through `audited` and a save routed
to a branch's op log.
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
c68833d209
📚 The audit props cover the mutations, not every command
The guide claimed the three keys for every branch operation. The three metadata
mutations and the three reads carry the generic RPC event alone, which records
who called what and no duration, so the section now says which is which.
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
f0d6852394
📚 Say which command attaches which audit props
The guide claimed the three audit keys for every branch operation through
`audited`, which was true of creation, compare, merge, update-from-main and
materialise. A save routed to a branch's op log attaches them on the
`update-file` event instead, because that is the props map the ordinary save
already replaces.
2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
fcaf1945fe
📚 Name the three readers that share the summary cache 2026-10-06 10:36:55 +02:00
Álvaro Tejero Cantero
c78e6f6eea
📚 Describe the size gates and the per-operation audit record 2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
8b714497b0
📚 Describe bounding the branch side, and where it does not apply 2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
7e623c4526
📚 Describe materialization, the preview's exit door 2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
f39905455e
📚 Describe the listing summary cache 2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
dbab1d5620
📚 Reword remap-refs for the derived-data model 2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
9b3436e7a2
📚 Describe the op log storage model
The technical guide documented the full-copy branch model this change
replaces. Update the conceptual model, data model, creation steps,
ahead/behind computation, update-from-main (squash), and the limitations
section to describe the derived-state model.
2026-10-06 10:36:54 +02:00
Álvaro Tejero Cantero
2a5a235063
✨ Add the design preview of file branching and pull requests
This commit carries the design-branching preview by Juan de la Cruz:
elhombretecla/penpot:branching at 0ac265a3f5, which holds 45 commits
and 3 develop merges and was last synced with develop on 2026-09-16.
The branch is squashed together with a merge of origin/develop at
7c039231f7, so the tree is the merged result.

The merge resolved conflicts in six files:

- backend/src/app/migrations.clj: develop's 0155 and 0156 steps run
  first, then the branching steps; the names differ.
- backend/src/app/rpc/commands/viewer.clj: both requires stay.
- common/src/app/common/flags.cljc: both flags stay.
- frontend/src/app/main/ui/dashboard/file_menu.cljs: "Create branch"
  becomes a menu-item* with the same id, guard and handler.
- frontend/src/app/main/ui/dashboard/grid.cljs: the portal goes with
  develop's menu rewrite; the branches popover stays in the trigger.
- frontend/translations/en.po: both sides' entries stay, with
  develop's source references.

Co-authored-by: Juan de la Cruz <delacruzgarciajuan@gmail.com>
AI-assisted-by: mixed models
2026-10-06 10:36:54 +02:00
Andrey Antukh
e6983a1e4e
✨ Enforce idle and absolute session expiration (#11654)
* ✨ Enforce idle and absolute session expiration

Sessions now expire on two server-side conditions: an idle window
(PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE, default 7d) and an absolute cap
from creation (PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE_ABSOLUTE, default
30d, enforced by the token :exp claim). The daily session-gc task
deletes rows that exceed either window, so idle sessions can no
longer be replayed and active sessions are not deleted at the idle
window.

Also remove the legacy v1 HTTP sessions: the http_session table and
the string-id / :ver 0 token code paths are gone. Any v1 cookie now
requires a fresh login.

Document the session expiration configuration in the technical guide
and add a backend memory describing the token, renewal and GC model.

Closes #11646

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Address session-expiration review findings F1-F4

Fix the unreadable test (a stray paren broke whole-suite
discovery), enforce idle expiration on every request in
wrap-authz, fail boot fast when the absolute cap sits below
the idle window, and align config defaults with the memory
rule while fixing its migration number and stale reference.

Closes #11646

AI-assisted-by: muse-spark-1.3-contributor
2026-10-01 10:12:05 +02:00
Andrey Antukh
eec06a5987
🐛 Allow registration with disabled public registration (#11912)
* 🐛 Allow invitation-based registration when disable-registration is set (#5178)

Per documentation, disable-registration 'disables registration
(still enabled for invitations only)'. Two bugs prevented this:

1. verify_token.clj: when processing an invitation token for a
   non-logged-in user with no member-id, the redirect included
   registration-disabled? in its condition, sending invited users
   to the login page instead of the register page.

2. auth.clj validate-register-attempt!: the registration-disabled
   check fired unconditionally before the invitation-token check,
   rejecting the actual register RPC even with a valid invitation.

Fix: in verify_token.clj remove registration-disabled? from the
redirect condition for new-user invitations. In auth.clj restructure
the check as an if/else: with an invitation token, validate the token
and allow registration; without one, enforce the flag as before.

* 🐛 Allow registration with disabled public registration

Allow valid team invitations to create new profiles when public
registration is disabled, while keeping password login and invitation
validation required.

Add backend regression coverage for flag combinations and verify-token
redirects, frontend route coverage, and configuration documentation.
Closes #5178

AI-assisted-by: space-bunny-free

* 🐛 Revalidate active invitation during registration

Require a live, unexpired team invitation before using the
registration exception, and recheck it before creating a profile.
Reuse the same lookup in invitation token verification.

Add regression tests for canceled and expired invitations, the
registration race, and explicit redirect contracts. Update docs
and backend auth guidance.

AI-assisted-by: Space Bunny Free

* 🐛 Lock and normalize invitation registration checks

Lock active invitation rows during transactional registration and
acceptance so cancellations cannot race with profile or membership
creation.

Normalize invitation emails before comparisons and database lookups.
Add concurrency, email casing, and final flag regression tests.

AI-assisted-by: Space Bunny Free

---------

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-09-25 14:13:12 +02:00
Andrey Antukh
acd146f6f4
🐛 Restore rate-limit headers and add Retry-After on 429 (#11895)
* 🐛 Restore rate-limit headers and add Retry-After

The account-lockout change replaced the header-forwarding 429 handler
with a body-only one, so existing RPC rate-limit responses lost their
x-rate-limit-remaining and x-rate-limit-reset headers. Account lockout
never sent Retry-After either.

Make handle-error :rate-limit preserve ::http/headers and add a
retry-after header when the exception carries a non-nil :ttl in
seconds, keeping the current JSON body. Add focused tests for both the
lockout and the RPC limiter paths.

Document activation, defaults, password/LDAP scope, Redis fail-open
behavior, and the lockout risk, and record the final HTTP contract in
the backend auth memory.

Refs #11397

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Add Retry-After to RPC 429 and expose headers in CORS

Address review follow-ups on the account-lockout 429 contract:

- The RPC limiter now sets retry-after in its 429 headers (seconds
  until the longest rejecting limit resets), so it matches the
  account-lockout response and the HTTP standard.
- CORS exposes retry-after, x-rate-limit-remaining, and
  x-rate-limit-reset so browser clients can read them.
- Use backticks for Retry-After and account-locked in the docs for
  consistency with nearby sections.

Refs #11397

AI-assisted-by: deepseek-v4.1-flash
2026-09-24 12:19:29 +02:00
Andrey Antukh
b3c1aab720 Merge remote-tracking branch 'origin/staging' into develop 2026-09-23 19:19:18 +02:00
Dr. Dominik Jain
6031f6c318
⬆️ Upgrade MCP SDK to v2, removing SSE support and HTTP session management (#11841)
* ⬆️ 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
2026-09-23 15:07:19 +02:00
David Barragán Merino
40ef4a90d5 📚 Document Admin Console setup for Docker (>= 2.18)
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 20:00:54 +02:00
David Barragán Merino
d842b835d3 📚 Reorder menu entries for setup methods
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-22 20:00:54 +02:00
Andrey Antukh
117c8db0bb Merge remote-tracking branch 'origin/staging' into develop 2026-09-22 10:24:30 +02:00
Andrey Antukh
d68531b783
⬆️ Update devenv dependencies (#11790)
* ⬆️ 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
2026-09-22 10:22:31 +02:00
Andrey Antukh
e34c618c3a Merge remote-tracking branch 'origin/staging' into develop 2026-09-22 08:44:45 +02:00
Álvaro Tejero-Cantero
d8fddf9942
♻️ Keep agent content on standard paths, drop client shims (#11719)
* ♻️ Keep agent content on standard paths, drop client shims

Agent guidance stays where the ecosystem reads it: AGENTS.md at the
root and the skills in .agents/skills. Codex scans .agents/skills and
follows symlinks, opencode lists it among its discovery locations, and
Cursor, Zed, Warp, Cline and Amp use the same project path.

Claude Code reads neither, so it needed CLAUDE.md and .claude/skills.
Committing those two paths is not a neutral default. Both are
gitignored, git treats ignored files as expendable, and a checkout
that needs the path deletes whatever a contributor keeps there without
a warning. Neither sparse-checkout nor skip-worktree offers a way out,
because both make the next pull abort. The two links are now created
by the contributor, once per clone, and documented in
.agents/README.md.

AGENTS.local.md and .agents/local/ give every client one personal
layer that composes on top of the shared one, instead of one file per
client inside the repository.

sync-workspace seeds the two links for ws1 and above, the way it
already seeds the gitignored config.js, so the devenv workflow does
not change.

AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* ♻️ Lean on Claude Code reading AGENTS.md, drop the pointer

Claude Code 2.1.277 reads AGENTS.md through the built-in agents-md
plugin, whose default mode loads it in a project that has no
instruction file of its own. Deleting the committed CLAUDE.md is what
turns that on here, so the pointer a Claude user had to create is no
longer part of the setup, and only the .claude/skills link remains:
the plugin covers instruction files, and skills still load from that
path alone.

The instruction that replaces the pointer is a removal. A CLAUDE.md,
.claude/CLAUDE.md or CLAUDE.local.md anywhere from the root down to
the working directory hands the project back to CLAUDE.md and hides
this repository's AGENTS.md, while .claude/rules/*.md and the
person's own ~/.claude/CLAUDE.md do not count and stay usable for
personal steering. CLAUDE.local.md joins the ignore list, and a hard
rule in AGENTS.md forbids all three.

The docs name the version, say the fallback has not reached Bedrock,
Vertex or Foundry, and say the stable auto-update channel runs about
a week behind the default one, so the symlink is documented as the
stopgap for those cases and as something to drop afterwards.

sync-workspace seeds the skills link alone for the same reason.

AI-assisted-by: claude-opus-5
Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 📎 Remove skills directory mention on AGENTS.md

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-22 08:43:25 +02:00
David Barragán Merino
b402637fe4 🐳 Allow extending the CSP directives without replacing the policy
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-18 19:05:37 +02:00
David Barragán Merino
1e8acbb2db 🐳 Cover the inline scripts of the served pages with CSP hashes
The frontend build now emits the sha256 hashes of the inline scripts of every page it writes into resources/public, the image moves them out of the document root, and the entrypoint splices them into the default script-src. This removes one of the two reasons why enforcing mode was not usable.

The hashes are computed on the rendered output rather than on the mustache templates, since the digest covers the exact bytes served between the script tags. All four served pages contribute, not just index.html: challenge.html handles the redirect, render.html is loaded by the exporter in a headless browser, and rasterizer.html is initialised by the frontend itself, so leaving any of them out would have broken those paths under enforcing mode. The storybook previews are excluded because that container does not serve them.

A bundle predating this change yields no hashes and the policy stays as it was, so older bundles keep building.

The three external locations were also passing through the security headers of their upstreams. raw.githubusercontent.com returns its own Content-Security-Policy and both it and fonts.googleapis.com return Strict-Transport-Security. Browsers enforce the intersection of every policy they receive, so the upstream one takes precedence on those responses, and the HSTS one lands on our own host, meaning a deployment that deliberately disables HSTS would get it set anyway by a third party. Hide all three at the proxy.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-18 18:55:19 +02:00
Andrey Antukh
b660ea9d53 ✨ Route the app by query string with screen key
Move SPA routing out of the URL fragment into the normal query
string. The screen travels in a reserved `screen` key holding
the route name (`?screen=workspace&team-id=…`); every other
param keeps its name. `rt/nav` and `rt/resolve` keep their
signatures.

This deletes the fragment-mirroring URL surgery, simplifies
link-preview (the server sees everything) and nginx (single
path, no SPA fallback rules needed), and migrates OIDC
redirects, email links, e2e helpers and plugin test utils to
the new format.

Legacy `#/…` URLs translate client-side for one Penpot version
(`legacy-routes`, marked TODO(next-version)); non-SPA paths
are untouched.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-15 17:10:54 +02:00
elhombretecla
a91d81c695 🎉 Add link preview metadata for shared links 2026-09-15 17:10:54 +02:00
David Barragán Merino
52723b3851 🐳 Add configurable CSP and HSTS headers to the frontend image
Ship both headers from the image so that every deployment starts from a sensible default instead of each installation deriving its own policy. Report-only mode never blocks a request, so this changes no behaviour for existing deployments, and HSTS stays absent unless PENPOT_PUBLIC_URI declares an https scheme.

The policy can be narrow because the frontend already reverse proxies its own external dependencies, so 'self' covers them. What it must permit beyond that comes from the code: 'wasm-unsafe-eval' for the render engine, 'unsafe-inline' styles for the inline style attributes of the UI, and blob:/data: for thumbnails, exports and fonts.

Closes #11374
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
AI-assisted-by: Claude
2026-09-15 13:41:51 +02:00
Shreyash
3f1c88fca2
📚 Document MCP and internal resolver environment variables (#11572)
Add the frontend MCP variables PENPOT_MCP_URI and PENPOT_MCP_URI_WS,
the PENPOT_INTERNAL_RESOLVER variable used by nginx to resolve the
internal service names, and the enable-mcp flag to the configuration
guide. All three were used by the frontend docker image but were
missing from the docs.

Closes #11318

AI-assisted-by: claude-opus-5

Signed-off-by: Shreyash Agare <envisiontechdevelopers@gmail.com>
2026-09-09 08:16:04 +02:00
Andrey Antukh
f5aad7b1ae Merge remote-tracking branch 'origin/staging' into develop 2026-09-01 12:45:17 +02:00
Andrey Antukh
73d3d63616 ✨ Enable a way to provide custom opencode config on starting devenv 2026-08-31 16:31:24 +02:00