Compare commits

...
Author SHA1 Message Date
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
andrés gonzález 93b18fb1c1 Add Penpot-specific board size presets (#11565)
Add a "PENPOT" section to the board size presets list with sizes for
File thumbnail, Template cover, Plugin icon and Plugin cover, so
designers no longer need to look up these dimensions manually when
creating thumbnails for files, template submissions or plugin
listings.

Closes #11561

AI-assisted-by: claude-sonnet-5

Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
2026-09-09 08:01:04 +02:00
Andrey Antukh 8fac9cf178 Merge remote-tracking branch 'origin/staging' into develop 2026-09-08 23:01:03 +02:00
Andrey Antukh 61ed2a203f Present full flow and step-by-step mode in implement-plan
Two changes to the implement-plan flow:

- It now shows the whole picture before touching the repo: which
  issue and branch will be created (or the current branch it
  continues on), the execution style, and the task checklist — then
  waits for the user's confirmation. Flow detection is read-only;
  issue and branch creation moved to the execution step.

- New opt-in execution mode: on request ("step by step"), it runs
  one task at a time, commits each one with the create-commit skill,
  and waits for the user's review before the next task. The default
  stays as it was: every task, one commit.

README and the implement-plan command description updated to match.

AI-assisted-by: omen-alpha
2026-09-08 21:00:32 +00:00
Andrey Antukh 3b49ff532f Merge remote-tracking branch 'origin/staging' into develop 2026-09-08 21:41:43 +02:00
Andrey Antukh fbfef42145 ♻️ Move skills and plans to .agents for cross-tool use
.agents is the shared home that opencode, Claude Code (through the
.claude/skills symlink) and Codex all read, so the skills and the
saved plans now live there instead of .opencode:

- .opencode/skills moved to .agents/skills (24 files, no content
  changes).
- .opencode/plans moved to .agents/plans; the .gitignore entry
  follows, so plans stay untracked.
- .claude/skills symlink retargeted to ../.agents/skills.
- planner, make-a-plan and review-plan updated to the new plans path;
  new .agents/README.md documents every skill with when-to-use
  examples and links to each SKILL.md.
- workflow/creating-issues memory: create-issue path updated.

opencode discovers .agents/skills natively, so .opencode needs no
reciprocal link.

AI-assisted-by: omen-alpha
2026-09-08 19:40:38 +00:00
Andrey Antukh f695553469 Merge remote-tracking branch 'origin/staging' into develop 2026-09-08 21:15:58 +02:00
Andrey Antukh 59c8a690da ♻️ Add when to use sections to all skills
Every skill in .opencode/skills now carries a "When to use" section:
triggers in any phrasing plus the matching /command for the flow
skills, one-line triggers for the utility skills, an
explicit-invocation mirror for ste, and the proactive case for
resolve-git-conflicts.

code-review-criteria drops its old usage bullets ("before merging any
PR ...") for the same role pattern as plan-review-criteria: loaded by
the reviewer subagent of the review-code flow, redirect there to
review code. Flow and criteria skills no longer compete for the same
trigger.

AI-assisted-by: omen-alpha
2026-09-08 21:12:28 +02:00
Andrey Antukh b115b75d83 📚 Add CLAUDE.md pointer and Claude Code skills symlink
CLAUDE.md points Claude Code to AGENTS.md as the canonical project
instruction file, and .claude/skills symlinks the opencode skills so
both tools discover the same single source of truth. The root
.gitignore keeps ignoring new files under those paths; the tracked
entries are unaffected from now on.

AI-assisted-by: omen-alpha
2026-09-08 18:04:15 +00:00
Andrey Antukh 3d1393e8fc ♻️ Make commands thin dispatchers over canonical skills
Every command in .opencode/commands now only switches to the build
agent, injects the user context, and delegates to a same-named skill
that holds the full flow logic — skills are the primary mechanism for
Claude Code and Codex compatibility.

- make-a-plan, implement-plan, resolve-git-conflicts: flow logic
  moved to same-named skills.
- review-plan / review-code: orchestration skills delegating to the
  renamed criteria skills plan-review-criteria and
  code-review-criteria, with redirecting descriptions so there is no
  trigger overlap between flow and criteria.
- create-pr: absorbed the open-pr flow as Open mode (validated
  branch-to-PR pipeline using scripts/detect-target-branch) plus a
  new Update mode (gh pr edit); /open-pr removed.

AI-assisted-by: omen-alpha
2026-09-08 18:00:05 +00:00
David Barragán Merino afe62e6592 Add force rebuild flag and adhoc ref builds
Add a `force` input to Bundles Builder, Docker Images Builder and the
admin-console dispatcher to bypass the existing S3/registry skip
checks and overwrite artifacts unconditionally. Propagate it through
_DEVELOP, _STAGING and _TAG (defaulting to false on non-dispatch
triggers, since schedule/push events carry no inputs).

Add a new _ADHOC workflow to build the full image set (bundle + all
docker images + nitrate admin-console) from an arbitrary ref pair,
for one-off builds of feature branches like test-bameda.

Enrich the build/promote step summaries with the built version
(`git describe --tags --always`), a link to the commit and the build
timestamp. Add run-name to all `_`-prefixed workflows so the target
ref (and, where reliable, the commit sha) is visible directly in the
Actions run list.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-08 19:44:44 +02:00
Andrey Antukh 99e6d4f1ad 📚 Forbid hand-editing CHANGES.md in agent guides
Adds the hard rule to AGENTS.md and to mem:critical-info: CHANGES.md
is generated from GitHub milestones during the release process; it
must be updated only via the update-changelog skill flow or on
explicit user request.

AI-assisted-by: omen-alpha
2026-09-08 16:39:35 +00:00
Andrey Antukh f91ea6efc4 Make implement-plan flow-aware and add open-pr command
/implement-plan now detects the flow from the current branch instead
of always creating an issue: on a base branch it starts standalone
(issue + branch issue-NNNN from HEAD); on a feature branch it
continues on it with no new scaffolding. Arguments override
detection (standalone, continue, no issue, from origin/<base>);
"no issue" on a base branch creates a plan-<slug> branch. Execution
is direct and the closing suggests /review-code or /open-pr.

/open-pr opens the PR for the current task branch: it detects the
base with scripts/detect-target-branch (canonical: develop, staging,
main), validates commits, issue and remote state, and stops with one
message listing everything missing. It never pushes.

AI-assisted-by: omen-alpha
2026-09-08 16:39:31 +00:00
Andrey Antukh 32d313b0c8 🎉 Add planning and review agent commands
Add the planning/review command suite for opencode and align the
planner skill with it:

- /make-a-plan (build agent): researches the session subject, drafts
  the plan with the planner skill, asks open questions, then saves
  the final plan to .opencode/plans/.
- /review-plan: reviews a plan with the plan-review skill; on
  approval suggests /implement-plan, on request changes /make-a-plan.
- /review-code: reviews a diff, PR, or code change with the
  code-review skill; suggests /make-a-plan for substantive findings.

All commands run as the build agent with explicit read-only rules
that expire when the command's work is done, so follow-up requests
need no agent switching. The planner skill no longer writes the plan
file on its own: it saves only when a command or the user explicitly
instructs it, and it suggests /review-plan and /implement-plan as
next steps.

AI-assisted-by: omen-alpha
2026-09-08 15:40:15 +00:00
Alejandro Alonso 2e5157e6aa Export image fills to WASM SVG as linked images (#11529)
Emit clipped `<image href>` for fills with a registered media URL instead
of Skia base64 embeds. Register URLs from the frontend and exporter via
store_image_url. Cover rect, path, frame, and text (glyph clip) cases in
SVG export tests.

Closes #11383
2026-09-08 17:30:53 +02:00
Pablo Alba fb6ece7a7e Revert "🐛 Enforce SSRF checks and add timeouts to HTTP client (#11474)" (#11556)
This reverts commit ff63668c1e.
2026-09-08 13:38:00 +02:00
Elena Torró 18e641d79a 🐛 Fix selrect auto-width on selrect click and selrect resize (#11541)
* 🐛 Fix text edge double click needing two undos

* 🐛 Fix text font change needing two undos
2026-09-08 13:32:37 +02:00
efa2518fe8 🐛 Fix deep-harden of host plugin context on load (#11521)
* 🐛 Fix deep-harden of host plugin context on load

ses.harden(context) in loadPlugin deep-freezes every host-owned object
and function reachable through the context. The host keeps needing to
modify those across page navigation (listener wrappers, proxies), so a
later property augmentation (e.g. assigning toString) throws
'TypeError: Cannot assign to read only property toString' and kills the
MCP session (penpot/penpot#11001).

Pass the host context through untouched; sandbox isolation stays at the
compartment boundary (hardened sandbox-owned globals + ses.safeReturn).

Regression test: load-plugin-context.spec.ts (no ses mock).

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Add real SES bootstrap to host-context regression test

The previous load-plugin-context.spec.ts had no SES bootstrap, so it
failed on the original code with 'ReferenceError: harden is not defined'
instead of the intended freeze assertion, and passed on the fixed code
merely by avoiding ses.harden.

Now the spec bootstraps real SES (repairIntrinsics + hardenIntrinsics),
adds a control test proving real ses.harden deep-freezes host-owned
functions (Object.isFrozen === true, later toString assignment throws
TypeError - the #11001 crash signature), and keeps the regression test
asserting loadPlugin leaves host functions unfrozen and patchable.

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Add production-order hardening contrast evidence

Proves the initialization-ordering hazard behind #11001 (cf. #8636):
in production, index.ts runs repairIntrinsics only at module load while
hardenIntrinsics runs later in createSandbox. The original loadPlugin
called ses.harden(context) between those steps, freezing the shared
Function.prototype with plain data properties so later override taming
is skipped and any subsequent fn.toString assignment throws TypeError.

Kept in a separate spec file so the full SES bootstrap in
load-plugin-context.spec.ts cannot mask the ordering effect.

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Apply approved lint fix and CHANGELOG entry

Restores the two approved deliverables missing from the previous push:
the prefer-rest-params fix in load-plugin-harden-order.spec.ts
(replacing the deprecated arguments usage) and the plugins-runtime
CHANGELOG entry for the host-context harden fix (#11001).

AI-assisted-by: muse-spark-1.3

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Remove deep-hardening of host plugin context on load

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Align CHANGELOG and context comment with reviewed evidence

AI-assisted-by: multica-agent
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>

* 🔥 Remove SES semantic tests from plugin regression coverage

Drop the tests that only verify SES library semantics rather than Penpot
application behavior:

- Delete load-plugin-harden-order.spec.ts (pure SES initialization-order
  evidence, never calls loadPlugin).
- Remove the ses.harden control test and its SES bootstrap setup from
  load-plugin-context.spec.ts.
- Remove the #8636 hardening-order contrast test and the now-unused ses
  import from load-plugin-real-path.spec.ts.

Keep the application-level regression coverage: the real loadPlugin
initialization path, permission enforcement, host-context isolation and
safeReturn protection. No production code changes.

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: Omen Alpha

---------

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-08 12:32:45 +02:00
Elena TorróandBelén Albeza af5b767933 🐛 Fix microinteractions on text shape selrects for autowidth/autoheight (#11068) (#11545)
Co-authored-by: Belén Albeza <belen.albeza@kaleidos.net>
2026-09-08 10:29:15 +02:00
Andrey Antukhandalonso.torres 937b3fc65f 🐛 Add missing permission checks to plugin API (tokens, shapes, variants, flows, layouts, user identity) (#11139)
* 🐛 Add content:write permission checks to Design Tokens plugin API

The Design Tokens API (tokens.cljs) had zero permission checks, allowing
any plugin to create, modify, and delete tokens, sets, and themes
regardless of granted permissions.

Add r/check-permission checks to all 22 write operations across:
- token-proxy: name, value, description, duplicate, remove, applyToken
- token-set-proxy: name, active, toggleActive, addToken, duplicate, remove
- token-theme-proxy: group, name, active, toggleActive, addSet, removeSet,
  duplicate, remove
- tokens-catalog: addTheme, addSet

Follows the established pattern from comments.cljs, file.cljs, page.cljs.

Closes #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add permission checks to shape proxy interactions, detach, export, and variants

The shape proxy (shape.cljs) had multiple operations missing permission
checks, plus a cond ordering bug that bypassed the existing content:write
check for text shapes in commit-fills!.

Fix commit-fills! cond ordering: move permission check before the
text-shape branch so text shapes are also protected.

Add content:write permission checks to:
- interaction-proxy: :trigger, :delay, :action setters, :remove method
- shape-proxy: :addInteraction, :removeInteraction, :detach
- shape-proxy: :applyToken, :switchVariant, :combineAsVariants

Add content:read permission check to:
- shape-proxy: :export (read/extraction operation)

Follows the established pattern from :resize, :rotate, :blocked setters.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add library:write permission checks to variant plugin API

The library.cljs variant operations (variant-proxy and
lib-component-proxy) had seven mutating operations that
did not check the library:write permission, allowing
any plugin to create, modify, and delete component
variants regardless of granted permissions.

Add r/check-permission checks to all 7 operations:
- variant-proxy: addVariant, addProperty,
  removeProperty, renameProperty
- lib-component-proxy: transformInVariant, addVariant,
  setVariantProperty

Follows the established pattern from the :name and
:path setters in the same file.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add content:write permission checks to flow and flex layout plugin API

Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.

Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
  startingBoard setters, remove) and page-proxy (createFlow,
  removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
  (remove, appendChild)

Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Add user:read permission checks to plugin API

Add permission checks to user identity accessors that were bypassing
the consent model, allowing plugins to access user data regardless
of whether the user granted user:read permission.

Changes:
- api.cljs: Add user:read checks to getCurrentUser and getActiveUsers
- comments.cljs: Add user:read checks to comment-proxy and
  comment-thread-proxy owner/user getters
- file.cljs: Add user:read check to file-version-proxy createdBy getter

When user:read permission is not granted:
- getCurrentUser() returns null
- getActiveUsers() returns empty array
- owner/user/createdBy getters return null

Follows the established pattern from other permission checks in the
plugin API.

Relates to #11137

AI-assisted-by: qwen3.7-plus

* 🐛 Fix problem with token API

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-08 09:24:45 +02:00
Andrey Antukhandalonso.torres 5f1e151e84 🐛 Validate plugin UI URLs cannot target Penpot's own domain (#11273)
* 🐛 Validate plugin UI URLs cannot target Penpot's own domain

The plugin UI iframe combines allow-scripts and allow-same-origin in
its sandbox. While necessary for plugins to use their own cookies and
storage, this creates a risk if a plugin's UI URL resolves to Penpot's
own origin, potentially allowing the iframe to escape sandbox isolation.

Add validateUIUrl() that checks the resolved URL against Penpot's
origin (from penpotPublicURI or location.origin) and throws if they
match. Called in openModal() after prepareUrl() resolves the URL.

Closes #11271

AI-assisted-by: qwen3.7-plus

* 💄 Fix prettier formatting in plugin-manager.spec.ts

Apply prettier formatting to fix format:check failure.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix problem with penpot origin plugins

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-08 09:23:20 +02:00
Pablo Alba ff63668c1e 🐛 Enforce SSRF checks and add timeouts to HTTP client (#11474) 2026-09-07 15:59:35 +02:00
Elena Torró 7b135b80b2 🐛 Fix background blur clipping (#11532) 2026-09-07 15:51:27 +02:00
Eva Marco 5b97fb9408 ♻️ Migrate editable-select to DS icon* and design tokens (#11500)
* ♻️ Migrate editable-select to DS icon* and design tokens

The dropdown toggle and selected-item tick used the deprecated
app.main.ui.icons component and the SCSS relied on refactor/
placeholders and aliased design tokens.

Replace both icon usages with the DS icon* component (arrow-down
instead of a rotated arrow, tick unchanged, both at DS size "s"),
and rework editable_select.scss to depend only on DS modules:
placeholders inlined, aliases resolved to their terminal --color-*
tokens, sizes/spacing/borders mapped to DS tokens, logical
properties used where they preserve behavior, and per-state colors
driven by component-local CSS custom properties (fed through
currentColor for the icons, since DS icon* strokes default to
currentColor).

AI-assisted-by: claude-sonnet-5

* ♻️ Migrate frame_grid to the DS and unify its options dropdown

Refactors frame_grid.scss off the deprecated refactor/ styling system
onto DS tokens (borders, sizes, spacing, typography), and replaces the
remaining deprecated-icon usages (gap-horizontal, grid-margin) with
the DS icon* component.

Unifies the square/column/row "reset to default" toggle+panel, which
had drifted into two inconsistent, duplicated implementations, into
two shared components (default-options-toggle*, default-options-
dropdown*) built on the existing dropdown component, with proper
menu/menuitem roles and Enter/Space keyboard activation.

Along the way, fixes several regressions the migration surfaced:
- .show-more-options lost its border width when converted to a
  CSS-variable-driven color.
- the reset-to-default toggle was enabled while its own options were
  disabled instead of the other way around.
- numeric inputs in .height/.gutter/.margin lost their explicit width,
  letting them fall back to the browser's intrinsic input width and
  overflow the sidebar.
- a leftover CSS override was forcing the docked dropdown-select's
  icon to render fully transparent.

Also fixes editable-select's is-selected check, which compared a
stringified option value against a non-stringified current-value and
so could never match, silently hiding the selected-item checkmark.

AI-assisted-by: claude-sonnet-5

* ♻️ Migrate frame_grid's numeric inputs to the DS component

Replaces the deprecated app.main.ui.components.numeric-input with the
DS numeric-input* across .grid-size, .height, .gutter and .margin,
using its built-in :icon/:text-icon slots (via input-field*) instead
of rendering a separate icon element beside the input.

The gutter/margin icons previously rotated 90deg via CSS to indicate
row vs column orientation; DS ships dedicated icon-ids for both axes
(gap-horizontal/gap-vertical, margin-top-bottom/margin-left-right),
so the icon itself now swaps per grid type instead of being rotated.
The height row's "H"/"W" text label is now the matching DS
character-h/character-w icon rather than literal text.

AI-assisted-by: claude-sonnet-5

* ♻️ Flatten unnecessary nesting in frame_grid.scss

CSS Modules already scope every class to this file, so nesting
purely to dodge cross-file name collisions is dead weight now.
Rewrite the nested rules as flat top-level selectors, dropping
ancestor-wrapper prefixes wherever the target class is already
unique in the file and keeping them only where they still do real
work (winning a same-node specificity fight against another
component's own class).

Also drop --show-options-icon-color: it was assigned across every
state but never actually read anywhere, so it did nothing.

AI-assisted-by: claude-sonnet-5
2026-09-07 14:37:40 +02:00
Alejandro Alonso 269aa36f82 Export solid and patterned strokes to WASM SVG (#11513)
*  Export solid inner and outer strokes to WASM SVG

SkSVGDevice drops save_layer+Clear used for path Outer strokes, so
solid Inner/Outer strokes on closed shapes are expanded to filled
outlines via stroke_to_path. Center and open paths stay on the shared
stroke path.

Closes #11381

*  Export dotted, dashed, and mixed strokes to WASM SVG

PathEffects do not survive SkSVGDevice, so dotted/dashed/mixed strokes
expand via stroke_to_path with the effect kept, producing filled
outline geometry for all alignments including Center.

Closes #11382

* 🐛 Fix WASM SVG stroke export edge cases

Keep expanded stroke outlines in local space under the leaf CTM so
rotated rects/paths are not double-transformed; overlay open-path caps
after dotted expansion; draw frame strokes outside the content clip;
and route per-side rect/frame strokes through the evenodd band path.
2026-09-07 13:50:58 +02:00
Andrey Antukhandniwinz acc078064b 🐛 Do not register developer tools in multi-user MCP mode (#11310)
Prevent developer tools from being exposed when the MCP server runs in multi-user mode. Keep them available for local devenv usage and document the mode restriction. Add regression coverage for the registration policy.

Closes #11291

AI-assisted-by: gpt-5.6-luna

Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
2026-09-07 13:19:32 +02:00
Alexei Bratuhin 1f12561427 📚 Fix typo in docker-compose.yaml comment (#11517)
Signed-off-by: Alexei Bratuhin <alexei.bratuhin@googlemail.com>
2026-09-07 13:15:41 +02:00
María Valderrama 77bf3ea419 🐛 Fix sso expiration time (#11528) 2026-09-07 11:56:25 +02:00
Eva Marco 5f6169e1d3 🐛 Fix typography sample errors (#11515)
* 🐛 Fix font-family sample not showing for numeric font names

Setting style.fontFamily to a raw, unquoted family name (e.g. "Micro
5") parses it against CSS's <family-name> grammar: a whitespace-
separated sequence of <custom-ident>s. "Micro" tokenizes fine, but a
bare "5" isn't a valid CSS identifier (idents can't start with a
digit) — it tokenizes as a number instead, so the whole property is
invalid CSS and the browser silently drops it. Every other font in
the list happened to avoid this because none of their names have a
token that's purely numeric.

Quote the family name, matching what font-item-preview* (the font
selector's own preview, a few lines down in the same file) already
does, so it's parsed as a CSS string instead of unquoted identifiers.

Also falls back to the live fontsdb entry's family when the
typography record's own :font-family is blank — a font that was
unloaded when a typography's font/variant was last changed can leave
that field nil (the same failure mode remove-nil-style-attrs already
repairs for shape text spans) — and loads the font unconditionally in
the collapsed asset row, matching the expanded editor, since the
optical-offset cache can otherwise skip loading it entirely.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix flaky typography sample position in automated tests

The optical-centering offset for the "Ag" sample (and the font
selector's fallback name label) resolves asynchronously: first paint
is unshifted, then an idle-scheduled Canvas measurement lands and the
sample jumps to its final position. Any test that checks position or
takes a screenshot shortly after paint races that jump — whether it
runs before or after is a timing accident, not a deterministic
outcome, which is exactly the "sometimes a few pixels up, sometimes
down" flakiness QA hit.

use-optical-offset now returns [offset ready?], with both lazily
initialized from the cache so a cache hit needs no async round-trip
at all. sample-text-style hides the glyphs until ready?, so the
sample only ever appears already in its final, correct position
instead of visibly moving there after the fact.

The font selector's own name label uses the same hook but always
shows real text content rather than a decorative sample, so it keeps
the old behavior instead: hiding it would blank out font names while
scrolling, worse than the minor positional nicety it's fixing.

AI-assisted-by: claude-sonnet-5
2026-09-07 11:29:36 +02:00
Andrey Antukh c52778d6f6 Merge remote-tracking branch 'origin/staging' into develop 2026-09-07 11:28:06 +02:00
Andrey Antukh 9462543fb5 Add skip-onboarding option to create-demo-profile (#11526)
Add optional skip-onboarding param to create-demo-profile. When true, the demo profile is created with onboarding-viewed and release-notes-viewed set, so it skips the onboarding flow. Default keeps the current behavior. Cover both cases with RPC tests. AI-assisted-by: muse-spark-1.3-contributor
2026-09-07 11:15:17 +02:00
Andrey Antukh 5c10ea5bd6 📚 Document branch naming convention
The creating-prs memory described a branch format (issue/... with a
<type>/<short-description> fallback) that does not match actual repo
practice, where issue-driven branches are issue-NNNN. Replace it with
a Branch Naming section: issue-NNNN as primary, descriptive name
without slashes as fallback.

Add the matching public convention to CONTRIBUTING.md under Pull
Requests, which previously defined no branch naming at all.

AI-assisted-by: omen-alpha
2026-09-07 08:41:54 +00:00
Eva Marco cd98a88c4d 🐛 Fix errors on shortcuts (#11081)
* 🐛 Fix shift + character recording

* 🐛 Fix importing conflicts

* 🐛 Fix paste as non customizable shortcut

* 🐛 Fix duplicate manage of custom shortcuts

* 🐛 Fix json file name

* 🎉 Add search by commands

* 🐛 Fix CI
2026-09-07 10:14:09 +02:00
Andrey Antukh 1dfa2cd9f2 Merge remote-tracking branch 'origin/staging' into develop 2026-09-07 09:53:58 +02:00
Elena Torró f0680cf5f8 🐛 Handle empty variant options (#11499)
* 🐛 Handle empty variant options

* 🐛 Disable variant value select when there are no options
2026-09-07 09:53:45 +02:00
Andrey Antukh 7ebd7cc0d5 Merge remote-tracking branch 'origin/staging' into staging 2026-09-07 09:52:07 +02:00
Andrey Antukh 2100ed29ea Merge remote-tracking branch 'origin/main' into staging 2026-09-07 09:45:06 +02:00
Andrey Antukh 7e1139b906 🐛 Validate origin and route messages to sender in plugin postMessage channel (#10970)
* 🐛 Fix plugin postMessage channel allowing cross-plugin message injection

The global postMessage listener was broadcasting incoming messages to all
loaded plugins without validating the origin or routing to the correct
sender. This allowed any plugin (or any iframe from any origin) to inject
messages into other plugins.

- Added origin validation — messages from origins other than
  window.location.origin are rejected.
- Added sender-based routing — a message is only delivered to the plugin
  whose iframe contentWindow matches event.source.
- Exposed iframeWindow getters in PluginManager, PluginModalElement, and
  createPlugin so the runtime can compare event.source against the correct
  iframe reference.
- Updated documentation examples to include origin validation and
  recommend window.location.origin over '*' for postMessage targetOrigin.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix plugin origin check breaking cross-origin plugin messaging

The origin check added in the previous commit compared event.origin
against window.location.origin (Penpot own origin). Since plugins
are cross-origin by design (hosted on the plugin author domain),
this check rejected every legitimate message from every real plugin.

The event.source-based sender routing (matching iframeWindow identity)
is the correct and sufficient security mechanism - it cannot be forged
cross-origin, so the redundant origin check was removed.

- Removed event.origin check from load-plugin.ts message listener
- Updated tests to use realistic plugin origins (localhost:4202/4203)
  and to verify rejection based on source identity, not origin
- Fixed documentation examples: use event.source for receiving
  validation and '*' for postMessage targetOrigin

AI-assisted-by: mimo-v2.5-pro
2026-09-07 09:31:35 +02:00
Andrey Antukh 960209f1fa 🐛 Fix update-profile-props not rejecting plugin props at validation
The schema:props schema still included :plugins as a valid key,
so the closed-map validation passed instead of rejecting it.
The system-managed-props denylist only silently stripped the key
after validation, never raised an error.

Derive schema:props-writeable from schema:props using sm/dissoc-key
to exclude system-managed keys, and use it in the update-profile-props
params schema. The original schema:props remains intact for get-profile
result validation.

AI-assisted-by: mimo-v2.5
2026-09-07 09:30:01 +02:00
Andrey Antukh 9289e676e1 Require re-confirmation when plugin manifest differs on open
When the fetched manifest differs from the stored manifest
(code, name, etc.), show the permissions dialog instead of
silently updating — preventing execution of tampered/injected
plugins.

On fetch error, show a warning notification instead of loading
with the old manifest.

Bundled plugins (no URL) skip validation as they are trusted
with no remote source.

Completes the 3-layer defense for T3-N1-02: (1) closed
permission schema, (2) dedicated RPC methods, (3) integrity
validation on open.

AI-assisted-by: qwen3.7-plus
2026-09-07 09:30:01 +02:00
Andrey Antukh a8e0b3c1f8 🐛 Add dedicated RPC methods for plugin registry operations
Add `add-profile-plugin` and `remove-profile-plugin` RPC methods for
atomic plugin registry operations, preventing manipulation via the
broader `update-profile-props` endpoint.

- Close the `:plugins` field in `update-profile-props` schema to
  eliminate the mass assignment attack vector for plugin data.
- Define `valid-permissions` and a closed `schema:permissions` enum to
  restrict plugin permissions to known values.
- Migrate the frontend to use the new granular RPC methods with
  optimistic updates and rollback on failure.
- Add comprehensive backend tests covering valid/invalid permissions,
  updates, removal, and rejection via old endpoint.

AI-assisted-by: qwen3.7-plus
2026-09-07 09:30:01 +02:00
Andrey Antukh c8691d0cde 🐛 Fix crash when a nil shape id reaches WASM modifiers (#10896)
* 🐛 Fix crash when a nil shape id reaches WASM modifiers

A modif-tree containing a nil shape id crashed the WASM heap write
with "Cannot read properties of null (reading '__u32_buffer')" in
propagate-modifiers. Production builds elide the upstream asserts,
so a nil id (e.g. update-dimensions called with a missing shape's
parent id in the variants flow) reached the uuid heap write.

Drop nil-keyed modif-tree entries in apply-wasm-modifiers and
set-wasm-modifiers before they reach propagate-modifiers and
set-structure-modifiers, filter nil ids at the parse level, and
guard the variant reposition/creation events against missing shapes.

AI-assisted-by: hy3

* 🐛 Fix let scoping bugs in modifiers.cljs

Two let forms in set-wasm-modifiers and apply-wasm-modifiers
had their bindings vectors closed prematurely, causing undeclared
var warnings for snap-pixel?, translation?, ids, update-shape,
options, bool-ids, undo-id, and other bindings.

In set-wasm-modifiers: removed premature ) after ] on the let
bindings so the let body wraps snap-pixel? and translation?.

In apply-wasm-modifiers: the cond form was not explicitly closed,
so all subsequent bindings (ignore-tree, options, modif-tree, ids,
update-shape, bool-ids, undo-id) were parsed as cond clauses
instead of let bindings. Added ) to close cond after :else, and
moved ] to close the bindings vector after undo-id.

AI-assisted-by: mimo-v2.5-pro
2026-09-07 08:34:06 +02:00
Elena Torró 00e0492bb5 🐛 Fix import libraries missing team-id (#11505) 2026-09-04 13:49:37 +02:00
Elena Torró d82038a570 🐛 Fix paragraph span restyle on line change (#11503)
* 🐛 Fix paragraph span restyle on line change

* 🐛 Fix text shape boundaries on resize
2026-09-04 13:43:23 +02:00
Marina López 5452eb5c8a 🐛 Fix email sales (#11514) 2026-09-04 13:36:10 +02:00
Elena Torró 0533be100d 🐛 Fix text selection render overlay and double/triple click flash (#11512) 2026-09-04 12:38:31 +02:00
Eva Marco df48c834e3 🐛 Fix create nil typography token (#11489)
* 🐛 Fix stroke width token dropdown closing mid-interaction

The stroke-row* key included a hash of applied-tokens, forcing a full
remount whenever the async token-resolution pipeline updated that
state (e.g. after a detach action settles). If the remount landed
while the width dropdown was open, it destroyed the dropdown and
reset its open state, permanently closing it before the user could
pick a token.

Drop the hash from the key so the row only remounts on actual
structural changes (add/remove/reorder), not on every token
resolution tick.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix crash when a typography token has a nil value

A composite typography token saved with no fields filled in stores a
nil :value. That value reached StyleDictionary's tokens-studio
font-styles preprocessor, which assumes a typography value is never
null and threw, crashing token resolution for every other token in
the file.

Reject the nil value at the source in the typography token form
validation, and as defense in depth, filter nil-valued tokens out of
every resolve-tokens* entry point before they reach StyleDictionary,
tagging them with the existing empty-input error instead.

Also remove a stray debug prn left in find-refs.

AI-assisted-by: claude-sonnet-5

* 🔧 Gitignore local CLAUDE.md

Keep the commit-conventions instructions file local to each
contributor's checkout instead of tracking it in the repo.

AI-assisted-by: claude-sonnet-5

*  Use a single transducer to tag invalid-value tokens

merge-invalid-value-tokens ran three separate passes over the token
map (remove, map, into) and then merged the result back in. Combine
the remove/map steps into one ns-level transducer, defined once
instead of rebuilt on every call, and pass resolved as the seed to
into so the trailing merge isn't needed either.

AI-assisted-by: claude-sonnet-5

* ♻️ Drop redundant t/testing wrapper in nil-value token test

The outer t/testing just repeated the deftest's own name and added
nothing the two inner t/testing blocks (each covering one concrete
assertion group) don't already say.

AI-assisted-by: claude-sonnet-5

* 🐛 Fail the nil-value token test on a resolution error

rx/sub! only handles the success case, so if token resolution ever
errors instead, done is never called and the async test hangs
instead of failing. Switch to rx/subs! with an error handler that
reports the failure and calls done, matching the pattern already
used elsewhere in the tokens test suite.

AI-assisted-by: claude-sonnet-5
2026-09-04 12:31:26 +02:00
Alejandro Alonso 86aebd3429 Reduce WASM text selrect churn after batch page load (#11471)
* 🐛 Align WASM auto-width text size with HTML measurement

Include the HTML paragraph-set 1px right margin in Skia auto-width
so WASM and DOM measurements match.

*  Scope and defer post-font WASM text relayout

Track pending font faces per shape, force-relayout only affected
texts, defer stale-selrect sync until the first tiles paint, and
batch request-render after attr updates. Tolerate missing font-face
keys so callers that omit them do not blow up.
2026-09-04 12:24:37 +02:00
Alejandro Alonso 7f46c8c80e Merge remote-tracking branch 'origin/staging' into develop 2026-09-04 11:00:56 +02:00
Elena Torró fdcc4666e3 🐛 Fix multiple shapes export on files rendered with webgl (#11506) 2026-09-04 11:00:35 +02:00
Eva Marco 064f200774 🐛 Fix crash pressing Ctrl+D with no shape selected (#11491)
AI-assisted-by: claude-sonnet-5
2026-09-04 10:21:31 +02:00
Eva Marco 214dbd0c8b 🐛 Stop opening Comments section from persisting display-comments (#11492)
AI-assisted-by: claude-sonnet-5
2026-09-04 10:15:38 +02:00
Marina López f2f352d06c Add props to invitations and organizations events (#11508) 2026-09-04 10:13:25 +02:00
David Barragán Merino 034707a9d7 Enable parallel integration tests on staging branch
Remove the temporary serial exception introduced alongside sharding.
Sharded runs on `develop` complete in ~13 min against ~52 min for the
serial runs on `staging`, and the gap keeps widening as the suite
grows.

Also restore the JSON report as a 30-day artifact. It was dropped when
the suite moved to sharded blob reporters, which left flakiness rates
only obtainable by scraping job logs.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-03 18:10:38 +02:00
Elena Torró eb8d3a2fef 🐛 Fix paragraph selection (#11493) 2026-09-03 16:17:38 +02:00
Alejandro Alonso fb22c1547c 🐛 Skip component sync for derived WASM text layout commits (#11490)
Post-font-load selrect fixes and position-data regeneration write
sync-attrs on texts inside mains. That made watch-component-changes
treat them as edits and run touch/sync per component, freezing large
files. Mark those commits with skip-component-sync? (same idea as
translation?) so only real user edits propagate.
2026-09-03 13:43:36 +02:00
Alejandro Alonso 7b26949c76 🐛 Fix typography.cljs error from staging merge 2026-09-03 12:34:54 +02:00
Alejandro Alonso 37f1d3f0fd Merge remote-tracking branch 'origin/staging' into develop 2026-09-03 12:24:44 +02:00
Alejandro Alonso a4becb5d1f 🐛 Fix size-limiting-stream read arity on v3 binfile import (#11468)
The FilterInputStream proxy only implemented read() and
read(byte[], int, int). Buffered reads call read(byte[]) (and
read(byte[], int) via Clojure interop), causing ArityException
while hashing storage objects and breaking v3 imports.

Implement all read overloads and extract shared byte-count logic.
2026-09-03 12:16:26 +02:00
Luis de DiosandEva Marco a19b3c8d62 🐛 Fix font preview in assets breaks the font row (#11428)
* 🐛 Fix font preview in assets breaks the font row

* 🐛 Fix font height problem also in the font dropdown

* 🐛 Fix a small bug within the changes

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-09-03 12:16:26 +02:00
Elena Torró 80dea409c6 🐛 Fix share-link viewers unable to load file fragments (#11484) 2026-09-03 12:15:35 +02:00
María Valderrama 7c762d8a98 🐛 Fix recommended plan (#11488) 2026-09-03 11:59:20 +02:00
Pablo Alba 15dff4a9e1 💄 Fix sales email (#11478) 2026-09-03 08:46:52 +02:00
Eva Marco c6a32a2f5a 🐛 Fix list on registration toast notification (#11479) 2026-09-02 17:19:37 +02:00
David Barragán Merino f633d82f51 ♻️ Switch penpot images to sha-<commit> tagging
Replaces the content-hash build key (bundle_version + docker/images tree hash) used to tag and dedupe the backend/frontend/exporter/storybook/mcp image set with sha-<commit>, matching the scheme already used by admin-console, licenses-manager and payments across the org. The check→build→promote pattern with the S3 marker is unchanged; only the key used for the marker, the immutable tag and the local bundle cache filename moves from the composite build key to the git commit sha (the bundle cache now keys on bundle_version alone, which is what it actually caches). devenv is intentionally left out of this pass, it has no versioned tagging today.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-02 17:14:31 +02:00
David Barragán Merino f22abc9861 ♻️ Switch penpot images to sha-<commit> tagging
Replaces the content-hash build key (bundle_version + docker/images tree hash) used to tag and dedupe the backend/frontend/exporter/storybook/mcp image set with sha-<commit>, matching the scheme already used by admin-console, licenses-manager and payments across the org. The check→build→promote pattern with the S3 marker is unchanged; only the key used for the marker, the immutable tag and the local bundle cache filename moves from the composite build key to the git commit sha (the bundle cache now keys on bundle_version alone, which is what it actually caches). devenv is intentionally left out of this pass, it has no versioned tagging today.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-02 16:37:20 +02:00
Alejandro Alonso c5897bc50a Paint plain text directly onto Current (#11355)
* ♻️ Share text layout paragraphs across modifier clones

Store Skia paragraphs in Rc so TextContentLayout::clone keeps the
cached layout for rotate/pan modifiers. Add layout.clear() and treat
needs_update as paragraphs-empty only.

*  Reuse cached Skia paragraphs when painting text

Add try_paint_from_layout_cache to paint from TextContent.layout when
versions match, skipping ParagraphBuilder rebuild and layout on each
frame. Wire into the layered text path for plain fills without strokes
or effects.

*  Paint plain text directly onto Current

Extend can_render_directly for stroke-free text and skip the empty
save_layer in draw_text when no stroke-group opacity is set. Plain text
paints into Current without the Fills/Strokes blit.
2026-09-02 16:17:06 +02:00
Eva Marco cb80ea98ba 🎉 Add a new favicon for localhost enviroment (#11459) 2026-09-02 16:02:23 +02:00
0xTHAC0 b2fb63988f 🐛 Fix font-size dropdown clipping multi-digit values in Firefox (#11162)
The .custom-select-dropdown used width: fit-content which, in Firefox,
is capped to the containing block width (60 px for the font-size input).
This caused two- and three-digit font-size values (e.g. 48, 120, 1000)
to be visually clipped in the dropdown list.

Switching to width: max-content lets the dropdown expand to fit its
content regardless of the containing block width, matching the behaviour
Chrome already exhibited with fit-content.

Fixes #11008
2026-09-02 15:54:33 +02:00
Elena Torró 70b443a716 Render multiple-object SVG exports with render-wasm (#11475) 2026-09-02 14:45:32 +02:00
andrés gonzález 88d715938f 📚 Add migration guide page (#11454)
Point First Steps at the enterprise migration PDF with a short
summary, without duplicating the Community post.
2026-09-02 14:06:06 +02:00
Eva Marco 7620386215 🐛 Fix font family preview sample (#11473)
* 🐛 Fix font family preview sample

* 🐛 Fix font line height inside font selector
2026-09-02 13:55:38 +02:00
andrés gonzález b6b1a47a7b 📚 Update MCP Quick demo video (#11453)
Replace the outdated Quick demo embed on the MCP docs with the
new recording.
2026-09-02 13:54:01 +02:00
Alejandro Alonso b92112da54 🐛 Export multiple solid fills to WASM SVG (#11467)
Shapes and text with stacked solid fills were exported without fill
attributes because merge_fills builds a Skia shader that SkSVGDevice
does not serialize. Draw each fill layer separately for SVG export,
bottom to top, matching the classic exporter.

Closes #11466
2026-09-02 13:40:05 +02:00
Elena Torró 03e6f119e5 ♻️ Clean unnecessary methods (#11472)
* ♻️ Remove duplicated/unused set-children code

* ♻️ Remove unused methods
2026-09-02 12:58:52 +02:00
Alejandro Alonso b9ddfc1596 Batch WASM shape upload to speed up page switches (#11443)
Upload structural shape attrs (base, children, blur, shadows,
flex, layout-item) via multi-shape `_set_shapes_batch` FFI in
chunks of 512, then apply host attrs with use-shape selection.
2026-09-02 10:32:52 +02:00
Alejandro Alonso b46ed37141 🐛 Fix size-limiting-stream read arity on v3 binfile import (#11468)
The FilterInputStream proxy only implemented read() and
read(byte[], int, int). Buffered reads call read(byte[]) (and
read(byte[], int) via Clojure interop), causing ArityException
while hashing storage objects and breaking v3 imports.

Implement all read overloads and extract shared byte-count logic.
2026-09-02 10:28:16 +02:00
Alejandro Alonso d1a54134bb Add text and @font-face to WASM SVG export (#11442)
*  Add text and @font-face to WASM SVG export

Emit selectable <text> elements and inject @font-face rules that
reference the font URLs registered at load time. Text fill bypasses
save_layer wrappers that SkSVGDevice drops. Text strokes are deferred
to a follow-up PR.

* 🐛 Strip trailing commas from Skia SVG text positions

Skia's SVG backend appends a trailing comma to per-glyph x/y lists.
Firefox rejects the malformed attribute and drops glyph positioning,
making exported text vanish or mis-render.
2026-09-01 17:06:50 +02:00
Luis de DiosandEva Marco 99378dc02d 🐛 Fix font preview in assets breaks the font row (#11428)
* 🐛 Fix font preview in assets breaks the font row

* 🐛 Fix font height problem also in the font dropdown

* 🐛 Fix a small bug within the changes

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-09-01 15:48:09 +02:00
Eva Marco c7b3a0849f 🐛 Fix colopicker image checkbox (#11456) 2026-09-01 13:27:09 +02:00
Elena Torró b458dc764e 🔧 Set wasm export by flag instead of team feature (#11449) 2026-09-01 12:47:05 +02:00
Andrey Antukh f5aad7b1ae Merge remote-tracking branch 'origin/staging' into develop 2026-09-01 12:45:17 +02:00
Andrey Antukh 41ec134ea2 📚 Update changelog 2026-09-01 12:02:57 +02:00
Andrey Antukh 9051b87f0e 📎 Update the update-changelog skill 2026-09-01 12:02:19 +02:00
Andrey Antukh a1079cf788 ⬆️ Update JVM, pnpm and node dependencies (#11404)
* ⬆️ 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
2026-09-01 12:01:35 +02:00
Eva Marco 5c4b35e350 🐛 Fix top spacing on left sidebar (#11446) 2026-09-01 10:56:53 +02:00
Andrey Antukh 23869f3b1b 🐛 Gate MCP REPL server behind isDevEnv check (#11282)
* 🐛 Gate MCP REPL server behind isDevEnv check

The ReplServer was starting unconditionally on every MCP server
instance, regardless of configuration. This exposed an
unauthenticated POST /execute endpoint that forwarded arbitrary
JavaScript to connected Penpot plugins.

Gate ReplServer creation, startup, and shutdown behind isDevEnv(),
consistent with how CljsReplTool and other dev tools are already
protected. Log an info message when the REPL server is disabled.

Consolidate the dev-env check into a single static isDevEnvEnabled()
method that isDevEnv() delegates to, avoiding duplicate logic. Add
PluginBridge.close() for proper WebSocket server cleanup on shutdown.

Add regression tests that construct PenpotMcpServer and verify
hasReplServer() returns the correct value based on the dev-env flag.

AI-assisted-by: mimo-v2.5-pro

*  Add PENPOT_MCP_REPL_ENABLE env var for explicit REPL control

Allow the REPL server to be enabled independently of the devenv
setting via a new PENPOT_MCP_REPL_ENABLE environment variable.
When set to "true", the REPL server starts regardless of
PENPOT_MCP_DEVENV; when set to any other value, it is disabled.
When unset, the previous isDevEnv fallback applies.

Addresses review feedback on PR #11282.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:50:05 +02:00
Andrey Antukh 810f8ef671 🐛 Cascade profile deletion in objects-gc task (#11395)
The objects-gc task was performing a hard delete on profiles
without cascading the soft-delete to owned teams, projects, and files.
This left orphaned objects that were never cleaned up.

Now the task invokes delete-object before the hard delete, ensuring
all owned resources are properly marked for deletion and cleaned up
in subsequent GC iterations.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:49:18 +02:00
Andrey Antukh 6d9f411fab 🐛 Enforce share-link comment permissions and page scope (#11371)
Fix two security vulnerabilities in comment RPCs when accessed
via share-links:

- GHSA-4p97-v4wg-jxfx: Share-link holders with who-comment=team
  could bypass the restriction and comment. The check-comment-permissions!
  function treated can-read as sufficient, but share-links always set
  can-read=true.

- GHSA-fwm4-hm9f-rmcp: Comment query RPCs returned threads from all
  pages, ignoring the share-link's :pages restriction.

Changes:
- files.clj: Differentiate :membership vs :share-link in
  check-comment-permissions!. For share-links, require
  has-comment-permissions? only (who-comment=all).
- comments.clj: Filter threads by (:pages perms) for share-link
  access in get-comment-threads, get-comment-thread, and get-comments.

Closes #11370

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:55 +02:00
Andrey Antukh 29376d033b 🐛 Include tenant in library summary cache keys (#11408)
The redis cache keys for the file library summary were built as
penpot.library-summary.<file-id>, missing the tenant component used
by the rest of the shared-infrastructure keys (rlimit, msgbus,
worker queues).

Build the key with a new file-summary-cache-key function that appends
the configured tenant, following the penpot.library-summary.<tenant>.<file-id>
pattern. Stale keys in the old format expire on their own with the
existing 30-day TTL.

Closes #11407

AI-assisted-by: glm-5.3-flash
2026-09-01 08:48:34 +02:00
Andrey Antukh 45f0153e8f 🐛 Prevent admin from removing team owner in delete-team-member (#11368)
Add owner protection to ::delete-team-member RPC command.
Previously, a team admin could remove the team owner, permanently
locking them out of their team and all resources.

Changes:
- Fetch target member data before deletion
- Validate member exists (return :not-found if not)
- Reject removal if target is owner and caller is not owner

This mirrors the existing protection in update-team-member-role.

Closes #11367

AI-assisted-by: qwen3.7-plus
2026-09-01 08:48:09 +02:00
Andrey Antukh 326d83e780 🐛 Block IPv6 transition addresses in SSRF guard (#11320)
* 🐛 Block IPv6 transition addresses in SSRF guard

The outbound HTTP SSRF blocklist did not classify NAT64
(64:ff9b::/96), 6to4 (2002::/16) or Teredo (2001:0000::/32)
addresses, whose embedded IPv4 target is invisible to the JVM
InetAddress predicates, so URLs resolving to them could reach cloud
metadata, loopback or RFC 1918 hosts from webhook delivery and media
import.

Transition ranges are now rejected outright and any embedded IPv4 is
re-checked against the full blocklist, including operator-supplied
extra blocked CIDRs.

Closes #11319

* ♻️ Remove dead embedded-IPv4 re-check from SSRF guard

The previous commit added a recursive re-check of the IPv4 embedded in
NAT64/6to4/Teredo addresses, but the `or` in `blocked-address?`
short-circuits on the truthy keyword returned by `transition-prefix`,
so the embedded-IPv4 branch was unreachable. The transition ranges are
already rejected outright (fail-closed), making the re-check both
unnecessary and untested.

Remove `transition-embedded-ipv4`, simplify the IPv6 branch to a plain
prefix check, and correct the docstrings and tests to match what the
code actually does.

AI-assisted-by: glm-5.3-flash
2026-09-01 08:47:46 +02:00
Andrey Antukh 15195b3bbb 🐛 Filter share-link tokens in get-view-only-bundle response (#11286)
* 🐛 Filter share-link tokens in get-view-only-bundle response

The get-view-only-bundle RPC command returned all share-link tokens for a file, allowing an anonymous holder of a restrictive share-link to enumerate and use more permissive tokens.

When authenticating via a share-link, the response now only includes the share-link used for authentication, preventing token disclosure and scope escalation.

Implemented using TDD:
- RED: Test demonstrates vulnerability (all tokens visible)
- GREEN: Filter share-links when (:type perms) = :share-link
- Verified all existing tests still pass

Closes #11285

AI-assisted-by: qwen3.7-plus

* 🐛 Add membership-side test for share-link token visibility

Add test coverage for the allow side of the share-link token filtering:
team members and file owners should still see all share-links, while
anonymous share-link holders only see their own token.

This protects the (:type perms) = :share-link guard from accidental
regression that could break the owner's share-link management dialog.

AI-assisted-by: qwen3.7-plus
2026-09-01 08:46:59 +02:00
Andrey Antukh 3df039abc3 Add several improvements for demo profile creation mechanism (#11257)
*  Optimize demo user setup for performance tests

Use UUID-based demo emails to prevent concurrent profile collisions.\nUse fast PBKDF2 hashing for demo profiles while keeping regular user hashing unchanged.\nAdd focused coverage for hashing, email uniqueness, and the feature flag.\n\nAI-assisted-by: gpt-5.6-luna

* 🐛 Harden font upload test setup

Report upload-session errors before chunk validation.
Skip chunk uploads when the session ID is invalid.
Remove unnecessary Mockery state from the foreign-font test.

AI-assisted-by: gpt-5.6-luna

*  Add demo profile purge task

Schedule delayed deletion for demo profiles through the worker system.
Restore normal profile filtering and cover the purge handler with tests.

AI-assisted-by: gpt-5.6-luna
2026-09-01 08:46:28 +02:00
Andrey Antukh 92c2079ae2 🐛 Add configurable limits for ZIP entry count and object size in v3 import (#11022)
* 🐛 Add configurable limits for ZIP entry count and object size in v3 import

Add binfile-import-max-zip-entries (default 500,000) and
binfile-import-max-object-size (default 100 MiB) config entries.
Both are configurable via PENPOT_BINFILE_IMPORT_MAX_ZIP_ENTRIES and
PENPOT_BINFILE_IMPORT_MAX_OBJECT_SIZE env vars.

Entry count is checked before processing begins. Per-object size is
checked after each storage object content is resolved.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Enforce actual decompressed byte limits on v3 import

The previous object-size check trusted the ZIP entry header's declared
size (ZipEntry.getSize()), which a malicious zip-bomb can forge. The
check would pass, then the full decompressed payload would be read
anyway during hashing and storage persistence.

Add size-limiting-stream, a FilterInputStream wrapper that counts
actual bytes read and raises :validation :max-file-size-reached when
the configured limit is exceeded. Wire it into zip-entry-storage-content
so both the hash calculation and storage write paths are bounded by
real decompressed bytes, not declared header size.

Also wire import limits into management.clj (clone-template) and
debug.clj (import-handler + clone path) for defense-in-depth, and
add a test that exercises the object-size limit with a real storage
object in the exported ZIP.

AI-assisted-by: mimo-v2.5-pro
2026-09-01 08:44:06 +02:00
Pablo Alba 5b3a1d9360 Add check version menu entry (#11411)
*  Add check version menu entry

*  MR fixes

*  MR changes 2
2026-08-31 16:47: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
Alejandro Alonso 89a17df403 Add basic WASM SVG export path (#10891)
Introduce a minimal vector SVG exporter via render-wasm: solid fills,
native opacity/blend wrappers, frame clip-content, FFI/frontend wiring,
insta snapshots, and a visual preview gallery. Defer save_layer
workarounds (shadows, blur, masks, special strokes) to follow-up PRs.
2026-08-31 16:06:38 +02:00
Elena Torró 736b99076f 🐛 Fix shapes cut when overflowing a non-clipping board (#11423) 2026-08-31 15:50:19 +02:00
Elena Torró 66b4a99ac3 🎉 Implement export jobs to process export requests (#11296)
*  Add export job model, store and scheduler to exporter

*  Render wasm exports on pooled worker threads

*  Add export job REST API to exporter

*  Use export job API and allow cancelling wasm exports

* 🔧 Show export jobs in the internal debug panel

* 🔧 Pass flags and export job settings to the exporter container

* 📚 Document the exporter job API and its redis layout
2026-08-31 14:42:51 +02:00
Vaibhav Srivastava ac5c88be66 📚 Fix typo on docs (#11429)
Signed-off-by: Vaibhav Srivastava <vaibhavsri1712@gmail.com>
2026-08-31 14:11:43 +02:00
Dr. Dominik Jain 36cf4f39e1 Apply method Penpot.waitForLayoutUpdate, removing workarounds (#11433)
* MCP server: Update instruction manual to point to method instead of sleep
* MCP plugin: Call waitForLayoutUpdate prior to every code execution
* Composable test suite: Use waitForLayoutUpdate instead of 200ms delay in tests
2026-08-31 14:10:28 +02:00
Luis de Dios fc207a1768 🐛 Fix shapes flyout in toolbar only opens with the space key, and doesn't close when pressed again (#11331) 2026-08-31 14:00:14 +02:00
Pablo Alba 93ac6d8338 Add a new nitrate endpoint to check the airgapped flag (#11434) 2026-08-31 13:31:17 +02:00
Luis de Dios 806f947360 🐛 Fix avoid empty space at the bottom when there are many layers (#11427) 2026-08-31 12:38:20 +02:00
Luis de DiosandEva Marco bfb5cae9f4 🐛 Fix viewer login modal lacks spacing between SSO buttons and work email field (#11312)
* 🐛 Fix spacing between SSO buttons and email field

* ♻️ Update scrollbar on register page

* ♻️ Translate hardcoded string

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-08-31 12:17:57 +02:00
Andrey Antukh 292390f0eb Merge remote-tracking branch 'origin/staging' into develop 2026-08-31 11:52:26 +02:00
Luis de Dios 2ed0981e8a 🐛 Fix disabled numeric input with token applied is broken (#11324) 2026-08-31 11:51:42 +02:00
Luis de Dios 169e6fcbca 🐛 Fix comments not close main menu (#11347) 2026-08-31 11:50:59 +02:00
Andrey Antukh 995a5460e5 📎 Update agents and opencode on devenv 2026-08-28 21:20:17 +02:00
Andrey Antukh 4c9aeab65e 📚 Add issue/PR CLI auto-trigger to agent guide
When an issue or PR is mentioned in conversation, fetch details via gh
CLI or scripts/gh.py instead of WebFetch. Use gh issue view for issues,
gh pr view for single PRs, and scripts/gh.py prs for multiple PRs.

AI-assisted-by: glm-5.3-flash
2026-08-28 14:55:30 +00:00
María Valderrama c01306734a 🐛 Fix default team and UI labels to use Personal Projects (#11418)
* 🐛 Fix default team and UI labels to use Personal Projects

* 📎 Code review
2026-08-28 14:04:56 +02:00
Pablo Alba 10e40e62a2 Add airgapped check to nitrate (#11421) 2026-08-28 13:58:18 +02:00
Marina López f06d1df5be 🐛 Align management API with payments service (#11414) 2026-08-28 11:54:02 +02:00
Andrey Antukh 2ce202c7d8 📎 Add changelog entry for 2.19.0 (unreleased) 2026-08-28 11:35:49 +02:00
Andrey Antukh 0a14a1f3db Merge remote-tracking branch 'origin/staging' into develop 2026-08-28 11:22:52 +02:00
María Valderrama eb0c51ed74 📚 Rename personal space label from Your Penpot to Personal Projects (#11399) 2026-08-28 10:41:25 +02:00
Pablo Alba 18ebf5a92d 📚 Add highlights file (#11406) 2026-08-28 10:40:25 +02:00
María Valderrama cdfdbf9adc 🐛 Fix labels for teams navigation and personal files section (#11398) 2026-08-28 09:49:13 +02:00
38004e6bb2 Add the graph subsystem and graph visualization console to the backend (#11101)
* 🎉 Basic lbug connection for ingestion

*  Add Penpot-to-Ladybug graph ingest vertical slice

*  Use embedded Ladybug Java API instead of CLI

* ♻️ Share Ladybug connection across ingest and stats

*  Validate graph ingest projections with Malli

*  Project nested shapes recursively into the graph

*  Load graph ingest via Ladybug COPY bulk import

* 🐛 Fix graph COPY ingest for multiline text names

*  Add Ladybug graph export to debug UI

*  Add debug graph console for in-memory Cypher queries

*  Add live file-change feed to debug graph console

*  Incrementally sync debug graph from Penpot file changes

*  Handle mov-objects in debug graph sync

* 🐛 Fix batch delete sync and keep graph console feed alive

* ♻️ Derive graph node schema from Malli registry

*  Add G6 graph view to debug graph console

POC per work/g6/plan.md. New /dbg/actions/graph-data exports the in-memory Ladybug session as plain JSON (per-table node queries + multi-table IsChildOf match, row cap 100k with truncation flag). Console page renders it with AntV G6 v5 (jsDelivr CDN, antv-dagre BT layout, color+glyph per node table, validated palette) and refetches debounced on live :file-change messages.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Fix list-column CSV ingest and serialize graph session access

COPY failed on any file with container shapes: list-typed DDL columns (shapes UUID[], points STRING[], strokes JSON[], ...) were JSON-encoded in staging CSVs, which Ladybug's list parser rejects. Write Kuzu list literals instead, typed per column. Also: value->clj no longer crashes on LIST/STRUCT values (binding lacks value_get_value support; fall back to string), and the debug session Connection is now guarded by a per-session lock — it was shared unsynchronized between the msgbus sync loop and HTTP query/export handlers, and one lost DETACH DELETE was observed under concurrent refetch load.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Split graph console in two columns; add file tree and fullscreen

Graph view moves to its own sticky right column (overrides .widget max-width). New /dbg/actions/graph-files endpoint lists teams -> projects -> files for the profile; the console renders it as a collapsible tree where clicking a file loads it. Maximize button fullscreens the graph panel and resizes G6 on fullscreenchange.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* ♻️ Replace fullscreen with in-page expand for graph view

Fullscreen API took over the whole output and broke window-manager splits (and is denied in some environments). The Expand button now toggles a fixed-position overlay covering the page while keeping browser chrome; Esc restores. Column positioning moved from inline style to the stylesheet so the expanded class can override it.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Fold containers as collapsible combos in graph view

Non-empty containers (Page, Frame, Group, Boolean, SVGRaw) render as nested G6 rect combos holding their own node plus direct children; Document stays a plain node. Double-click folds/expands (collapse-expand behavior); collapsed combos show a member count and re-route child edges. Fold state is read back from getComboData and re-marked on every refetch, so it survives live redraws. Layout gains sortByCombo to keep same-rank nodes grouped by box.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Fix graph view freeze on large files; add fold toggle and root rule

Root cause of the tab freeze on ~1700-node files was G6's default entrance animation: measured 1700 nodes at >2 min animated vs 1.5 s with animation: false. Secondary cost was antv-dagre (~7 s at that size); since IsChildOf is a tree, an O(n) tidy layout (depth = rank, post-order leaf slots, parents centered) computed client-side replaces it and renders the same file in ~1.4 s. A guard skips auto-render above 4000 nodes with an explicit Render-anyway button, so opening the console with a huge session loaded stays responsive.

Folding is now switchable ('fold containers' checkbox, persisted in localStorage) and generalized: any node with children folds except the IsChildOf root of the loaded graph, so Documents (and later Projects/Teams) fold automatically once they gain a parent node.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add layout dropdown to graph view

Adds a layout <select> next to the fold toggle, populated from the LAYOUTS map in the template: 'tree' (the O(n) preset layout, default) plus 13 G6 layouts (antv-dagre, dagre, circular, concentric, radial, grid, force, d3-force, force-atlas2, fruchterman, mds, combo-combined, random), all smoke-tested against combo data on this UMD build. Layout and fold toggle are independent; switching layouts recreates the graph instance (cheap with animation off); both choices persist in localStorage. antv-dagre stays available for when non-tree edges arrive.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add query-result subgraph, ws auto-reconnect, adaptive animation

The Cypher result pane now offers 'Show result in graph view': any UUID found in any result cell selects the matching nodes in the cached export and the view renders the induced subgraph (edges kept when both endpoints match); 'Show full graph' resets. No graph reconstruction from the query result is needed.

The notifications websocket reconnects automatically (3 s retry) and resubscribes + refetches on reopen, so backend restarts no longer permanently kill the live feed; a lost session now reports 'no graph session (backend restarted?) - reload a file' instead of a bare 404.

Animation is size-adaptive: graphs (or filtered subgraphs) up to 100 nodes render animated for didactics, larger ones stay animation-free; crossing the threshold recreates the instance like a layout switch.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add graph toolbar, animate toggle, filter columns, repaint skip

Graph view gains an on-canvas G6 toolbar (auto-fit, expand, restore - the fullscreen icons drive the existing in-page expand), an 'animate' checkbox that disables animation unconditionally when off (persisted, adaptive <=100-node rule applies only when on), and a ResizeObserver on the canvas so the panel follows window/flex resizes without touching the user's viewport. Preset tree positions are now only injected for the built-in tree layout, removing the tree-then-layout flash on animated re-renders under G6 layouts. Refetches skip the repaint when the display projection (nodes, edges, truncated) is byte-identical, so attribute-only change bursts no longer repaint.

Console: default query returns s/t name+label over all edges plus filter_src_id/filter_tgt_id columns; filter_* columns are hidden from the results table (client and server render) but still feed the 'Show result in graph view' id harvest, keeping the table legible while the graph filter stays available. The query text persists in localStorage across page reloads (restored only over the default, never over a server-rendered query). Legend shows colored Unicode glyphs matching node shapes instead of squares with textual annotations. Load/Unload buttons share one row (HTML5 form attribute), and the loaded file name links to the Penpot workspace via the legacy /#/workspace/<project-id>/<file-id> route resolved client-side from the files-tree payload.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Fix runaway graph panel growth and blank canvas; drop Expand button

Root cause of 'graph flashes on load then disappears' plus unbounded horizontal growth of the graph panel: fieldsets default to min-inline-size: min-content, so #graph-view-panel sized to its content, and the new ResizeObserver->setSize path closed a feedback loop (setSize -> slightly wider G6 canvas -> wider fieldset -> wider .dashboard flex column -> observer fires) that grew the page ~10px per frame and wiped the painted canvas on every step. Fix severs the feedback path: #graph-view-panel gets min-inline-size: 0, #graph-canvas gets overflow: hidden, and the page section gets flex: 1 1 0 with min-width: 0 so column widths are viewport-driven, never content-driven. This also fixes the original narrow-window scrollbars defect for real. The observer stays (guarded by a current-size comparison) because G6's autoResize is inert on this UMD build (verified: window resizes left the canvas size untouched); the inert autoResize flag is dropped. Legend items now join with spaces so the nowrap spans can wrap between entries.

Also removes the header Expand button - the toolbar's expand/exit icons cover it, Esc still restores.

Verified against the running devenv with a logged-in profile and variants_simple loaded: graph renders and persists, widths stable over multiple seconds at 1400px and 1000px viewports with no horizontal overflow, canvas follows both window shrink and grow, toolbar expand gives a full-page canvas and Esc restores.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Make the default query self-explanatory; link the Cypher docs

The default query is now multi-line with // comments that explain the filter_* column convention in place (Kuzu accepts comments and blank lines mid-statement; verified against an in-memory database through the console query path). The query fieldset is retitled 'LadybugDB Cypher' with the Cypher word linking to https://docs.ladybugdb.com/cypher/.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 📎 Fix linter issues

*  Add Component nodes and IsInstanceOf edges

* 🐛 Fix memory leak

*  Style Component nodes and IsInstanceOf edges in graph console

Slice-3 export sends edges with a rel field. Derive tree ranking, combo derivation and fold-ability from IsChildOf only; draw other rels as overlay edges with per-rel styles (EDGE_STYLES: IsInstanceOf violet dashed, matching the new Component diamond in NODE_STYLES). Legend now lists only displayed node tables and rels, re-rendered per redraw; help text trimmed to essentials.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add graph diff marks with step fade to graph console

Each display-changing refetch is a step: added nodes/edges get a green halo, removed ones stay as ghosts with a dashed crimson halo (nodes, fading opacity) or thicker crimson stroke (edges), re-entering layout and combos through their ghost IsChildOf edges. Marks fade linearly and drop after N steps; N is the new "fade" number input (localStorage, 0 = off). Dash + fade carry the added/removed distinction under red-green CVD (#40c057/#c2255c, deutan dE 17.4); diff is vs the previous display step, not arbitrary revisions.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Reserve chroma for changes in graph console diff mode

Monochrome entity scheme: all node tables share one slate hue, lightness separates within-glyph siblings (validated, worst pair dE 17.5), SVGRaw becomes the hollow hexagon, both rels go grey with dash as the only separator. Diff marks now own all color: thick green/crimson stroke ring (dashed for removals) plus a larger, subtler halo; the legend gains +/- entries while marks are live. Two additions to guide the eye: a brief DOM-overlay pulse on age-0 elements (independent of the G6 animation gate) and a "fold unchanged" toggle that collapses every combo not on an ancestor path of a changed element.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Expand folded combos that gain changed elements

setData merges datum props by id on a live G6 instance, so omitting style.collapsed retained a previous true: with "fold unchanged" on, a change inside a folded combo pulsed but never expanded it. Write the boolean explicitly both ways.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Label edge rels with compact unicode symbols

Dash variants alone cannot carry the growing rel roster: EDGE_STYLES entries gain a sym rendered as a small mid-edge label with a white backing (IsInstanceOf = "∈"; IsChildOf stays unlabeled as the background structure), and the legend shows the symbol. Convention from the abacus viewer EDGE_SYM dict.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add node inspector panel to graph console

Clicking a node fetches its full attribute row (MATCH (n:`Table` {id: uuid(...)}) RETURN n.*) through the query endpoint and renders non-null attrs into a panel under the canvas (count of empty attrs noted). Panel over tooltip: projected tables carry ~80 columns, and the panel persists for reading without obstructing the graph. Table/id are validated before Cypher interpolation; the listener is re-attached on every instance recreation.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Graph console QoL round

"Show result in graph view" moves into an actions bar above the results table; results scroll inside a 45vh container (client and server render paths); the Loaded-session fieldset gains a live "Graph size" line that stays fresh through skipped repaints; IsInstanceOf mid-edge label becomes the spelled-out rel name (∈ read as membership, not derivation) with the legend falling back to the dash-arrow for long syms.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Sync Component library changes into the Ladybug graph

* 💄 Polish graph console session panel and edge labels

Loaded-session fieldset: graph size gains a resident-memory estimate (fit to graph_sizes.md: ~1.1 MiB floor + ~5.4 KiB/node) with per-table counts on hover, replacing the load-time Projection stats; loaded-at compacts to local HH:MM with the full instant on hover. Edge rel labels drop to 7 px and lose the dashed stroke — the text label alone carries rel identity.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add PNG export and hover tooltips to graph console

Toolbar gains an export item: graph.toDataURL({mode: "overall"}) downloads the whole laid-out graph as graph-<revn>.png — page-chrome-free captures, also the fast path for agents debugging the console. A hover tooltip (table, label, id) backs the reduced/absent labels on dense layouts.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Prune graph console layout roster and tune overlap

Remove grid, random, force, fruchterman, force-atlas2 (nothing over the kept set) and mds (stress layout degenerates to spokes on tree distances, no collision term to tune). Parameterize the keepers against node overlap — concentric/radial get preventOverlap+nodeSize, d3-force a collide radius — and shrink node labels to 7 px on those layouts (DENSE_LABEL_LAYOUTS), verified against variants_simple (72 nodes).

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Highlight clicked node neighborhood in graph console

click-select behavior with degree 1: the clicked element keeps a black ring, direct neighbors stay full-strength, everything else dims to 0.2 opacity (inactive state); clicking empty canvas clears. Works on edges too (selects both endpoints) and composes with the node inspector on the same click.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Add overview mode: fold containers at or beyond a depth

"fold >= depth" number input (root = 0, empty = off, localStorage): every combo whose container sits at that IsChildOf depth or deeper collapses, giving a top-of-file overview (e.g. 2 folds the containers hanging from a Page). Composes with fold-unchanged — depth folds first, changed ancestor paths are then drilled open. Derived fold state overrides manual folds while active.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Restore fold-containers as the combo master gate

Since fold-unchanged and depth folding arrived, withCombos ORed them in, so unchecking "fold containers" could no longer remove the combo boxes. The checkbox is the gate again; the derived fold rules are dormant without it.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Console UI polish round

Merge the load form and files tree into one "Load graph from Penpot" box (tree first, uuid + Load/Unload in a row); Loaded session carries HH:MM in its legend; the Live changes box stays hidden until the first change arrives; query fieldset reads "Query graph (LadybugDB Cypher)" with the link covering both terms. Drop the hover tooltips (distracting, useless zoomed out) and the resident-size estimate (per-table counts stay on hover); every toggle gets a "When set/checked ..." title. Depth fold: 0 now expands every container (no more hunting for max depth). Node inspector: two-column flow, structured or long values folded behind the file-tree disclosure triangle.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Report actual graph memory from the buffer manager

graph-data gains bm-bytes (CALL bm_info() -> [mem_limit mem_usage], nil-safe, under the session lock); the session panel shows it as MiB behind the node/edge counts — real resident memory replacing the removed estimate.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Console control-bar and session-panel rework

Left column narrowed 440->330 px (uuid input flexes). Control bar reordered: layout first, then animate and fade (narrow inputs), then the fold set; "fold containers" renamed "foldable containers" (on = foldable, not folded). Load becomes Reload once a session exists (same operation as the removed Full-reload button — load-session! on the current id; tooltip explains the fallback role) with Unload beside it. Session panel: revisions on one line ("ingested at N · graph now M", hover explains the difference), duplicate uuid after the file name dropped. Tried and rejected: fishbone (no positions on graph data) and compact-box (G6 tree layouts walk parent->child, IsChildOf points child->parent).

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Legend entries toggle node-table visibility

Clicking a legend entry hides/shows that table across the view (struck-through while hidden, kept listed for re-enabling; pure client-side id filter through filteredGraphData, edges drop with their endpoints, ghosts respect it). Also: setting fold >= depth above 0 now switches foldable containers on — a positive depth was silently inert without combos.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

*  Enable the edge-bundling plugin

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 Session breadcrumb, changelog colors, spacing

File line becomes team › project › file (clickable) with the resident-memory figure beside it (moved up from the graph-size line; breadcrumb resolves from the files-tree payload, so files outside the profiles teams show plain). add-obj/del-obj in Live changes wear the canvas diff colors. Paragraph margins tightened above Feed; left column 330→350 px.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Guard renders against heavy graphs; add ?safe escape hatch

A heavy file could freeze the tab on load-and-render despite the animation gate: the render guard counted nodes only, and the edge-bundling plugin is iteration-heavy in edges. Guard now also trips on edges (8000), edge bundling only activates at <= 300 edges, and /dbg/graph?safe disables auto-render entirely (counts + "Render anyway"), so a page that hung can always be re-entered with the session intact.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🔥 Remove the edge-bundling plugin

Bundled edges render unsmooth and ugly on this build; the gating constant goes with it.

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 💄 One row per operation in the Live changes table

Columns revn | op | id: the revn repeats across a batch, the op wears the canvas diff colors (shape/attrs detail on hover), and the id column shows the uuid last group with the full uuid on hover, or N/A for ops without a subject id (e.g. mov-objects).

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>

* 🐛 Use app.system/system in the graph ingest helper

develop renamed app.main/system to app.system/system and dropped the
app.main require while this branch was away. Rebasing replays the old
call, so clj-kondo reports an unresolved namespace and the ns will not
load.

*  Put the graph subsystem behind a flag, off by default (#11075)

`app.graph.ladybug` imports `com.ladybugdb.*` at namespace load. Two
namespaces reach the subsystem and both required it at the top level:
`app.http.debug`, which registers the `/dbg` routes, and
`app.srepl.main`, which loads with the REPL server. Every backend built
from this branch therefore linked the Ladybug native library into the
JVM at boot, whether or not a graph was ever used.

Add a `:graph` flag to `varia`, deliberately absent from `default` so
that a released Penpot ships with the subsystem off. Both require sites
now resolve `app.graph.*` at call time, so with the flag off no
`com.ladybugdb` class is loaded. The nine `/dbg` graph routes are
registered only when the flag is on, and 404 otherwise. The `/dbg` admin
gate is untouched: the flag decides which routes exist, not who may
reach them. When the flag is on, route init requires the subsystem
eagerly, so a missing or unusable native library fails the boot rather
than the first console request.

No tracked file turns the flag on. `backend/scripts/_env` leaves it out,
so a devenv boots with the subsystem off exactly as a released build
does, and `docker/images/docker-compose.yaml`, the self-hosting
distribution, is untouched. Whoever works on the graph turns it on for
one checkout through the gitignored `backend/scripts/_env.local`, which
every backend and exporter dev script sources right after `_env`.

Verified with `-verbose:class` over a boot's namespace load plus
`ig/init-key ::routes`: 9 `com.ladybugdb` classes before this change
with no flag set, 0 after it with the flag off, 9 with `enable-graph`.

* ⬆️ Take Ladybug 0.19.1

`com.ladybugdb/lbug` moves from 0.18.0 to 0.19.1, the current release on
Maven Central. The engine fixes a SIGSEGV on an unwrapped parameter and
moves parameter coercion out of JNI, so shipping 0.18.0 would land a
native library into `develop` with a known crash already fixed upstream.

Nothing else changes. This branch has no `app.graph.arrow`, so the
top-level Arrow field-name backticking that 0.19.x retires does not
exist here and there is no workaround to remove alongside the bump.

AI-assisted-by: mixed models

* 📎 Pin the graph console's G6 bundle to an exact version

The console loaded `@antv/g6@5` from jsDelivr, a floating major range,
so the JavaScript served into the page could change without a Penpot
release. Pin it to 5.1.1, the version the range resolves to today.

Where the dependency finally belongs is an open question for review:
vendored into `backend/resources`, declared in `frontend/package.json`
if the console moves out of `/dbg`, or left on the CDN. Pinning removes
the floating-code problem without pre-empting that decision.

AI-assisted-by: mixed models

*  Add graph provenance, column naming and two transforms

A projected graph is a cache of one file at one revision, built by one
schema, and nothing in it said so. `GraphMeta` records the file, the
revision, the schema version and the producer, and is written last, so
its presence also marks the build complete and its contents say whether
a cached database is still worth opening.

- `graph/meta.clj`: the `GraphMeta` table and its writer.
- `graph/schema/contract.clj`: one place that maps a Penpot key to its
  graph column. The rule is snake_case of the key; every exception, be
  it a rename, a drop or a type override, is recorded there with its
  reason, so a divergence is a diff to review rather than a silent
  rename.
- `graph/project/document.clj`: `page-id` and the inherited
  `component-id` are written during the tree walk, which already knows
  both, rather than by a post-ingest statement. `graph/sync.clj` does
  the same on the incremental path, so a live-synced graph matches a
  rebuild.
- `graph/project/transforms.clj`: a registry, so adding a derived-link
  pass is one entry. Adds `RefersTo` (from `shape-ref`) and
  `FillsSwapSlot` (from `swap-slot-*` entries in `touched`, then
  stripped as `ctk/normal-touched-groups` does).
- `graph/debug.clj`, `graph/stats.clj`: enumerate relationship tables
  from the catalog instead of naming them, so the console's graph view
  and the ingest counts pick up new edge types without being told.
- `graph/debug.clj`, `http/debug.clj`: `graph-export` gains
  `source=session`, which snapshots the live in-memory console graph
  through EXPORT/IMPORT DATABASE. Live sync moves that graph away from a
  fresh projection, and taking it away to query elsewhere is the point
  of asking for it.

AI-assisted-by: mixed models

* 🐛 Write graph values Ladybug's CSV reader cannot carry through Cypher

Three parity failures against beadpot's suite, all one cause: the bulk loader
put compound and multi-line values into CSV, where Ladybug parses a field's
*contents* as a literal with no escape mechanism at all. Verified against
0.18: a comma inside a list element ends the element, quotes are kept as part
of the value rather than delimiting it, and the parallel reader rejects
quoted newlines outright.

So a value now goes through CSV only if it cannot be misread there — UUIDs,
numbers, booleans, single-line strings, and lists of those. Everything else
(MAP, STRUCT, STRING[]/JSON[], any string containing a newline) is written
after the COPY by one Cypher statement per row, where `app.graph.ladybug`
escapes properly. Parquet removes the distinction entirely and is still the
right destination (masterplan P0 T1); this is what CSV can honestly do.

Consequences beyond the encoding:

- `touched` entries reached the graph as `:swap-slot-…`, keywords stringified
  with their colon, so `LinkSwapSlots` matched nothing. Keywords now render
  through `name`.
- Shape names lost their newlines to a flattening step that existed only to
  keep the CSV writer happy. They are preserved.
- `applied_tokens` keys are rendered camelCase, the form Penpot's own JSON
  encoder produces and the one beadpot's `AppliedTokenKey` holds — a MAP
  column's keys are values, not schema, so they are not snake_cased.
- `link-component-instances!` keys on `component-file`, not `component-id`
  alone. The projection denormalizes `component-id` down the shape tree, after
  which it no longer tells an instance head from a shape inside one, and the
  transform linked every descendant frame; `ctk/instance-of?` requires both
  keys anyway. IsInstanceOf on the variants fixture: 78 -> 60, matching
  beadpot exactly.

`app.graph.schema.nodes/format-column-value` is now the single place that
knows a column's type and its contract details, used by the bulk loader and
the incremental sync alike so the two cannot disagree about a value's shape.

*  Type graph columns as tightly as Ladybug allows

Ladybug is schema-first and strongly typed: a property key gets its type
at table-creation time and there is no widening later. That makes the
Malli to Ladybug mapping the whole of the graph's typing, and it was
leaving a lot on the table: a transform stored as `STRING`, a rect as
`JSON`, a set of feature flags as a single `STRING`. A column typed
`DOUBLE[4]` is four numbers a consumer reads as a tensor row; the same
value as JSON is text somebody has to parse and trust.

`app.graph.schema.types` now maps, in order: scalars; Penpot value types
whose layout is fixed even though Malli only sees a map or a string
(`::gmt/matrix` to `DOUBLE[6]`, `::gpt/point` to `DOUBLE[2]`,
`::grc/rect` to `DOUBLE[4]`, `::clr/hex-color` to `UINT32`); then
structure, with collections to `T[]`, `:map-of` to `MAP(k, v)`, and a
closed map of scalars to a `STRUCT`. JSON is the fallback of last
resort, for schemas that genuinely admit more than one shape.

Two defects fell out. `::sm/set` was unmapped, so `features` and
`migrations` were single strings rather than `STRING[]`, and
`::sm/one-of`, how Penpot spells a closed set of keywords, was unmapped
too, so `blend-mode`, `grow-type`, the constraints and every `layout-*`
were mistyped.

A tight column is only worth having if the writer fills it in that
shape, so `app.graph.schema.values` shapes a value for its type: a
matrix record into six doubles, a hex colour into a packed integer, a
map into a struct's fields. Both writers go through it, so the bulk load
and the incremental sync cannot disagree. What that required:

- STRUCT field names must be backticked in the DDL *and* in every
  literal, because a grid cell has a field named `column`. The catalog
  reports them bare.
- A struct literal's type is its field list, so every declared field
  must appear, and an absent one needs `cast(NULL, '<type>')`. A bare
  NULL is typed STRING and changes the struct's type.
- `STRUCT(…)[]` starts with `STRUCT(` but is a list, so the list check
  comes first.
- Nested lists cannot be rendered with `str`: Clojure's `[1 2]` is
  space-separated and Ladybug reads it as a one-element array.

Three more corrections in the same area:

- `project-attrs` used truthiness where it meant `some?`, so `opacity 0`
  and `blocked false` projected as absent.
- Set-valued columns are written sorted. A set has no order, so the
  column varied between builds of the same file, which is precisely what
  stops two builds being diffable.
- An empty collection is written as `[]` rather than skipped. A shape
  with no fills has none; NULL would say "unknown".

Renamed the `kuzu-*` helpers to `ladybug-*`: Kùzu is deprecated and
Ladybug substitutes it, so a name bearing the engine should bear this
one. The one remaining mention cites the upstream issue Ladybug
inherits.

AI-assisted-by: mixed models

*  Add the file-level graph columns and tighten the svg ones

Split out of "🐛 Declare the shape attributes stored files carry",
which is now #11125 and carries only its `common/` half. This commit is
the graph's own side of that change, and it stays on this branch.

`app.graph.schema.contract` pins `svg_viewbox` to `DOUBLE[4]` and
`svg_transform` to `DOUBLE[6]`. The shape schema types both `:map` on
purpose, because legacy files hold them as plain maps rather than as
`::grc/rect` and `::gmt/matrix` records, and a tighter *schema* would
reject those files. A tighter *column* costs nothing, since
`app.graph.schema.values/coerce` reads either form.

`app.graph.schema.nodes` declares four file-level attributes as
projection `:extra` rather than in `ctf/schema:file`: `:options`,
`:backend`, `:comment-thread-seqn`, and `:ignore-sync-until`. Declaring
them in the file schema breaks saving, measured at 185 failures, because
`app.binfile.common/update-file!` derives its UPDATE column list from a
file map's keys and the `file` table has no `backend` column, that value
being synthesized on read. An `:extra` is local to the graph and cannot
reach a write.

`app.graph.project.document` lifts `:options` out of `:data` before the
blob is dropped, so a consumer reads file-level configuration without
opening the blob.

AI-assisted-by: mixed models

*  Add the Arrow prerequisites for in-memory bulk load

lbug pulls arrow-memory-core and arrow-vector but no allocation-manager implementation, so RootAllocator cannot be constructed; arrow-memory-netty 18.2.0 matches the arrow-vector lbug already brings and pulls only netty-buffer, netty-common, jackson and slf4j-api, all of which the backend already has.

--add-opens=java.base/java.nio=ALL-UNNAMED is the second half: without it MemoryUtil's static initializer dies with an InaccessibleObjectException that surfaces as an unhelpful NoClassDefFoundError from anything touching RootAllocator. It has to be present at JVM start, hence all three places. Note app.main/restart will not pick it up — it restarts integrant inside the same JVM, so the process must be restarted.

Worth a reviewer's attention: this is a JVM-wide flag added for one subsystem. It is the standard Arrow requirement and grants nothing beyond reflective access to java.nio, but it strengthens the case for putting the whole graph subsystem behind a feature flag.

*  Bulk load through in-memory Arrow; delete the CSV loader

app.graph.arrow stages rows as Arrow VectorSchemaRoots and COPYs from them. No file is written at any point and no value is rendered as text for the engine to re-parse, so the defect class that produced three of this branch's four backend defects cannot recur.

app.graph.bulk is deleted whole. csv-representable?, defer-to-cypher?, multiline?, fixup-statements, ladybug-literal, ladybug-list-element, ladybug-list-cell and staging-dir go with it, along with the post-COPY Cypher pass that emitted one SET per row.

Measured before deciding: the fixup pass was ~77% execution, 16-22% parse and 6-7% round-trip, and prepared statements could not have recovered any of it — every fixup row carries a MAP column and Ladybug binds scalars only. So this replaces rather than optimizes. Marginal ingest 4.0 -> 1.21 ms/shape; ~25 s extrapolated at 20k shapes against the ~2 min the CSV path projected. Size unchanged.

Four engine facts the implementation rests on, each verified against 0.18.2 with a standalone probe:

- An Arrow table is not a COPY source identifier but is a MATCH-able node label.
- A MAP vector's entries child must be a non-nullable struct, and MapVector.getWriter promotes it to a sparse union, so map vectors are built from an explicit Field and filled child-first.
- Ladybug names a staged table's columns and struct fields from the Arrow field names and quotes none of them, so anything needing quotes must arrive quoted — hence cypher-property-key, not column-name, names the Arrow fields.
- createArrowRelTable cannot resolve endpoints against a UUID-keyed node table under any encoding, so edges stage as a node table and the COPY subquery joins them.

values/coerce is reused unchanged, so the Arrow and Cypher writers cannot disagree about a value's shape; nodes/column-map-key-fn is extracted so they cannot disagree about a MAP's key spelling either.

Verified with pytest --graph-origin=penpot-only unchanged at 225/38/1 and --graph-origin=penpot unchanged at 258 passed / 2 pre-existing failures, both baselines re-established against a reverted backend rather than assumed; with bp graph diff between a CSV-built and an Arrow-built graph reporting "Graphs agree"; and with an adversarial round-trip carrying a quote, a backslash, a newline, a CRLF and a tab through STRING, STRING[] elements and MAP values.

The diff was necessary, not belt-and-braces: both parity suites passed an earlier revision of this change that was writing EDN into every JSON column, because beadpot's assertions never parse those columns. It also showed Arrow correcting a CSV defect — an empty Component.path was being stored as NULL, because Ladybug's CSV reader cannot distinguish an empty field from an absent one.

*  Add a prepared-statement surface to the graph connection

`app.graph.ladybug` could only run Cypher as text. Every value the sync
path writes is therefore concatenated into the statement, and nothing can
ask the engine whether a statement is even valid without running it.

Add the four functions that close both gaps. `prepare-on-connection!`
parses and binds without executing. `execute-prepared!` binds a parameter
map and runs it. `exec-prepared-on-connection!` prepares every statement
in a batch before executing any of them, so a parse or bind failure
aborts before the first mutation. `validate-on-connection!` returns
`{:ok? :error :read-only?}` instead of raising, which is what a gate
wants.

`->param-value` is the only `Value` constructor on the write path. It is
unconditional: on lbug 0.18.2 an unwrapped parameter does not raise, it
SIGSEGVs the JVM inside `lbug_value_clone`. Parameters are scalars only,
because the JNI `Value` constructor takes no list or map, so `MAP`,
`STRUCT` and `T[]` columns stay literal-rendered and the `:else` branch
raises rather than crashing.

Two departures from the design, both closing a JNI-handle leak on the
error path: `prepare-on-connection!` closes the failed
`PreparedStatement` before raising, and `execute-prepared!` closes every
`Value` it built, including the ones built before a later parameter was
rejected.

`as-statement` accepts a bare string, so the sync builders can convert to
bound parameters one family at a time rather than in one commit.

AI-assisted-by: mixed models

* 🐛 Write the document revision to the column that exists

`set-document-revision-statement` emitted `SET d.revn`, but the column is
`revision`: the beadpot contract renames `:revn` and the DDL has followed
it since. The statement is the last one in every sync batch, so each
batch raised after its mutations had already committed, and the session's
in-memory index stayed frozen at its load-time revision.

Name the column through `nodes/cypher-property-key` rather than spelling
it, so the DDL and the statement cannot disagree again.

Found by the binder gate in the next commit, on its first run.

AI-assisted-by: mixed models

*  Gate every sync statement template through the binder

Nothing checked that the eleven Cypher templates `app.graph.sync` emits
still bind against the DDL the schema registry generates. A renamed
column, a dropped table or a reserved word emitted unquoted surfaced only
when a live session ran the statement, and by then the batch's earlier
mutations had committed.

`backend-tests.graph-binder-gate-test` opens a `:memory:` database,
creates the live schema on it, and *prepares* one instance of each
template without executing any of them. 14 tests, 51 assertions: the
eleven templates, label coverage over all twelve registered node tables,
and two assertions on the gate itself, that a `RETURN` reads as read-only
and a `SET` does not, and that an unbindable statement is reported rather
than thrown.

It was not green on HEAD: it caught `set-document-revision-statement`
writing a column that no longer exists, fixed in the previous commit. Red
on both injected templates tried.

No `:jvm-opts` change: CI's `-M:dev:test` already carries the native
access flags the engine needs.

AI-assisted-by: mixed models

* 🐛 Let the engine quote the Arrow field names it interpolates

`node-batch` named every top-level Arrow field with backticks, so that a
column whose name is a reserved word (`Page.index`, `Document.options`)
survived the DDL Ladybug generates for a staged table. The engine now
quotes those identifiers itself, and it does not collapse a doubled
backtick, so a pre-quoted name reaches the parser as ``index`` and
`createArrowTable` fails outright:

    Parser exception: mismatched input '``' expecting PRIMARY

Name the fields with `column-name`. The `COPY` projection is Cypher
rather than DDL and keeps its own backticks through
`cypher-property-key`, and STRUCT member names keep theirs too: those
come out of `LogicalType::toString()`, which the DDL builder does not
touch, so an unquoted member called `column` still fails to parse.

Measured with `probes/arrow/probe25.clj` against lbug 0.19.1: a plain
top-level reserved word loads and reads back, a pre-quoted one fails to
parse, a plain STRUCT member fails to parse, and a pre-quoted one loads
and reads back.

Also re-dates the engine facts in the `app.graph.arrow` docstring to the
version they were checked against, drops the SIGSEGV note from
`->param-value` now that `Connection.execute` rejects an unwrapped
parameter, and removes two references to the CSV loader.

AI-assisted-by: mixed models

* 📚 State what the graph schema does, not what it mirrors

The graph namespaces explained themselves by citing a separate project
whose Python pipeline reads the graphs this backend writes. A reader of
this repository does not have that project and should not need it, and a
docstring that justifies a choice by pointing elsewhere cannot be checked
here.

Every claim survives; only the framing changes. Column names and types
are Penpot's own decision, recorded with the reason for each divergence
from the snake_case default. The transform registry describes the edges
it materializes. The denormalizations in `app.graph.project.document`
are justified by the walk already holding both answers.

Three corrections fall out of the rewrite:

- `app.graph.schema.contract` claimed a test, `graph_contract_test`,
  that walks a checked-in schema manifest and fails on any divergence.
  No such test exists. The paragraph is gone.
- `app.graph.project.document` pointed at
  `app.graph.meta/projection-transforms`, which does not exist.
- `app.graph.project.transforms/registry` claimed its entries were "in
  application order" while `apply-transforms!` reduced over the literal
  vector. The three registered transforms read disjoint columns, so the
  order is not load-bearing. The docstring now says so, and the one real
  ordering constraint is stated where it applies: `link-swap-slots!`
  strips `swap-slot-*` entries from `touched`, so anything reading
  `touched` has to run before it.

`contract/pending-beadpot-columns` becomes `contract/unprojected-keys`.
It is referenced nowhere else.

AI-assisted-by: mixed models

*  Refuse a mutating query from the graph console

`debug/query-session!` ran whatever it was handed against the session
connection. A session graph is a projection of a file, rebuilt from that
file by Reload, so a mutation from the console produces a graph no
rebuild reproduces and no query result explains.

Bind the statement against the live schema first. A statement that does
not bind reports the binder's own message and executes nothing, which
also turns a misspelt table or property into an immediate error instead
of an empty result. A statement that binds runs only when the engine's
own read/write analysis calls it read-only.

The console's query box is labelled read-only. Load, Reload, Unload and
live sync are unaffected: they are separate handlers and do not go
through this path.

AI-assisted-by: mixed models

* 🐛 Keep a synced graph equal to a rebuilt one

Cold projection and incremental sync are two implementations of one
mapping and nothing checked that they agree. They did not.

`backend-tests.graph-sync-parity-test` projects a file into one
`:memory:` database, applies a change list to that database and the same
list to the file data, projects the result into a second database, and
diffs the two down to the row and the column. It found four
disagreements, each fixed here.

**Sibling order was inverted.** A container's stored `:shapes` list runs
bottom to top and `IsChildOf.position` numbers children in Penpot
z-order, so appending to the list means taking position 0 and pushing
every sibling up. Sync instead handed each new child the next free
number, so any container edited live carried its children in the
opposite order to a rebuild, and a delete left a gap where a rebuild
renumbers densely. `insert-position` and `renumber-siblings` put the two
paths on the same rule for `:add-obj`, `:mov-objects` and `:del-obj`,
including a block move and `:after-shape`.

**A moved shape kept its old parent.** `:mov-objects` moved the edge and
left the shape's own `parent_id` and `frame_id` columns pointing at the
container it came from. Both now follow, and `frame_id` follows through
the whole subtree the shape carries, as
`app.common.files.changes` does for `:mov-objects`. A top-level shape's
column holds `uuid/zero`, the page's root frame, while its edge points
at the Page.

**A container's `shapes` column went stale.** Nothing maintained it
after an add, a move or a delete. It is now rebuilt from the sibling
order on every change that touches a container.

**Pages came out backwards.** `projection-data` reversed `:pages` before
numbering them, which is right for child shapes and wrong for pages:
`:pages` is the tab order and has no second ordering to undo. `Page.index`
and the page's `IsChildOf.position` are now that order.

One defect the test does not reach, fixed on the way past:
`index-add-shape!` accepted `:component-ctx` and dropped it, so a shape
added under an instance head added in the same session inherited no
`component-id`.

AI-assisted-by: mixed models

* 🐛 Build a synced page node the way the projection does

`apply-add-page` sent the new Page node through `nodes/validate-node`,
which checks a map against the registry schema and returns it unchanged.
Every other node on both write paths goes through
`nodes/project-attrs`, which also selects the projected keys and is the
single place a column-level rule can live. A rule added there reached a
rebuilt page and not a synced one.

AI-assisted-by: mixed models

* 🐛 Let the graph view's query filter follow the graph

"Show result in graph view" froze the set of node ids the query returned
and filtered every later repaint against it. Live sync creates ids the
set has never seen, so a shape created while a filter was on could not
appear in the view at any point, and clicking "Show full graph" was the
only way to see it. A node the query would no longer match stayed.

Keep the query beside the ids and re-run it whenever the graph repaints,
which is only when the projection actually changed. A failed re-run
keeps the ids in hand and says so on the status line rather than passing
a stale view off as current.

`idsInResult` and `presentIds` are extracted from the two places that
scraped UUIDs out of a result.

Verified in the devenv: with a filter showing 108 of 276 nodes, a
`:file-change` adding a Frame published on the session's msgbus topic
took the view to 109 of 277, with the new node carrying its added mark,
and no interaction.

AI-assisted-by: mixed models

* ♻️ Rename app.graph.project to app.graph.projection

`project` is a Penpot noun: a team holds projects and a project holds
files, and the graph will carry a `Project` node table. A namespace
called `app.graph.project.document` therefore reads as "the graph of a
Penpot project" and means the opposite.

`projection` is the word the rest of the subsystem already uses for the
operation: `projection-data`, `load-projection!`, `:projection` in the
ingest report, and `app.graph.schema.projection`.

Pure rename. Both namespaces and every alias move; nothing else changes.

AI-assisted-by: mixed models

* 📎 Apply the project formatter to the graph namespaces

`cljfmt check src/ test/` is a step of the Backend workflow and these two
files did not pass it: an import block sorted the way a human reads it
rather than the way the formatter sorts it, and a `cond` in
`format-typed-value` indented one column short.

Formatter output only. No semantic change.

AI-assisted-by: mixed models

* 📚 Document graph experiment architecture

Add Serena memory coverage for the embedded Ladybug graph subsystem.\nDocument projection, incremental sync, console data flow, tests, and operational risks.\n\nAI-assisted-by: gpt-5.6-luna

---------

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-27 18:54:56 +02:00
Andrey AntukhandSumit Ridhal 1baa8bb29f 🐛 Center canvas and select layer when navigating layer search results (#10422) (#11391)
When using previous/next buttons in the layer search panel, the canvas
now centers on the matched layer and selects it in the layers panel.
Previously only the internal search cursor was advanced without any
visible canvas or selection update.

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-27 18:33:48 +02:00
Luis de Dios 11224940ab 🐛 Fix wrong selection color on inputs (#11400) 2026-08-27 18:32:22 +02:00
Andrey AntukhandLuis de Dios 43bbf756b0 🐛 Fix font change regression and activate font preview by default (#11341)
*  Materialize font-preview flag to make it permanent

* 🐛 Fix font change regression when WebGL is disabled

* 📎 Add fixes for e2e tests

* 🐛 Fix font change from legacy render

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-27 18:30:33 +02:00
María Valderrama 6bedd5ac76 🐛 Fix move organization modal (#11390)
* 🐛 Fix move organization modal

* 📎 Code review
2026-08-27 12:41:32 +02:00
Andrey Antukh 0e388442a1 Add storage object status lifecycle and verified dedup (#11345)
* ♻️ Simplify storage GC delays and add skip-delay task params

The touched GC no longer applies an extra deletion-delay when marking
storage objects as deleted. By the time a storage object is touched, its
referencing domain row has already passed its own deletion delay, and the
reference scan is the only safety check needed. Touched objects are now
marked with deleted_at = now, so the deleted GC removes them on the next
run.

For the tempfile bucket, upload chunks now set touched-at in the future
(1h, aligned with the upload-session-gc TTL) instead of relying on a
special-case deletion delay.

Task handlers now read their task props:
- storage-gc-touched accepts :skip-delay to process all touched objects
  immediately, bypassing the min-age threshold.
- objects-gc accepts :chunk-size and :skip-delay to process recently
  deleted rows without waiting for the deletion delay.

This allows running the deletion cascade immediately from the REPL via
run-task! with the skip-delay option.

AI-assisted-by: deepseek-v4-flash

*  Add storage object status lifecycle, verified dedup, and deletion retry tracking

Storage object lifecycle hardening:

- Add status column ('valid' | 'pending') as write-ahead marker for
  object creation. put-object! inserts in 'pending' state, writes blob,
  then promotes to 'valid'. Failed writes remove the pending row.
- Add :storage-pending-gc task to reclaim orphaned pending rows (e.g.
  after crash between blob write and promotion).
- Verify blob existence on every dedup hit via exists-object? (fs stat /
  s3 headObject). Missing blobs mark the row as deleted and create fresh
  object.
- Add deletion_attempts column (migration 0154) to track physical blob
  deletion attempts. Restructure gc_deleted to use chunked processing
  with per-chunk transactions (short lock duration). Failed deletions
  are deferred to tomorrow (deleted_at = NOW() + 1 day) to prevent
  infinite loops. After 7 attempts, give up and accept orphan.
- Change del-objects-in-bulk contract to return #{fail-ids} for precise
  per-id tracking (fs and s3 backends updated).
- Use tmp/tempfile for fs atomic writes with cleanup queue registration
  (crashed-JVM temp files swept ~60min later). Document ATOMIC_MOVE
  POSIX-only assumption.
- Add linear backoff to s3 exists-object? retries (100ms/200ms/300ms).
- Wrap compensating delete in put-object! catch block to prevent
  masking original error when connection is aborted.
- Fix assert messages in pending_gc.clj and gc_deleted.clj (pool
  assertion said 'expected valid storage' instead of 'db pool').
- Add pending-objects-excluded-from-gc-deleted test. Use unique path in
  put-object-write-failure-leaves-no-row test to avoid collisions.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix review comments on gc-deleted and storage

- Fix process-chunk! returning nil causing (+ acc nil) crash
- Add FOR UPDATE SKIP LOCKED to sql:get-deleted-chunk to prevent
  infinite loop when another worker holds locks
- Pass :cause to log messages in gc_deleted.clj and s3.clj
- Fix extra space in log hint string
- Remove unused ::blob-missing? reference from storage memory
- Rename test to match actual behavior (leaves pending row)
- Add test for gc-deleted giving up after max attempts

AI-assisted-by: qwen3.7-plus
2026-08-27 12:37:05 +02:00
Andrey Antukh a3feb4ef3b Merge remote-tracking branch 'origin/staging' into develop 2026-08-27 11:54:55 +02:00
Andrés MoyaandAndrey Antukh c9a2b8f12f 🔧 Add TokensStatus data type for forward compatibility (#11314)
* 🔧 Add TokensStatus data type for forward compatibility

* 📚 Add Transit round-trip tests for TokensStatus type

Add serialization tests validating TokensStatus survives Transit
encode/decode and Fressian round-trip (JVM). Clarify tokens-source
field comment as forward-compatibility placeholder.

AI-assisted-by: mimo-v2.5-pro

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-27 11:51:47 +02:00
Andrés Moya 582187dd03 🎉 Allow to validate and repair files from /dbg (#11335) 2026-08-27 11:50:14 +02:00
Andrey Antukh 46fdec5c0f 📎 Fix linter issues on frontend 2026-08-27 11:08:55 +02:00
Alonso TorresandAndrey Antukh 217284b1e1 Improve path operations and edition (#10807)
*  Improve path operations and edition

* 🐛 Fix floating-point equality issues in path editing

Replace exact equality checks with tolerance-based comparisons
in path editing functions to handle floating-point rounding
differences after transforms, rotations, or curve fitting.

Changes:
- distribute-content: Round coordinates to 0.1 precision before
  grouping to ensure coincident nodes move together
- separate-node: Use gpt/close? instead of exact equality to
  find nodes with floating-point imprecision
- collision-step: Use mth/close? for tolerance-based comparison
  to detect paste collisions correctly
- resolve-edit-fills: Add cycle detection to prevent infinite
  loops with corrupted parent chains

Made collision-step, available-offset-step, and resolve-edit-fills
public for better testability.

Added comprehensive tests for all fixes covering both exact and
floating-point coordinate scenarios.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix path editor code review findings

Fix issues identified during code review of path editor enhancements:

- Fix unused binding lint warning in distribute-content that blocked CI
- Fix collision-step floor comparison to use round instead of floor,
  correctly detecting collisions when coordinates drift slightly below
  integer boundaries
- Fix resolve-edit-fills to recurse through empty parent groups when
  searching for inherited fills in nested group hierarchies
- Fix expand-coincident-node-indices to use fuzzy comparison (gpt/close?)
  instead of exact equality, handling floating-point divergence after
  transforms or rotations
- Remove unreachable dead code in path-point* on-pointer-down handler
- Add tests for collision-step boundary cases, nested group fill
  inheritance, and coincident node alignment/flipping

AI-assisted-by: mimo-v2.5-pro

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-27 10:51:10 +02:00
David Barragán Merino 6f35348c7c 🐳 Pin docker images to 2.17
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-08-27 10:51:08 +02:00
Andrey AntukhandEva Marco 03cd3fa70f ♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity (#9958)
*  Auto-link libraries during import based on slugified name

When a Penpot file is exported without bundled libraries and then
imported into a different environment, external library links are
broken because library UUIDs differ across environments.

This feature adds a heuristic to auto-relink libraries by matching
slugified library names against shared files in the target team:

- Export: embed external library metadata (id, name, slug, used-by)
  in the manifest when libraries are not included in the export.
- Import: resolve external libraries by slugifying shared file names
  in the destination team and matching against manifest slugs.
- Single match: auto-link silently (creates file-library-rel row).
- Multiple matches: emit SSE event so the frontend shows a selection
  dialog for the user to pick the correct library.
- No match: import continues without linking (current behavior).

Backend changes:
- Extended manifest schema with optional :external-libraries field
- Added slugify-name, get-files-names, get-shared-files-for-team,
  find-shared-files-by-slug helpers in app.binfile.common
- Threaded team-id into import cfg from RPC layer
- Added resolve-external-libraries and auto-link-libraries in v3
- Emit :library-candidates SSE event for multi-match cases

Frontend changes:
- Worker captures library-candidates SSE events and forwards them
- Import dialog shows auto-link notification and multi-match
  selection UI with select dropdowns
- Added link-files-to-library! RPC helper for user selections
- Added en/es translations for new UI strings

Closes #9263

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

*  Add UI for the auto-link plumbing

* ♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity

Consolidates the auto-link libraries feature into a single coherent implementation:

- Unify boolean flags (embed-assets, include-libraries, link-later)
  into single ::bfc/export-type parameter
- Fix critical reference-integrity bug: pre-resolution no longer remaps
  :component-file refs when no link is created (multi-match / no-permission)
- compute-link-decisions as single source of truth for auto-link logic
- 80+ backend tests covering round-trip, cross-team, permissions,
  edge cases, and reference integrity

AI-assisted-by: longcat-2.0

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-08-27 10:09:52 +02:00
Andrey Antukh 17befc1db9 Merge remote-tracking branch 'origin/staging' into develop 2026-08-27 10:05:43 +02:00
Andrey Antukh b4dc8207ff Merge remote-tracking branch 'origin/main' into staging 2026-08-27 10:05:27 +02:00
Andrey Antukh 1d2c37e52c 📎 Update changelog 2026-08-27 10:00:13 +02:00
Andrey Antukh f7fd3e1cd5 📚 Update the update-changelog skill 2026-08-27 10:00:00 +02:00
Andrey Antukh 06491a2d24 🐛 Fix svg-raw child ids stored as strings in existing files (#11387)
* 🐛 Fix svg-raw child ids stored as strings in existing files

Before schema:svg-raw-attrs declared :shapes as a vector of uuid, the
JSON decoder had no type information for those child ids and left them
as plain strings on any round trip, so they got persisted as strings.
Once the schema was tightened those files fail schema validation on
load.

Add migration 0026-fix-svg-raw-shapes-uuids that coerces the :shapes
entries of svg-raw shapes back to uuid instances, in both pages and
components.

AI-assisted-by: deepseek-v4-flash

* ♻️ Drop redundant vector check in svg-raw shapes migration

:shapes is always a vector when present, so the guard is unnecessary.

AI-assisted-by: deepseek-v4-flash
2026-08-27 09:49:22 +02:00
Alejandro Alonso f7bdc9786c 🐛 Compare text numeric attrs with float tolerance (#11366)
Editor/WASM round-trips can truncate line-height strings
(e.g. 1.3333333333333333 → 1.33333). Exact string compare
treated that as a style change and detached typography tokens.
2026-08-27 09:41:50 +02:00
Andrey Antukh 88a52d1098 📎 Update changelog 2026-08-27 09:24:53 +02:00
Andrey Antukh 9657a848e3 Merge remote-tracking branch 'origin/staging' into develop 2026-08-27 08:31:56 +02:00
Luis de Dios 2bce69f927 🐛 Fix increasing value clicking and dragging in a numeric input (#11334) 2026-08-26 21:35:07 +02:00
David Barragán Merino 980ccf15fa 👷 Fix names related to concurrency policy for workflow calls 2026-08-26 20:27:22 +02:00
Andrey Antukh 87c51090b1 Merge remote-tracking branch 'origin/staging' into develop 2026-08-26 20:14:51 +02:00
Andrey Antukh 6e173a02fb 📚 Split backend testing memory and link from testing skill
Extract the backend Testing section from backend/core into a dedicated
backend/testing memory, following the pattern of common, frontend, and
exporter. Update the testing skill and root testing memory to point at
the new location, and add exporter/testing to the skill's required
reading list.

AI-assisted-by: deepseek-v4-flash
2026-08-26 18:11:40 +00:00
Andrey Antukh 4be749d45f Add minor improvements to scripts/gh.py 2026-08-26 19:19:22 +02:00
Pablo Alba e1a2d0b932 🐛 Fix nitrate sso failure message (#11214) 2026-08-26 17:24:59 +02:00
Andrey Antukh 33e39bc7ed 🔧 Fix backend format check script in CI
The backend format check was using 'check-fmt' instead of 'check-fmt:clj',
causing CI to always fail on the fmt step.

Closes #11358

AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 33f13f9bfd 🐛 Fix test formatting for get-team-invitation-token
AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 4adfa5d2f2 🐛 Require edition permissions for get-team-invitation-token
The handler previously allowed any team member (including viewers) to
generate invitation tokens. Now requires at least edition-level
permissions (can-edit, admin, or owner).

Closes #11358

AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh 7079d33ae1 🐛 Enforce ownership check on tempfile bucket access (#11270)
* 🐛 Enforce ownership check on tempfile bucket access

The upload-tempfile RPC stores profile-id with tempfile objects, but
objects-handler never verified the requester was the owner. Any
authenticated user who knew the UUID could access the tempfile.

Add ownership check: tempfile bucket now requires the request's
profile-id to match the stored profile-id. Returns 404 on mismatch
(not 403) to avoid leaking object existence.

Legacy tempfiles without stored profile-id remain accessible to any
authenticated user for backward compatibility.

Closes #11269

AI-assisted-by: qwen3.7-plus

* ♻️ Extract tempfile-bucket constant and fix docstring indentation

Extract the 'tempfile' bucket string literal into a named constant
(sto/tempfile-bucket) to prevent typos and make future bucket renames
trivial. Updated 9 occurrences across 7 files.

Also fixed minor docstring indentation inconsistency in authenticated?
function.

AI-assisted-by: qwen3.7-plus

* ♻️ Refactor process-bucket! and authenticated? helpers

Replace case with cond in process-bucket! to properly resolve
sto/tempfile-bucket var from another namespace (case does not
evaluate qualified vars at compile time).

Redefine authenticated? in terms of request-profile-id to remove
duplicated lookup logic.

Closes #11269

AI-assisted-by: mimo-v2.5-pro
2026-08-26 13:05:36 +02:00
Andrey Antukh b33213787e 🐛 Add accumulated storage byte quota for media uploads (#11038)
* 🐛 Add accumulated storage byte quota for media uploads

Add media-storage-bytes-per-team quote to prevent persistent DoS via
repeated uploads. The quota sums storage_object sizes from both
file_media_object (media + thumbnails) and team_font_variant
(otf/ttf/woff1/woff2). Default limit is 20 GiB per team, configurable
via PENPOT_QUOTES_MEDIA_STORAGE_BYTES_PER_TEAM.

The check is invoked in upload-file-media-object before processing,
looking up the team-id via file -> project -> team_id join.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Fix deduplicated storage overcounting in media-storage-bytes-per-team quote

The SQL query sql:get-media-storage-bytes-per-team used UNION ALL
across six SELECT branches that each produce a so_id reference.
When deduplication causes multiple file_media_object or
team_font_variant rows to point at the same storage_object, UNION ALL
counts that objects size once per reference — inflating "used bytes"
and causing false :max-quote-reached rejections.

Change all five UNION ALL to UNION so that duplicate so_id values are
collapsed before the JOIN storage_object / SUM(so.size).

Add a test (media-storage-bytes-quote-deduped) that creates one
storage_object referenced by two file_media_object rows and asserts
the computed usage reflects the deduplicated physical size, not 2x.

AI-assisted-by: mimo-v2.5-pro
2026-08-26 08:33:20 +02:00
Alejandro Alonso 7419bc7007 🐛 Evict multi-scale tile cache on shape edits (#11337)
those textures across zoom for progressive previews, and invalidate
by old∪new document coverage so rotate/move edits do not leave
stale fragments on zoom-out.
2026-08-26 08:25:23 +02:00
María Valderrama 5e1ced03ea 🐛 Fix missing warning when moving a team (#11357) 2026-08-26 08:08:40 +02:00
Andrey Antukh d655aa9c63 Merge remote-tracking branch 'origin/staging' into develop 2026-08-25 20:46:47 +02:00
0xTHAC0 0a3352927d 🐛 Hide register link in viewer login modal when registration is disabled (#11199)
The viewer's login-register modal always showed the Register link
regardless of the disable-registration server flag. The main login
page already gates this link on (contains? cf/flags :registration);
apply the same guard to the viewer modal to make the flag consistent
across all entry points.
2026-08-25 15:10:09 +02:00
Alejandro Alonso 57c0e81616 Present viewport before interest and clamp paint to atlas (#11313)
Present visible tiles via ViewportReady so zoom settle turns sharp
without waiting on the interest ring, and paint at atlas slot size so
DPR 2 does not rasterize 1024 only to downscale into 512 slots.
2026-08-25 12:56:07 +02:00
Andrey Antukh 81c3b3cd56 📎 Update copyright name on file header (#11346) 2026-08-25 11:55:10 +02:00
Andrey Antukh a83a5e2eff Merge remote-tracking branch 'origin/staging' into develop 2026-08-25 09:41:31 +02:00
Andrey Antukh 64a52d6b04 Merge remote-tracking branch 'origin/staging' into develop 2026-08-23 21:01:31 +02:00
Alejandro Alonso dd6b521bc7 🐛 Fix WASM text selection copy to Windows apps (#11305)
Write text/html alongside text/plain on copy/cut so Windows apps that
prefer CF_HTML do not paste the empty contenteditable newline.
2026-08-21 15:15:40 +02:00
Alejandro Alonso 8aefa2ddfd 🐛 Freeze viewport gestures during WASM page transition (#11301)
Pan/zoom via render_from_cache while the tile atlas is still empty
left a blank workspace under the page-transition blur. Ignore
set-view-box / view-interaction-start until tiles-complete, block
pointer events on the viewport SVG, and flush any deferred local
viewport sync when the overlay ends.
2026-08-21 14:19:50 +02:00
Andrey Antukh 6d4a6f6a9a Merge remote-tracking branch 'origin/staging' into develop 2026-08-21 12:13:35 +02:00
Alejandro Alonso 4cb9f951d2 🐛 Skip atlas writes during pan/zoom fast mode
During view gestures, fast mode renders tiles without shadows or
blur. Writing those tiles into the doc/tile atlas left shadowless
patches when render_from_cache overlayed them on the scaled
preview. Keep the last HQ atlas tiles until the post-gesture
full-quality render completes.
2026-08-21 11:17:16 +02:00
Alejandro Alonso 5dab689a6e 🐛 Pack tile atlas and clamp HiDPI surfaces under GPU limits
HQ tiles are 512px and the atlas stays at 4096² (64 full-size
slots). Browser zoom plus a forced ?dpr= can need more visible
tiles than that, and a framebuffer larger than the GPU allows.

Pack interest tiles into smaller atlas cells, blit at 512 then
scale, and inset Linear samples so seams do not bleed. Clamp the
canvas backing store and DPR together, wrap Skia at the real
drawingBuffer size, and wait one frame after DPR changes so CSS
client size and overlays stay aligned.
2026-08-21 11:17:16 +02:00
Alejandro Alonso 689d506788 Render eligible frame drop shadows via direct geometry path
Add a direct container-geometry path for eligible frames: inline blur
when the kernel fits the tile margin, otherwise a cached filter-surface
pass reused across tiles via DropShadowFilterCache on both the direct
and slow render_shape paths.

Move frame shadow logic into shadows.rs. Fix nested/clipped frame
shadows by deferring parent clip to composite time, apply negative
spread via inset, and allow rotated/transformed frames on the direct
path. Skip descendant extrect walks for clipped frames when only
nested drop shadows matter, and skip child silhouettes when the
container fill already covers shadow descendants.
2026-08-21 11:17:16 +02:00
David Barragán Merino ca72213cbb 👷 Change the runner's label to a more descriptive one 2026-08-20 21:58:35 +02:00
Belén Albeza f29a94058a 🐛 Fix not quitting v3 editor with Esc + Undo transactions (#11293)
* 🐛 Fix Esc key not quitting editor v3

* 🐛 Fix undo transactions being split in editor v3
2026-08-20 14:45:43 +02:00
Alejandro Alonso 2dcf1a8a0a Merge remote-tracking branch 'origin/staging' into develop 2026-08-20 09:15:03 +02:00
Elena Torró ed588d4500 Disable ReduceOpsTaskSplitting Skia flag (#11280) 2026-08-19 17:27:50 +02:00
Andrey AntukhandSumit Ridhal 60d87a6342 🐛 Fix linear gradients in SVG text exports (#11272)
* 🐛 Use gradient type instead of export type in SVG renderer

data->gradient-def was comparing the render `type` parameter (:svg,
:png, :pdf) against "linear" to decide between linearGradient and
radialGradient elements. Since the export type is never "linear",
the comparison always fell through to radialGradient, causing all
linear gradients to be exported as radial in SVG output.

Read the gradient type from the data map instead:
(get-in data ["gradient" "type"])

Closes #5972

* 🐛 Add SVG gradient export regression test

Extract SVG gradient definition generation from the renderer so it can
be tested directly. Add exporter test build wiring and cover both
linear and radial gradient output.

AI-assisted-by: gpt-5.6-luna

*  Standardize exporter testing workflow

Align exporter scripts with the frontend testing pattern. Add a
dedicated GitHub Actions workflow and document the canonical exporter
commands in Serena memories.

AI-assisted-by: gpt-5.6-luna

*  Add focused exporter test execution

Mirror frontend test-runner behavior for focused namespaces and test
vars. Support --focus, --log-level, and --help, and document the
commands.

AI-assisted-by: gpt-5.6-luna

* 🐛 Replace shell exec with execFile in exporter

Replace child_process.exec with execFile to eliminate shell
interpretation. Add hex color validation in exporter and frontend
to reject malformed input before command construction.

This fixes GHSA-4f36-m4hj-cv86 (CVSS 9.9 Critical), an authenticated
OS command injection vulnerability where malicious fill-color values
could execute arbitrary commands in the exporter container.

Defense in depth:
- Layer 1: execFile passes arguments directly without shell parsing
- Layer 2: Exporter validates colors with strict hex regex
- Layer 3: Frontend filters invalid colors before DOM emission

All three independent reporters' attack vectors are addressed:
- Quote breakout (lyhtheori)
- Command substitution (B1gN0Se)
- Path traversal (KimiSecurityTeam)

AI-assisted-by: qwen3.7-plus

* 🐛 Use existing hex-color-string? and fix test path mismatch

Address code review feedback:

- Replace duplicated hex-color-rx and valid-hex-color? with existing
  hex-color-string? from app.common.types.color
- Fix RCE test to use marker path in payload instead of hardcoded /tmp/pwned

AI-assisted-by: qwen3.7-plus

---------

Co-authored-by: Sumit Ridhal <sridhal@redhat.com>
2026-08-19 13:29:04 +02:00
Elena Torró 1886697458 🐛 Add mock to fix WASM render regression tests (#11268) 2026-08-19 12:49:21 +02:00
Elena Torró 54aaebee1e Improve shape attrs parsing performance (#11259)
*  Memoize shape-attr->token-attrs and hoist per-type attrs in get-attrs*

*  Skip redundant token merges for token-less shapes in get-attrs*

*  Freeze group descendant attrs in design panel during transforms
2026-08-19 11:50:28 +02:00
Yamila Moreno 8da13b5fa1 🔧 Add CI for temporary environment 2026-08-19 11:31:52 +02:00
David Barragán Merino ddd32670b3 📚 Remove architectural constraints related to MCP Server HA 2026-08-18 19:15:27 +02:00
Belén Albeza b6c4cb48d7 🐛 Fix not being able to select right or center-aligned text in v3 (#11258) 2026-08-18 18:13:47 +02:00
Andrey Antukh aa5545c258 Merge remote-tracking branch 'origin/staging' into develop 2026-08-18 14:34:42 +02:00
0xTHAC0andLuis de Dios d3bee4ba9d 🐛 Fix comment bubbles rendering above workspace dropdown menus (#11201)
* 🐛 Fix comment bubbles rendering above workspace dropdowns (#10283)

Comment bubbles (workspace-comments-container) had z-index: 1000, which placed
them above dropdown menus (--z-index-dropdown: 400). Replace the hardcoded 1000
with $z-index-300 from the design-system z-index scale so comments sit above the
canvas/guides but below menus and dropdowns.

* Refactor workspace comments container styles

Modernize CSS properties for workspace comments container.

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

---------

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-18 14:08:21 +02:00
Jan KahmenandAndrey Antukh 162a381aed 🐛 Apply the asset attachment disposition on the s3 backend too (#10989)
29dbf9ab1 marks non public buckets as attachments, which works on the fs
backend because nginx applies those headers to the internally redirected
response. On the s3 backend the handler answers 307 and the client then
fetches the bytes from the object store, so the header set on the redirect
does not reach the response that carries the object.

Sign the disposition into the presigned url as well, so the object store
returns it. It is only signed when the bucket is not public, so urls for
inline served objects are unchanged.

Also cover the disposition in the handler tests, for the non public buckets
and for the public ones that stay inline.

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 13:33:24 +02:00
Andrey Antukh 296dd748bd Merge remote-tracking branch 'origin/staging' into develop 2026-08-18 13:19:34 +02:00
904570f970 🐛 Handle clipboard API unavailable on access token copy (#8496) (#11156)
When copying an access token over plain HTTP (non-secure context), the
browser does not expose navigator.clipboard, causing to-clipboard to
return a rejected Promise. The caller was ignoring the Promise entirely,
so the rejection became an unhandled exception that crashed the UI.

Fix: chain .then/.catch on the returned Promise so that a successful
copy shows the existing success toast and a failure (including
insecure-origin) shows an error toast using the existing
errors.clipboard-api-unavailable translation key.

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 12:49:16 +02:00
0xTHAC0andAndrey Antukh b4bc3dfe6a 🐛 Fix grid item date tooltip showing deletion text on non-deleted files (#11161)
The grid-item-metadata* component always used :will-be-deleted-at (falling
back to :modified-at) and always showed the "Will be deleted %s" tooltip,
even for files in the Recent tab that have no deletion date.

Now the component branches on the presence of :will-be-deleted-at:
- Deleted files: show the deletion timeago with the existing
  "Will be deleted %s" tooltip.
- Regular files: show :modified-at timeago with a new
  "Last modified %s" tooltip key (dashboard.grid.last-modified-at).

Closes #10873

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 12:44:22 +02:00
Gennadiy Ivashchenko 7f2dc66e86 🐛 Preserve public URI subpath in asset download URLs (#11234)
Join asset download paths relative to PENPOT_PUBLIC_URI so temporary
exports and binary file downloads retain configured subpaths.

Add regression coverage for both URL generation paths.

AI-assisted-by: gpt-5.6-sol
2026-08-17 23:44:43 +02:00
Andrey Antukh fb9f92ae6a Merge remote-tracking branch 'origin/staging' into develop 2026-08-17 13:52:46 +02:00
David Barragán Merino 59ef07633a 🔧 Align MCP workflow name with the rest of CI workflows
The MCP workflow was named "MCP CI" while every other tests-*.yml
workflow uses the "CI: <Component>" pattern. Rename it to "CI: MCP"
for consistency in the GitHub Actions listing.
2026-08-14 20:15:23 +02:00
Alejandro Alonso ba235f46c9 Merge remote-tracking branch 'origin/staging' into develop 2026-08-14 13:50:48 +02:00
Belén Albeza e56c801820 🐛 Fix selrect collapsing after undo (v3) (#11239) 2026-08-14 13:36:40 +02:00
Alejandro Alonso 6269fa7a3f Merge remote-tracking branch 'origin/staging' into develop 2026-08-14 10:37:43 +02:00
Alejandro Alonso 136052c15e Merge remote-tracking branch 'origin/staging' into develop 2026-08-13 14:28:21 +02:00
Alejandro Alonso cb57fd9dfa Skip save_layer for plain image fills (#11230)
Avoid an offscreen buffer per Fill::Image during tile walks: only use
save_layer when a shape image filter is present; axis-aligned rects and
frames without corner radii also skip the redundant container clip.
2026-08-13 12:12:43 +02:00
Belén Albeza be83656d55 🎉 Add caret style changes (text editor v3) (#11171)
* 🐛 Fix editor v3 quitting when changing typography options

* 🎉 Apply text styles to collapsed caret

* 🐛 Fix not persisting the new selrect

* 🐛 Fix selrect not being recomputed on caret style changes

* 🐛 Fix quitting the editor when changing typography on empty texts
2026-08-13 07:23:15 +02:00
Alejandro Alonso 1c14c854ae Merge remote-tracking branch 'origin/staging' into develop 2026-08-13 07:11:43 +02:00
David Barragán Merino af1537d071 Shard integration e2e tests across four parallel jobs
Split the integration suite into four shards running two Playwright
workers each. Median wall time for the job drops from ~40 min to an
expected ~15 min; the build job is unchanged at ~4 min.

Shard reports are merged into a single HTML report, and the merged
run is summarised in the job step summary: totals, failed specs and
flaky specs ranked by retry count.

Chromium is installed into a shared volume so shards do not
re-download it. `workflow_dispatch` allows running the suite manually
against an arbitrary ref, with configurable shard layout and workers.

PRs targeting `staging` keep running serially while the current
release stabilizes. The exception is marked TEMPORARY and removed in
a follow-up.
2026-08-12 18:43:42 +02:00
María Valderrama 3b9e0782e4 🐛 Fix sso error message (#11225) 2026-08-12 17:06:02 +02:00
Alejandro Alonso 201b51e8c5 Merge remote-tracking branch 'origin/staging' into develop 2026-08-12 15:11:31 +02:00
Alejandro Alonso be9df28b00 Merge remote-tracking branch 'origin/staging' into develop 2026-08-12 07:30:05 +02:00
Elena Torró 868340dfba 🐛 Fix text layer bounds clipping glyph (#11141) 2026-08-12 07:10:34 +02:00
David Barragán Merino 9f17aa6216 🔧 Report flaky e2e tests in integration workflow
Enable Playwright's JSON reporter alongside `list` and publish a
summary of flaky tests to the job step summary. The JSON report is
kept as an artifact for 30 days so flakiness rates can be aggregated
over time.

CI already runs with `retries: 2`, so unstable tests have been passing
silently on retry. This only surfaces what the suite already absorbs;
no test behaviour changes.

The reporter in `frontend/scripts/test-e2e` becomes overridable via
`PLAYWRIGHT_REPORTER` so the local developer default stays untouched.
2026-08-11 19:50:17 +02:00
Alejandro Alonso 290b14167a 🔧 Allow forcing render-wasm DPR via ?dpr= query param (#11211)
Makes HiDPI repro possible without hardcoding get-dpr or relying on the
real devicePixelRatio (e.g. ?dpr=2).
2026-08-11 17:11:06 +02:00
Eva Marco 044d7ac15f ♻️ Update colorpicker scss file (#11208) 2026-08-11 14:29:29 +02:00
Belén Albeza 4a1d6e6d57 🐛 Fix creating minimal path shapes (#11210) 2026-08-11 13:10:44 +02:00
Alejandro Alonso 0de47302a6 Merge remote-tracking branch 'origin/staging' into develop 2026-08-11 12:47:36 +02:00
Filip SajdakandClaude Opus 5 fcd33340b3 🐛 Use a single translation key for the Mixed values label (#11151)
The design sidebar named the same "mixed values" concept with two
different translation keys. Most sections use settings.multiple, while
the blur options and the design system numeric input used
labels.mixed-values.

Both read "Mixed" in English, so the split is invisible in the default
locale, but labels.mixed-values has no translation at all in 16 locales
and a different wording in 8 more. Where it is missing the string falls
back to the default language, so those controls rendered the English
word next to sections showing the localized one; where both exist, a
single sidebar named the same concept two ways (fr "Divers" against
"Melange", ru "Smeshanyy" against "Smeshat").

Point the two outliers at settings.multiple, the key the rest of the
sidebar already uses and the one translated in every locale that ships
a translation for it.

Fixes #11148.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 10:24:23 +02:00
Álvaro Tejero-CanteroandAndrey Antukh b5bec4f983 🐛 Declare new shape attributes in schemas to match stored files (#11125)
* 🐛 Declare the shape attributes stored files carry

`schema:shape-attrs` is the shape model as *declared*, and it has fallen
behind the `Shape` record. Three record fields are absent from it:
`rotation`, `flip-x` and `flip-y` are therefore present on every shape
that exists and declared nowhere. `rotation` is already named twice in
this namespace, in `allowed-shape-attrs`, and once in
`app.common.types.shape.attrs/editable-attrs`, so the schema is
demonstrably the odd one out rather than the data being unusual.

Nothing complains, because the maps are open: an undeclared key
validates fine. What breaks is everything that reads the model *from the
schema* rather than from a live value, such as the generative tests'
shape generator, the generated OpenAPI surface, and any consumer
reflecting over `schema:shape-attrs`.

Whether an entry is optional, nilable, or both is decided by the record
rather than by taste. `app.common.record/defrecord` cannot remove a base
field: its `without` assocs nil and its `containsKey` answers true
whatever the field holds, on both platforms. So a `Shape` base field is
always present, and nil is how that field says "unset". Every other key
lives in the `$extmap`, disappears on dissoc, and is dropped by
`setup-shape` when a caller passes nil. Base fields are therefore
nilable, and the rest are optional.

Declared here, measured over a 305-shape corpus:

- `rotation`, `flip-x` and `flip-y`, record fields present on every
  shape, nilable for the reason above: `make-minimal-shape` gives the
  two flip fields no default, so they are nil on all 305. Optional as
  well, unlike the geometry below, because `schema:shape-generic-attrs`
  has a second job: `check-shape-generic-attrs` validates partial update
  payloads with it, such as the `{:blocked true}` that
  `app.main.data.workspace/update-shape` passes, and a required key here
  would reject every such payload.
- `hide-in-viewer`, moved out of `schema:frame-attrs`, because circles,
  rects and texts carry it too, 197 shapes.
- `svg-attrs`, `svg-defs`, `svg-transform` and `svg-viewbox`, the SVG
  provenance an import leaves behind, 101 shapes and 63 for the
  transform. Typed `:map` rather than more precisely on purpose: legacy
  files hold `svg-transform` as a plain `{:a … :f}` map rather than a
  `::gmt/matrix` record, and `svg-viewbox` as either a `::grc/rect`
  record or a plain map, so a tighter schema would reject files that are
  otherwise valid.
- `use-for-thumbnail` on frames. The model has long had it:
  `app.common.files.migrations` renames `:use-for-thumbnail?` to it and
  `app.common.logic.libraries` reads it. This schema had not declared
  it.
- `rx` and `ry` on rects and circles, the legacy radii SVG import parses
  off the element and migration 0003 assocs as `0`. Superseded by `r1`
  to `r4`, but stored files carry them.
- `content` on svg-raw. `shapes-builder/create-raw-svg` sets it and
  `allowed-svg-attrs` names it. Typed `[:or :map :string]`, because a
  bare text node arrives as the string itself: `<text>hi</text>` becomes
  one svg-raw for the element and another for `"hi"`, and
  `shapes-builder/parse-svg-element` carries a FIXME about exactly that.

`schema:nilable-geom-attrs` is new, for bool and path. Those two are the
only shape types whose geometry can be nil: `make-minimal-shape` gives
`x`, `y`, `width` and `height` a default for every other type and skips
those two, whose extent their content and `selrect` imply instead. The
four keys stay required, as they already are in the other seven
branches, and only the nil is new.

**Do not make the analogous change to `ctf/schema:file`.** That map
carries `:backend`, `:comment-thread-seqn` and `:ignore-sync-until`,
none of which the schema declares, and declaring them breaks saving:
`app.binfile.common/update-file!` derives its UPDATE column list from a
file map's keys, and the `file` table has no `backend` column, it being
synthesized on read. Measured at 185 failures, mostly `rpc-file-test`.
Whether a schema serving as both read description and write contract is
itself a defect is a real design question, and a separate one. The
`check-shape-generic-attrs` case above is a second instance of it.

Adding entries changes what `shape-generator` produces, so generative
tests begin exercising code paths with these attributes present. That is
where a problem would surface. With this applied the common suite is
1142 tests and 24702 assertions on the Clojure side, 992 tests and 24017
assertions on the ClojureScript side, no failures on either.

AI-assisted-by: mixed models

*  Align shape generator with declared schema and add key-presence test

shape-generator now selects geometry attrs per-type: nilable-geom-attrs
for bool/path, shape-geom-attrs for everything else, and always merges
them. This removes the dead attrs2 generation for bool/path and the
implicit dependency on create-shape adding nil defaults for missing
base record fields.

The new shape-generator-key-presence test asserts that generated shapes
carry the required keys: rotation, flip-x, flip-y on all shapes and x,
y, width, height on bool/path, even when nilable.

AI-assisted-by: longcat-2.0-free

* 🐛 Sample 200 shapes in the key-presence test, not 10

`sg/sample` hands its options to `malli.generator/sample`, which reads
`:size`. `:num` is test.check's option. It is correct for the
`smt/check!` call directly above, where it came from, but `sg/sample`
ignores it and falls back to its default of 10.

Ten samples leave the bool and path assertions vacuous about one run in
fourteen. Simulated over 200 draws of 10, 14 contained no bool and no
path at all, and the median draw held 2. Those four assertions defend
exactly the keys this branch made required, so a run that skips them
silently is the one case worth not missing.

The assertion count shows the arithmetic. The test contributed 42 with
`:num`, which is 10 shapes times 3 keys plus 3 bool-or-path shapes times
4 keys, and contributes 756 with `:size`. The common suite goes from
1143 tests and 24744 assertions to 1143 tests and 25458 assertions, no
failures either way.

AI-assisted-by: mixed models

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-07 14:20:24 +02:00
Elena Torró 30bc2a4bc3 🔧 Add FF to enable wasm export at team level (#11130) 2026-08-07 12:36:52 +02:00
Alejandro Alonso 43b12bc4b9 Soft-drain GPU mid-walk on progressive Partials (#11127)
Release packs far more cheap Current draws (e.g. fills_none paths)
into one Partial than debug; a single end-of-Partial
flush_and_submit then stalls the browser. Soft-flush every N walker
nodes (and on Partial yield) keeps ops buffers bounded while Full
still submits via present_frame.
2026-08-07 09:45:39 +02:00
Andrey Antukh e1c51442cd Merge remote-tracking branch 'origin/staging' into develop 2026-08-07 09:10:25 +02:00
Andrey Antukh 88697794ce Merge remote-tracking branch 'origin/staging' into develop 2026-08-06 20:55:34 +02:00
Elena Torró 38b990ef90 🔧 Add exporter headless backend (#10875)
*  Add headless wasm render backend to the exporter

* ♻️ Move render-wasm bridge to common and split wasm builds

* 🔧 Upload builtin font variants in the wasm exporter

* ♻️ Move shared font and resources utils out of render_wasm

*  Fetch only the exported roots in the wasm exporter

*  Bound save_layer rects in the vector export path
2026-08-06 16:13:06 +02:00
Alejandro Alonso a76401596e Skip imperceptible shadows and simplify low-scale strokes (#11102)
*  Skip drop shadows that are imperceptible at current scale

Filter drop shadows by on-screen footprint (stricter for recursive
shapes) so overview HQ avoids expensive blur passes that barely show.

*  Simplify Path and Bool strokes at low scale

At overview zooms, Inner/Outer strokes fall back to Center and
dash/dotted styles become solid when the pattern is subpixel.
Strokes are never skipped so stroke-only icons stay visible.

*  Drain GPU work on partial render frames

Partial frames only flushed the Backbuffer, so tile GPU commands
queued until present_frame's flush_and_submit and stalled the
browser on large files. Submit the context each partial frame
without presenting Target or re-composing the tile atlas.

*  Prefer direct painting when effects are imperceptible

Skip the Fills/Strokes layered path when drop/inner shadows would
not paint at the current scale, and allow stroke-only shapes
(fills_none) on the direct path. Apply the same footprint LOD to
inner-shadow painting.
2026-08-06 15:58:11 +02:00
Belén Albeza de8d8ca401 🐛 Fix serialization of constraints (#11108) 2026-08-06 15:49:04 +02:00
Álvaro Tejero-Cantero 314a2a245f 📚 Fix the devenv backend-flags instructions (#11077)
The section pointed at `docker/devenv/docker-compose.yaml`, which #9906
deleted when it split the devenv compose into `docker-compose.infra.yml`
and `docker-compose.main.yml`. The same page names both replacements in
its architecture section, so only this one was missed.

Setting PENPOT_FLAGS in the container environment would not have worked
anyway: `backend/scripts/_env` expands the inherited value before its own
list, so its flags win. Document the mechanism that does work, the
gitignored `backend/scripts/_env.local` that `start-dev` sources right
after `_env`, and the left-to-right last-wins rule that lets an override
switch off a flag `_env` enables.
2026-08-06 14:14:44 +02:00
Andrey Antukh 614d619173 Merge remote-tracking branch 'origin/staging' into develop 2026-08-06 13:31:29 +02:00
Belén Albeza 2392015c63 🐛 Fix microinteractions on text shape selrects for autowidth/autoheight (#11068) 2026-08-06 12:40:57 +02:00
Alejandro Alonso 11fc090bc4 Expand direct shape painting and skip empty drop-shadow blits (#11100)
* ♻️ Extract apply_clip_stack_to_surfaces helper

Share the layered-path clip loop so the Current-surface direct
path can reuse the same hard-clip stack without duplication.

*  Expand direct shape painting onto Current

Allow clip stacks, frames, non-identity transforms, and SrcOver
opacity on the Current-surface fast path; skip empty non-masked
groups. Avoids Fills/Strokes blits for common shapes.

*  Skip empty drop-shadow blits; warm DropShadows once

Early-out drop-shadow composite when a shape has no visible
shadows, and touch DropShadows→Current once per tile instead
of per shape to keep flush_and_submit cheap.
2026-08-06 12:31:49 +02:00
Elena Torró 10a2c19f92 🔧 Improve text editor selection and tab conversion (#11071) 2026-08-06 09:43:02 +02:00
Alejandro Alonso 4b413299c2 Clear dirty flags after tile surface reset (#11095)
Marking intermediate surfaces dirty after clearing them on tile
context switch made the first stack composite blit empty
Fills/Strokes/shadows into Current. Dirty means content to
composite, so clear the flags after the clear instead.
2026-08-06 09:09:09 +02:00
Andrey Antukh 31c9ab4701 Merge remote-tracking branch 'origin/staging' into develop 2026-08-06 09:05:48 +02:00
Alejandro Alonso 8b64b0f84f Fix progressive render budget when timestamp is stale (#11094)
Pass performance.now from finalize/debounce and re-anchor the WASM
budget if the stamp is 0 or already past max_blocking_time, so HQ
tiles are not yielded after a few nodes with almost no real work.
2026-08-06 09:00:45 +02:00
Alejandro Alonso 649f4bebef Merge remote-tracking branch 'origin/staging' into develop 2026-08-06 08:38:15 +02:00
Andrey Antukh 5b26913cd3 Merge remote-tracking branch 'origin/staging' into develop 2026-08-05 17:30:41 +02:00
Andrey Antukh 36e76da26c Revert "🐛 Fix text creating on draft.js (#11086)"
This reverts commit 6df045b194.
2026-08-05 17:30:36 +02:00
Alejandro Alonso 35bdcde183 Avoid per-tile image_snapshot when filling atlases (#11093)
Copy Current into DocAtlas and the tile atlas with Surface::draw
instead of image_snapshot_with_bounds, matching the interactive
path and removing a GPU sync stall on every completed tile.
2026-08-05 17:12:09 +02:00
Eva Marco 6df045b194 🐛 Fix text creating on draft.js (#11086) 2026-08-05 13:10:21 +02:00
Belén Albeza 1b26b69b25 🐛 Fix Firefox not inserting emoji from MacOS Character Viewer (#11072) 2026-08-05 12:53:20 +02:00
Andrey Antukh 6f2bfb617c Merge remote-tracking branch 'origin/staging' into develop 2026-08-05 10:16:06 +02:00
David Barragán Merino 34702fd46b 🐳 Remove the configuration of the admin-console from Nginx if it is not enabled 2026-08-04 20:32:17 +02:00
Filip SajdakandAndrey Antukh 648c8e2152 🐛 Keep svg-raw children as uuids on binfile import (#10837)
Importing a .penpot file left every svg-raw subtree broken: the parent's
:shapes vector came back holding plain strings instead of uuids, so the
child ids no longer resolved against the page objects map. The next
persisted change touching that page then failed referential integrity
validation with :child-not-found, surfaced to the client as an HTTP 400
:referential-integrity error, which in practice bricks the file.

An svg-raw shape can be a container: importing an SVG builds a tree of
svg-raw shapes, and cfh/group-like-shape? explicitly treats an svg-raw
with children as group-like. But schema:svg-raw-attrs was an empty map.
Frame, group and bool all declare :shapes as a vector of uuid; svg-raw
did not, so the JSON decoder used by binfile had no type information for
those ids and left them as strings.

Declare :shapes on schema:svg-raw-attrs, optional because a leaf svg-raw
shape has no children, so the child ids decode back to uuids.
Closes #10496.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-04 16:49:12 +02:00
7ae57a035f 🐛 Position overlays by frame selrect, not filter-inflated bounds (#10454)
calc-overlay-position measured the destination overlay frame with its full
object bounds (get-object-bounds) while measuring the relative-to frame with
its selrect. Object bounds include padding for shadows, blur, outer strokes
and overflowing children, so centered/right/bottom overlays were shifted by
half that extra padding when the overlay frame had such effects (the overlay
appeared offset, e.g. a bit to the left).

Use the destination frame selrect (the visible frame box) instead, which
matches the sibling helper calc-overlay-pos-initial and the viewer, which
reserves the bounds size and re-aligns the selrect separately. The now unused
geom.shapes.bounds require is removed.

Adds a regression test asserting calc-overlay-position returns the same
position with and without a bounds-inflating drop shadow on the destination
frame.

Fixes #9048

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-04 16:42:55 +02:00
Elena Torró 14a6ea5c52 🔧 Support text style shortcuts (#11002) 2026-08-04 15:30:26 +02:00
Andrey Antukh ca29f734c7 Merge remote-tracking branch 'origin/staging' into develop 2026-08-04 15:08:55 +02:00
David Barragán Merino 0811b1cda6 🔧 Generate the Docker image for the admin console by creating a tag 2026-08-04 11:59:45 +02:00
Andrey Antukh 6e843faba3 Merge remote-tracking branch 'origin/staging' into develop 2026-08-03 18:36:27 +02:00
Belén Albeza c6c8a38544 🐛 Fix not being able to add multiple fills to text spans (v3) (#10988) 2026-08-03 17:11:59 +02:00
Elena Torró 0fed63eeb3 🐛 Fix text replacement on selection and text offsets (#10983)
* 🐛 Fix text not being replaced when there is a selection

* 🐛 Fix text editor offsets on transformed text
2026-08-03 15:22:49 +02:00
2018 changed files with 50279 additions and 12005 deletions

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
# Agent skills
This folder is the single home for the skills our coding agents use.
Each skill is a folder with a `SKILL.md` inside — a short instruction
manual that an agent loads only when it needs it.
One copy serves every tool:
- **opencode** reads this folder directly.
- **Claude Code** reads it through the `.claude/skills` symlink.
- **Codex** reads it directly.
To change how the agents behave, edit the `SKILL.md` here. There is no
second copy to keep in sync.
## How the skills are organized
**Flows** are the six skills you invoke by name. Each one covers one step
in the life of a change: plan it, review the plan, implement it, review
the code, open the pull request.
**References** hold the quality standards. A flow's reviewer loads them;
you rarely touch them directly.
**Procedures** define how one concrete step is done — a plan document, an
issue, a commit. Flows call them, but they also work on their own.
**Utilities** are small helpers for everyday work: search, file lookup,
JSON, REPL access, and so on.
## Flows
| Skill | What it does | When you would say |
|---|---|---|
| [`make-a-plan`](skills/make-a-plan/SKILL.md) | Researches the task, writes an implementation plan, asks you the open questions in plain language, and saves the plan to `.agents/plans/`. | "make a plan for the token refresh bug" |
| [`review-plan`](skills/review-plan/SKILL.md) | Evaluates a plan before anyone writes code: completeness, ordering, risks. Approves it or asks for changes. | "review this plan before we start" |
| [`implement-plan`](skills/implement-plan/SKILL.md) | Shows you the full flow first — the issue and branch it will create (or the branch it continues on), the execution style, and the task checklist — and, after your go-ahead, executes a ready plan. Default: every task, one commit. On request ("step by step"): one task, one commit, your confirmation between tasks. | "implement the plan" · "step by step, one commit per task" |
| [`review-code`](skills/review-code/SKILL.md) | Reviews a diff, branch, or PR and returns findings ranked by impact. | "review my changes before I push" |
| [`create-pr`](skills/create-pr/SKILL.md) | Opens a pull request for the current branch — with checks on base branch, commits, issue, and push state — or updates an existing PR's title and description. | "open a PR for this branch" |
| [`resolve-git-conflicts`](skills/resolve-git-conflicts/SKILL.md) | Untangles merge or rebase conflicts: explains both sides, proposes a resolution, applies it after you approve. Never runs `git rebase --continue`. | "resolve these conflicts" |
## References
| Skill | What it holds |
|---|---|
| [`plan-review-criteria`](skills/plan-review-criteria/SKILL.md) | The plan review rubric: six axes, severity levels, approval standard, output format. The `review-plan` reviewer loads it. |
| [`code-review-criteria`](skills/code-review-criteria/SKILL.md) | The code review rubric: five axes, core principles (DRY, KISS, YAGNI), severity format, verdict. The `review-code` reviewer loads it. |
## Procedures
| Skill | What it does |
|---|---|
| [`planner`](skills/planner/SKILL.md) | The spec of a good plan: context, architecture decisions, tasks with acceptance criteria, checkpoints. Used by `make-a-plan`. |
| [`create-issue`](skills/create-issue/SKILL.md) | Creates a GitHub issue that follows Penpot conventions. Used by `implement-plan`; also works on its own. |
| [`create-commit`](skills/create-commit/SKILL.md) | Makes a commit the Penpot way: emoji subject, clear body, `AI-assisted-by` trailer. Used by `implement-plan`; also works alone when you say "commit this". |
## Utilities
| Skill | What it does |
|---|---|
| [`bat-cat`](skills/bat-cat/SKILL.md) | Read files in the terminal with syntax highlighting and line numbers. |
| [`fd-find`](skills/fd-find/SKILL.md) | Find files by name or pattern, respecting `.gitignore`. |
| [`ripgrep`](skills/ripgrep/SKILL.md) | Fast content search with regular expressions. |
| [`jq-json-processor`](skills/jq-json-processor/SKILL.md) | Slice, filter, and reshape JSON output. |
| [`nrepl-eval`](skills/nrepl-eval/SKILL.md) | Run Clojure or ClojureScript code in the live REPL sessions (backend and frontend). |
| [`taiga`](skills/taiga/SKILL.md) | Look up Penpot issues, user stories, and tasks in Taiga. |
| [`testing`](skills/testing/SKILL.md) | The repo's testing rules and TDD workflow, loaded before writing tests. |
| [`security-and-hardening`](skills/security-and-hardening/SKILL.md) | Security checks for code that handles user input, auth, or external services. |
| [`ste`](skills/ste/SKILL.md) | Rewrites prose in Simplified Technical English. Loads only when you name it. |
| [`refine-prompt`](skills/refine-prompt/SKILL.md) | Rewrites a rough prompt into a clearer one. Never runs the prompt. |
| [`update-changelog`](skills/update-changelog/SKILL.md) | Regenerates `CHANGES.md` from a GitHub milestone. |
## A typical round
1. `/make-a-plan` — you get a plan and a saved file in `.agents/plans/`.
2. `/review-plan` — a second opinion; approve or request changes.
3. `/implement-plan` — the code gets written and committed. Starting from a base branch, it also opens the GitHub issue and the `issue-NNNN` branch; the plans that follow continue on that same branch.
4. `/review-code` — a reviewer checks the commit.
5. `/create-pr` — the branch goes up as a pull request.
Every step also works on its own, and you can always say what you want
in plain words — the agents pick the right skill from what you say.
## Adding or changing a skill
Create a folder here with a `SKILL.md` inside. The file needs `name` and
`description` in its frontmatter, and a clear "When to use" section so
agents know when to reach for it. Keep one job per skill, and keep the
two families apart: flows are named with a verb first; reference skills
end in `-criteria`.
@@ -9,6 +9,11 @@ metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"i
`cat` with syntax highlighting, line numbers, and Git integration.
## When to use
- Reading or displaying a file in the terminal — prefer it over plain
`cat`: syntax highlighting, line numbers, git-side indicators.
## Quick Start
### Basic usage
@@ -1,9 +1,9 @@
---
name: code-review
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
name: code-review-criteria
description: Code review criteria — the five review axes, core principles, severity format, and verdict for reviewing code changes. Loaded by the reviewer subagent of the review-code flow. Not a user-facing flow — to review code, use the review-code flow.
---
# Code Review and Quality
# Code Review Criteria and Quality
## Overview
@@ -13,11 +13,10 @@ Multi-dimensional code review with quality gates. Every change gets reviewed bef
## When to Use
- Before merging any PR or change
- After completing a feature implementation
- When another agent or model produced code you need to evaluate
- When refactoring existing code
- After any bug fix (review both the fix and the regression test)
- The reviewer subagent of the `review-code` flow loads this skill to perform
the review of a code change.
- To review code, always go through the `review-code` flow — never load this
skill directly for that. This is the criteria reference, not the flow.
## Core Principles
@@ -106,6 +105,8 @@ For detailed security guidance, see `security-and-hardening`.
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
| **Suggestion:** | Worth considering | Not required, but improves the code |
**Unique finding IDs.** Assign every finding a stable identifier: `F1`, `F2`, `F3`, … numbered in order of severity (Critical first, then High, Medium, Low, Suggestion). Use the ID everywhere the finding is mentioned — in section headers, in the verdict, in follow-up discussion. Never renumber within a review. Example: `**F3 (High)** — `app/validate.cljs:42` — duplicate branch logic…`.
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
@@ -122,11 +123,11 @@ Briefly explain what the code does and give an overall assessment.
### Critical and High-Priority Issues
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. Each finding gets its unique ID (`F1`, `F2`, …). For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
### Other Findings
List medium- and low-priority issues, including maintainability and design concerns.
List medium- and low-priority issues, including maintainability and design concerns. Continue the ID sequence started above (`F3`, `F4`, …).
### Suggested Refactoring
@@ -148,6 +149,8 @@ Choose one:
- **Approve with minor changes** — Good to merge after addressing low/medium issues
- **Request changes** — Critical or high issues must be resolved before merge
List the finding IDs the verdict depends on (e.g. "Request changes: F1, F4").
## Change Sizing
Small, focused changes are easier to review, faster to merge, and safer to deploy.
@@ -231,25 +234,13 @@ For supply-chain risk triage, follow the `security-and-hardening` skill.
## Verification
After review is complete:
Before emitting the verdict, verify the change as it stands. This is the reviewer's own due diligence — it covers the state of the code at review time, not the later resolution of findings (fixing findings is the author's job; confirming them is a new review):
- [ ] All Critical issues are resolved
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
- [ ] Tests pass
- [ ] Tests pass — run them yourself, don't trust the claim
- [ ] Build succeeds
- [ ] The verification story is documented (what changed, how it was verified)
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
## Multi-Model Review Pattern
Use different models for different review perspectives:
```
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
```
Different models have different blind spots.
## See Also
- For detailed security review guidance, see `security-and-hardening`
File renamed without changes.
+105
View File
@@ -0,0 +1,105 @@
---
name: create-pr
description: PR flow — open a new PR for the current task branch (validates base branch, commits, issue and push state) or update an existing PR's title or description to match Penpot conventions. Use it when the user asks to open or create a PR, in any phrasing.
---
# Create PR
Two modes. **Open mode** takes the current task branch to a new, validated
PR. **Update mode** rewrites an existing PR's title or description. Gather
information, validate, and act in one pass. If validation fails, STOP with a
single coherent message that lists every problem and states exactly what
information is missing — never fix or work around problems silently.
Both modes require an authenticated `gh` CLI (`gh auth status`) and never
push — the user pushes from their own shell.
## When to use
- The user asks to open or create a NEW PR for the current task branch, in
any phrasing ("open a PR", "create the pull request", "put this up for
review") — or runs `/create-pr`. → **Open mode**.
- The user asks to fix or update an EXISTING PR's title or description to
match conventions. → **Update mode**.
If the running agent cannot write (for example, the plan agent), say so and
stop — this skill needs the build agent.
## Open mode
### 1. Gather context (read-only)
- Current branch: `git rev-parse --abbrev-ref HEAD`.
- Target base branch: run `./scripts/detect-target-branch` from the repo root.
It prints the nearest ancestor branch of HEAD (exit 0) or fails (exit 1).
- Commits: `git log --oneline <base>..HEAD`.
- Push state (local): `git rev-parse --verify origin/<branch>` and compare
with HEAD. It reads the local remote-tracking ref — no network, no SSH. It
reflects the last push or fetch this clone knows about.
- Issue: from the session context, or from the branch name — `issue-NNNN`
maps to issue NNNN; recover its title and body with `gh issue view NNNN`.
### 2. Validate — stop with one message if anything fails
Run all checks before reporting, then report every failure together:
1. **Base branch not usable.** If the script fails (exit 1), or its output —
after stripping an optional `remotes/origin/` prefix — is not one of the
canonical branches (`develop`, `staging`, `main`), stop and ask the user
to re-run with more context — for example, passing the base branch
explicitly in their invocation. An explicit base given by the user
overrides the script's output.
2. **On a base branch.** There is no task branch to merge — say so and stop.
3. **No commits.** The branch has no commits ahead of the base — say so and
stop.
4. **No clear issue.** There is no issue in the session context, and the
branch name has no `issue-NNNN` pattern (or `gh issue view` finds nothing)
— say so and stop. Exception: the user's invocation says `no issue` /
`without issue` — then continue without an issue reference.
5. **Branch not pushed.** The remote-tracking ref `origin/<branch>` is
missing, or `git rev-parse origin/<branch>` differs from HEAD — the
branch was never pushed, or has commits the remote does not have. Never
push yourself; ask the user to push and to run `/create-pr` again
afterwards, then stop.
### 3. Already-open PR
Check whether a PR already exists for this branch (`gh pr list --head
<branch>`). If one exists, report its URL and stop — do not create a second
one. Title or description fixes belong to Update mode.
### 4. Write and create the PR
Write the title and body following `mem:workflow/creating-prs` (title format,
description structure, writing principles) and `mem:workflow/creating-commits`
(commit type emojis). Derive the title and body from the commits and, when
there is one, from the issue body. Reference the issue with `Closes #NNNN`.
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
### 5. Report
Report the PR URL and stop.
## Update mode
1. Identify the PR: the number given by the user, or `gh pr list --head
<branch>`.
2. Write the new title and/or body following `mem:workflow/creating-prs`.
3. Apply and verify:
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
4. Report and stop.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: overrides such as `no issue` /
`without issue`, an explicit base branch (`from origin/staging`), a PR number
for Update mode, and so on.
@@ -9,6 +9,11 @@ metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id
User-friendly alternative to `find` with smart defaults.
## When to use
- Locating files or directories by name or pattern — prefer it over
plain `find`: simpler syntax, smart defaults, respects `.gitignore`.
## Quick Start
### Basic search
+132
View File
@@ -0,0 +1,132 @@
---
name: implement-plan
description: Implementation flow — execute a ready plan from the session context: read the plan, detect the flow, then present the full picture (issue and branch to create or the branch to continue on, execution style, task checklist) and wait for confirmation. Default is every task with one final commit; on request ("step by step"), one task and one commit at a time with a pause after each. Use it when the user asks to implement or execute a plan, in any phrasing.
---
# Implement Plan
This flow is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context. It never pushes —
the user pushes.
By default it ends with exactly one commit. When the user asks for it
("step by step"), it commits once per task instead and waits for the
user's confirmation after each one (see *Execution modes*).
## When to use
- The user asks to implement or execute a plan, in any phrasing:
"implement the plan", "execute it", "go build it" — or runs
`/implement-plan`.
- A ready, reviewed plan is in the session context or a plan file path
was given (typically after `/make-a-plan` or `/review-plan`).
Do not use it to produce plans — that is the `make-a-plan` flow.
## 1. Read the plan first
Identify the plan to execute — from the file path the user gave, the
arguments, or the session context. Read it completely. Read the required
memories before writing any code: `mem:critical-info` and the core memory
of every module the plan touches, plus the deeper memories they reference
(AGENTS.md governs this).
## 2. Detect the flow (no questions)
Inspect the current branch with `git rev-parse --abbrev-ref HEAD`, pick the
mode, and announce it in one line before presenting anything. Detection is
read-only: nothing is created until the user confirms (step 3).
- **On a base branch** (`main`, `develop`, `staging`) → **standalone mode**:
a new GitHub issue and a branch `issue-NNNN` will be created after the
user's confirmation.
- **On any other branch** (a feature branch, typically `issue-NNNN`) →
**continue mode**: the implementation continues on the current branch.
No issue or branch is created. The branch name provides the issue
reference when it follows the `issue-NNNN` pattern.
Arguments override detection: `standalone`, `continue`,
`no issue` / `without issue`, or an explicit base such as
`from origin/develop`.
**Standalone while already on a feature branch:** stop and explain that this
would stack branches. Ask the user to re-run with an explicit base, for
example `from origin/develop` — then branch from that base instead of HEAD.
## 3. Present the checklist and wait
Before touching the repository, show the user the full picture:
- **The flow**: whether the GitHub issue and the branch will be created
(standalone mode — give the planned branch name, `issue-NNNN` or
`plan-<slug>`), or whether you continue on the current branch
(continue mode — name it).
- **The execution style**: batch or step-by-step (see *Execution modes*).
- A checklist (todolist) of the plan's tasks, in order.
Then WAIT for the user's explicit confirmation. Do not start until you
have it. If the plan has no discrete tasks, ask the user how to split
it, or propose running it as a single change.
## 4. Execute the plan
**Standalone setup, after the confirmation:** create the issue with the
**`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body
from the plan, capture the new issue's number — call it **NNNN** — and
create the branch from the current HEAD:
```
git checkout -b issue-NNNN
```
If the arguments say `no issue` / `without issue`, skip the issue and
create a branch named `plan-<slug>` instead, where `<slug>` is the plan
title, lowercase and hyphen-separated.
### Batch mode (default)
Implement every task in one go. Work methodically, keeping changes
focused on what the issue requires. Respect the plan's proposed
parallelization when it applies.
When the implementation is complete, load the **`create-commit`** skill
and follow its workflow to commit the changes. Provide a brief summary
of what was implemented and why, the issue reference (`issue-NNNN`) when
there is one, and the model name you are running as so the
`AI-assisted-by` trailer is set correctly.
### Step-by-step mode (on request)
When the user asks for it — "step by step", "task by task", "one commit
per task" — loop one task at a time:
- Execute exactly ONE task.
- Commit it now: load the **`create-commit`** skill and follow it —
one commit per task, never two tasks in one commit. Same inputs as
always: what and why, the issue reference, your model name.
- Show the user the result (what changed, files touched, how it was
verified).
- WAIT for the user's confirmation before starting the next task.
Never batch in this mode: no two tasks in one commit, and no new task
before the user confirms. If a task turns out much bigger than planned,
stop and ask the user before splitting it.
## When you are done
End by suggesting the next steps (suggestions, not a required pipeline — any
instruction from me overrides them):
- `/review-code` — to review the changes just committed; it routes to
`/make-a-plan` by itself if the findings need one.
- `/create-pr` — when the task is done and the branch is ready to merge.
## User context
Extra context in the user's invocation (the message that triggered this
skill) plays the role command arguments play elsewhere: `standalone`,
`continue`, `no issue` / `without issue`, an explicit base such as
`from origin/develop`, or `step by step` / `one commit per task` for the
step-by-step execution mode. Modes combine freely, for example
"standalone step by step".
@@ -9,6 +9,11 @@ metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id
Process, filter, and transform JSON data with jq.
## When to use
- Parsing, filtering, or transforming JSON from commands, files, or API
responses — slicing, reshaping, or validating JSON output.
## Quick Examples
### Basic filtering
+100
View File
@@ -0,0 +1,100 @@
---
name: make-a-plan
description: Planning flow — research the subject of this session, produce an implementation plan with the planner skill, resolve open questions with the user in plain language, and save the final plan to .agents/plans/. Use it when the user asks to plan, design, or break down a task, in any phrasing.
---
# Make a Plan
Act as a senior software engineer: research the subject of this session in depth and
produce a well-grounded, actionable implementation plan.
If the running agent cannot write (for example, the plan agent), say so and
stop — this skill needs the build agent to save the plan.
## When to use
- The user asks to plan, design, or break down a task, in any phrasing:
"make a plan", "how would we build X", "design an approach for Y" —
or runs `/make-a-plan`.
- The user asks to rework or extend an existing plan (for example, after
review findings) — revise the saved plan file in place.
Do not use it to execute a plan — that is the `implement-plan` flow.
## Instructions
1. **Produce the plan** with the `planner` skill. By default, research the
subject of this session and draft the plan yourself. If I ask for it (for
example, `delegated` in the user context), delegate to the `general` subagent
instead — the delegate must also follow the `planner` skill and receive all
the relevant session context (a review, user feedback, and so on).
2. Before asking me to decide anything, explain the plan and every open question in
plain language. Assume I know only the high-level project goal, not the codebase,
architecture, implementation terms, or the problem this task solves.
3. Once all decisions are answered and the plan is final, save it verbatim to the
announced path under `.agents/plans/` (create the directory if it does not
exist). This step is the flow's explicit authorization to write the plan
file — the only write allowed here. If I later ask for changes, update the
saved file directly.
4. Present me with a clear, self-contained summary of the plan's most relevant points
only after all required decisions have been answered. Write it for someone who knows
only the project's high-level goal and may not know the plan's low-level context.
Explain necessary technical language in plain terms, include the problem being
solved and the proposed outcome, and do not assume that listing technical task names
is enough.
### Hard rule — read-only while planning
While this flow runs, act read-only: research with read-only tools only.
Never edit source files, never run builds, tests, linters, or any command that
modifies state, and never commit. The single allowed write is the plan file in
step 3. This rule expires when I approve the plan or move on to another task;
then you act as a normal build agent again.
When the plan contains open questions, do not show them as bare technical questions or
assume that I understand the technical language or technical words used in the plan.
For each question, first explain:
- What part of the user problem the decision affects.
- The relevant concept from the beginning, with a small concrete example.
- What each available option would make the system do.
- The practical benefits, costs, risks, and user-visible consequences of each option.
- Which option the planner recommends and why.
Only after that explanation, use the `question` tool to ask the decision with clear,
non-technical option labels. Put the recommended option first and mark it as
`(Recommended)`. Group related questions when their context is shared, but do not ask a
question whose meaning has not already been explained.
If I say that I do not understand a question or its choices, do not treat my previous
answer as valid. Explain the concepts again from the high-level project goal, use a more
concrete example, explain the implications, and ask the question again with the
`question` tool. Repeat this until I can make an informed choice. If one answer creates
new design consequences or additional decisions, explain those consequences before
asking any new question.
Distinguish clearly between requirements already fixed by the roadmap or existing
architecture and choices that actually require my input. Do not ask me to choose an
implementation detail when the plan can resolve it safely without changing the public
behavior. If there are no decisions that require my input, say so and present the
summary.
IMPORTANT: **Under no circumstances execute the plan. Wait for the user to review it
after all possible questions have been answered.** The final summary must explain the
problem being solved, the proposed behavior, the main user-visible workflow, important
constraints and risks, what is deliberately out of scope, and the path where the plan
is saved. Never assume that a short list of task names is enough context. End
the final response by suggesting the next steps, in this order:
1. `/review-plan` — to get a second opinion on the plan before executing it.
2. `/implement-plan` — to execute the plan from the current session context.
These are suggestions, not a required pipeline — any instruction from me
overrides them (for example, asking you to implement the plan directly).
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, `delegated` to
hand the research and drafting to the `general` subagent, or corrections and
feedback about a previous plan.
@@ -10,6 +10,12 @@ Evaluate Clojure (or ClojureScript) code via a running nREPL server using
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
## When to use
- Evaluating Clojure or ClojureScript code against the running nREPL
sessions (backend 6064, frontend 3447) — live inspection, patching, or
debugging.
## Quick Reference
```bash
@@ -1,9 +1,9 @@
---
name: plan-review
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
name: plan-review-criteria
description: Plan review criteria — the six review axes, severity rubric, approval standard, and output format for reviewing implementation plans. Loaded by the reviewer subagent of the review-plan flow. Not a user-facing flow — to review a plan, use the review-plan flow.
---
# Plan Review
# Plan Review Criteria
## Overview
@@ -13,10 +13,10 @@ Multi-dimensional plan review with quality gates. Every plan gets reviewed befor
## When to Use
- After the planner skill produces a plan
- Before starting implementation on any non-trivial task
- When reviewing a plan written by another agent or a human
- When a plan feels too large, vague, or risky to start
- The reviewer subagent of the `review-plan` flow loads this skill to perform
the review of a plan.
- To review a plan, always go through the `review-plan` flow — never load this
skill directly for that. This is the criteria reference, not the flow.
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
@@ -87,7 +87,7 @@ Can an implementer actually execute this?
### 6. Proposed Code Quality *(when the plan includes implementation details)*
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review-criteria`:
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
- **Readability:** Are proposed names descriptive and consistent with project conventions?
@@ -215,7 +215,7 @@ Check that the plan can actually confirm it worked:
If the plan includes code snippets, types, or API designs:
```
- Load code-review skill for criteria
- Load code-review-criteria skill for criteria
- Check proposed signatures for edge cases
- Verify naming follows project conventions
- Confirm abstractions follow existing patterns
@@ -310,6 +310,6 @@ If the plan includes code snippets, types, or API designs:
## See Also
- For producing plans, use the `planner` skill
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
- For reviewing implemented code, use `code-review-criteria` — also the criteria source for axis 6
- For security-specific concerns, see `security-and-hardening`
- For testing strategy guidance, see `testing`
+380
View File
@@ -0,0 +1,380 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan with task breakdown, acceptance criteria, sizing, and checkpoints. Always output to the user with the plan's save path (saved or suggested) and the next steps.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans with task breakdowns that engineers or other agents can
execute. Never writes or modifies code.
## When to Use
- The user asks for a plan, design, or analysis of a feature or bug.
- The user wants to understand which parts of the codebase a task will touch.
- The user needs a step-by-step implementation plan with file paths, function
names, and test strategy.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
breakdown.
- A task feels too large or vague to start.
- Work needs to be parallelized across multiple agents or sessions.
Do **not** use this skill to actually implement anything — it is read-only.
**When NOT to use:** Single-file changes with obvious scope, or when the spec
already contains well-defined tasks.
## Role
You help users understand the Penpot codebase, design solutions, and produce
implementation plans that other agents or developers can execute. The plan
tells them what to build and how to verify it, task by task.
The implementer reads the project's agent docs (`AGENTS.md`, project memories
such as `mem:critical-info`, `mem:testing`, and each module's core memory)
before working. Reference those memories instead of re-explaining tooling,
conventions, or test design — explain in the plan only what they do not cover.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the implementer's responsibility.
## CRITICAL: Required Reading Before Planning
Before drafting any plan, work through the project's own guidance:
1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
that describes the monorepo structure and module dependency graph.
2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
4. For each affected module, note its lint, format, and test commands so the
plan can include concrete verification steps.
Skipping this step is the #1 cause of incorrect or incomplete plans.
---
## The Planning Process
### Phase 1: Architecture Analysis
1. Read the spec, requirements, or feature request.
2. Analyze the codebase architecture and identify affected modules.
3. Read project conventions (starting with `critical-info` and module core
memories) before drafting.
4. Map dependencies between components (see the dependency graph in
`critical-info`).
5. Identify risks, edge cases, performance implications, and breaking changes.
### Phase 2: Task Breakdown
#### Identify the Dependency Graph
Map what depends on what, following the monorepo's module dependency graph:
```
common (shared types, schemas — no deps)
├── backend (depends common)
│ ├── RPC handlers
│ └── persistence / migrations
├── frontend (depends common, render-wasm)
│ ├── UI components
│ └── state / API integration
├── exporter (depends common)
└── render-wasm (consumed by frontend)
```
Implementation order follows the dependency graph bottom-up: build shared
foundations first, then layer consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
**Bad (horizontal slicing):**
```
Task 1: Build all common types
Task 2: Build all backend handlers
Task 3: Build all frontend components
```
**Good (vertical slicing):**
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
Task 3: frontend UI component + API integration
```
Each vertical slice delivers working, testable functionality.
#### Write Tasks
Each task follows this structure:
```markdown
## Task [N]: [Short descriptive title]
**Description:** One or two paragraphs explaining what this task accomplishes.
Should be clear and concise.
**Rationale:** Why this task exists and why this approach over the obvious
alternatives — design decisions, trade-offs, constraints discovered during
analysis. One or two sentences; skip only if genuinely trivial.
**Code sketch (optional):** Signature-, type-, or shape-level example when the
intended interface is non-obvious. Keep it short — a skeleton that fixes the
contract (function signature, model fields, error shape), never a full
implementation. Omit when the task is mechanical.
**Acceptance criteria:**
- [ ] [Specific, testable condition]
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Relevant tests pass (module-specific test command).
- [ ] Lint/formatter passes (module-specific check command), if applicable.
- [ ] The core flow works end-to-end, if applicable.
**Dependencies:** [Task numbers this depends on, or "None"]
**Files likely touched:**
- `path/to/file.clj`
- `path/to/file_test.clj`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common,
`npx shadow-cljs compile test && npx karma start` for frontend, or the
commands noted in the module's core memory).
When possible, design each task with TDD in mind: acceptance criteria double
as a test list, and the natural first step of the task is writing those tests
before the implementation. Some tasks resist this (config, migrations, pure
wiring) — for those, keep the usual verification steps.
#### Estimate Scope
| Size | Files | Scope | Example |
|------|-------|-------|---------|
| **XS** | 1 | Single function, config change, or schema tweak | Add a validation rule |
| **S** | 1-2 | One handler or component method | Add a new RPC endpoint |
| **M** | 3-5 | One vertical feature slice | Bookmark CRUD with tests |
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
| **XL** | 8+ | **Too large — break it down further** | — |
If a task is XL, it should be broken into smaller tasks. Agents perform best
on S and M tasks.
**When to break a task down further:**
- It would take more than one focused session
- You cannot describe the acceptance criteria in 3 or fewer bullet points
- It touches two or more independent subsystems
- You find yourself writing "and" in the task title (a sign it is two tasks)
#### Order and Checkpoints
Arrange tasks so that:
1. Dependencies are satisfied (build foundation first)
2. Each task leaves the system in a working state
3. Verification checkpoints occur after every 2-3 tasks
4. High-risk tasks are early (fail fast)
Add explicit checkpoints with the relevant module commands:
```markdown
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
```
## Requirements
- Analyze the codebase architecture and identify affected modules.
- Read project conventions before drafting (start with `critical-info` and
affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist after every 2-3 tasks.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code. The
only file you may write is the plan itself, and only when the command or
user explicitly instructs you to save it.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`, `bat`).
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill. By default you never write the plan file;
announce the path instead. Write the file only when the command or user
explicitly instructs you to save it — and then only that file.
Announce the suggested save path:
```
.agents/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). If the user explicitly
provides a target file path, announce that path instead of the default.
End the response by suggesting the next steps: `/review-plan` to get a second
opinion on the plan and `/implement-plan` to execute it.
### Plan Document Template
```markdown
# Plan: [Feature/Project Name]
## Context
[One paragraph: what is the problem or feature request? Why is it needed?]
## Affected Modules
[Which modules of the monorepo are involved? Reference module paths and any
`mem:` memories that were consulted.]
## Architecture Decisions
- [Key decision 1 and rationale]
- [Key decision 2 and rationale]
## Risks & Considerations
[Edge cases, performance implications, breaking changes, migration concerns,
security implications.]
## Approach
[A short strategy summary: 3-5 sentences describing the overall approach and
the shape of the dependency graph (what depends on what, what gets built
first). High-level only — the task-by-task detail lives in the Task List.]
## Task List
Each task uses the full task structure defined in
[Write Tasks](#write-tasks) — description, rationale, acceptance criteria,
verification, dependencies, files, estimated scope, and optional code sketch.
Never reduce a task to a one-line checkbox; the plan must be self-contained
and executable without other context.
Tasks are a flat, ordered list — a plan is not a roadmap. Do not group tasks
into phases, milestones, or sprints; ordering and dependencies are already
captured per task. Insert a checkpoint after every 2-3 tasks.
## Task 1: [Short descriptive title]
**Description:** [What this task accomplishes.]
**Rationale:** [Why this approach over the alternatives.]
**Acceptance criteria:**
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Relevant tests pass (module-specific command).
**Dependencies:** None
**Files likely touched:**
- `path/to/file`
**Estimated scope:** [XS: 1 file | S: 1-2 files | M: 3-5 files | L: 5+ files]
**Code sketch (optional):** [Short contract-level example, only if the shape
is non-obvious.]
## Task 2: [Short descriptive title]
[Same structure as Task 1.]
## Task 3: [Short descriptive title]
[Same structure as Task 1.]
### Checkpoint: After Tasks 1-3
- [ ] Relevant tests pass (module-specific command).
- [ ] The relevant build or compilation passes, if applicable.
- [ ] The core flow works end-to-end.
- [ ] Review with human before proceeding.
## Task 4: [Short descriptive title]
[Same structure as Task 1.]
## Task 5: [Short descriptive title]
[Same structure as Task 1.]
## Verification & Testing
[How to verify each task and the whole plan: the project's real test, lint,
build, and run commands (extracted during Required Reading), coverage
expectations, and manual checks. Consult each module's core memory for the
exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features, documentation
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
## Open Questions
- [Question needing human input]
```
When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. |
| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. |
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
## Red Flags
- Delivering prose without a task breakdown
- Tasks that say "implement the feature" without acceptance criteria
- No verification steps in the plan
- All tasks are XL-sized
- No checkpoints between tasks
- Dependency order isn't considered
## Verification Checklist
Before delivering the plan, confirm:
- [ ] Every task has acceptance criteria
- [ ] Every task has a verification step
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task is XL or larger — break it down instead
- [ ] Checkpoints exist after every 2-3 tasks
- [ ] The response states the plan's path (saved or suggested) and suggests
`/review-plan` and `/implement-plan`
- [ ] The plan is ready for human review
@@ -0,0 +1,47 @@
---
name: resolve-git-conflicts
description: Conflict resolution flow — understand the local git conflicts, present a resolution plan, and resolve them after the user approves it. Never continues the rebase. Use it when the repo has unresolved conflicts (rebase, merge, cherry-pick) or the user asks to resolve them.
---
# Resolve Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## When to use
- The repository has unresolved conflicts — during a rebase, merge, or
cherry-pick — whether the user asks about them or not.
- The user asks to resolve conflicts, in any phrasing: "fix the merge
conflicts", "resolve these", "what's conflicting here?".
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
+73
View File
@@ -0,0 +1,73 @@
---
name: review-code
description: Code review flow — review a diff, PR, or code change, delegating the review to a subagent that follows the code-review-criteria skill. Use it when the user asks to review code or a PR, in any phrasing.
---
# Review Code
Act as a senior software engineer and perform a thorough code review.
## When to use
- The user asks to review code, in any phrasing: "review this diff",
"review the PR", "check my changes", "code review" — or runs
`/review-code`.
- A commit, branch, PR, or diff is ready and the user wants it assessed
before merge.
## Instructions
1. **Determine what is being reviewed** from the user context: a working-tree
diff, a commit range, a branch, a PR (number or URL), or specific files. If
the target is ambiguous, ask before reviewing.
2. Delegate the review to the `general` subagent (via the task tool), unless the
user specifies another agent. Include in the prompt the
**`code-review-criteria`** skill name and all user context.
3. When the subagent returns, output the review to the user verbatim. Do not
summarize it and do not act on its findings.
4. Right after the review, suggest how to proceed based on the findings. These
are suggestions — the user decides:
- **Approve (no required changes):** say so — there is nothing to address.
- **Minor findings (nits):** applying them directly as-is is fine once the
review is done — no plan needed.
- **Substantive findings:** suggest `/make-a-plan` to make a plan to address
them.
### Hard rule — read-only while reviewing
This flow is read-only **for the duration of the review**: from the moment it
starts until the user considers the review finished (including any feedback,
questions, or clarifications about it). During that period, never fix,
implement, edit files or create commits — not even "obvious" fixes derived from
the findings. Once the user explicitly states the review is done (or moves on to
a different task), this rule no longer applies and you act as a normal build
agent again.
## Instructions for the subagent
1. Load the **`code-review-criteria`** skill and follow its process and output
format.
2. Read `AGENTS.md` (if present) and follow its instructions for finding and
reading all related testing documentation from memories before reviewing.
3. Return in your final message the COMPLETE review, verbatim, exactly as the
skill instructs it to be produced. Do not summarize it — include the full
structured review.
### Strong rules for the subagent
1. Do not invent problems. Every finding must be real and actionable.
2. Read-only: do not modify any file and do not create a commit — reviewing
never writes.
3. Be specific and constructive. "This could be better" is not helpful — explain
why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged
finding — never as a recommendation.
6. Skip generated files, lockfile-only changes, and unrelated modifications
unless they introduce security risks.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, a PR number or
URL, a commit range, specific files, or a different agent to run the review.
+71
View File
@@ -0,0 +1,71 @@
---
name: review-plan
description: Plan review flow — evaluate an implementation plan before it is executed, delegating the review to a subagent that follows the plan-review-criteria skill. Use it when the user asks to review a plan, in any phrasing.
---
# Review Plan
Act as a senior software engineer and perform a thorough review of an
implementation plan.
## When to use
- The user asks to review a plan, in any phrasing: "review this plan",
"does this plan look right?", "second opinion on the plan" — or runs
`/review-plan`.
- A plan was just produced (typically by `/make-a-plan`) and the user
wants it evaluated before executing it.
## Instructions
1. **Determine the plan under review** from the session context (for example, a
plan just produced by `/make-a-plan`) or from a plan file path given by the
user (typically under `.agents/plans/`). If a file path is given, read the
file first so the complete plan is in context.
2. Delegate the review to the `general` subagent (via the task tool), unless the
user specifies another agent. Include in the prompt the
**`plan-review-criteria`** skill name and all user context.
3. When the subagent returns, output the review to the user verbatim. Do not
summarize it and do not act on its findings.
4. Right after the review, suggest the next step based on the verdict. These
are suggestions — the user decides, and any instruction overrides them:
- **Approve** → suggest `/implement-plan` to execute it.
- **Request changes** → suggest `/make-a-plan` to make a plan to address the
findings.
### Hard rule — read-only while reviewing
This flow is read-only **for the duration of the review**: from the moment it
starts until the user considers the review finished (including any feedback,
questions, or clarifications about it). During that period, never fix,
implement, edit files or create commits — not even "obvious" fixes derived from
the findings. Once the user explicitly states the review is done (or moves on to
a different task), this rule no longer applies and you act as a normal build
agent again.
## Instructions for the subagent
1. Load the **`plan-review-criteria`** skill and follow its process and output
format.
2. Read `AGENTS.md` (if present) and follow its instructions for finding and
reading all related documentation and testing memories before reviewing.
3. Return in your final message the COMPLETE review, verbatim, exactly as the
skill instructs it to be produced. Do not summarize it — include the full
structured review.
### Strong rules for the subagent
1. Do not invent problems. Every finding must be real and actionable.
2. Read-only: do not modify any file and do not create a commit — reviewing
never writes.
3. Be specific and constructive. "This could be better" is not helpful — explain
why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Judge the plan as the implementer would: every task executable without
guessing, ordering follows the dependency graph, risks named.
## User context
Extra context in the user's invocation (the message that triggered this skill)
plays the role command arguments play elsewhere: for example, a plan file path
to review, or a different agent to run the review.
@@ -9,6 +9,11 @@ metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id
Fast, smart recursive search. Respects `.gitignore` by default.
## When to use
- Searching file contents across the repo for regex patterns — the
default code search, respects `.gitignore`.
## Quick Start
### Basic search
@@ -9,6 +9,13 @@ Apply the ASD-STE100 standard to all prose you produce in this task. Do not anno
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
## When to use
Only when the user explicitly invokes it: they type `/ste`, or say "use
the ste skill" / "apply ASD-STE100". Requests like "simplify this",
"make it clearer", or "shorter sentences" do NOT invoke it — respond
normally unless it is named.
## Step 0 — Classify the text
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
@@ -11,6 +11,12 @@ Fetch information from Taiga public API for the **Penpot** project
**No authentication required** — only public project data is accessed.
## When to use
- The user asks about Penpot issues, user stories, or tasks tracked in
Taiga — fetch them via the public API (project id 345963), no
authentication needed.
## Prerequisites
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
@@ -34,7 +34,8 @@ Before writing any test, read:
2. Module-specific testing memory for the affected module:
- `mem:common/testing` — CLJC unit tests
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
- `mem:backend/core` — JVM clojure.test conventions
- `mem:backend/testing` — JVM clojure.test conventions
- `mem:exporter/testing` — exporter unit tests
## Key Rules
@@ -212,6 +212,37 @@ superseded it:
Replace the reference in the changelog entry with the correct merged PR number.
### 5b. Security advisory (GHSA) entries
Security advisories fixed in a release are documented in the changelog even
though they are **neither milestone issues nor PRs**. The GHSA ID and its
description are supplied by the user or the release notes — they never come
from the milestone fetch in step 2.
**Format** (matches the existing precedent in `CHANGES.md`, e.g. the
`create-font-variant` arbitrary file read advisory):
```markdown
- Fix <user-facing description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Rules:
- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link**
only the advisory URL.
- The advisory may be **draft/unpublished** at changelog time (the URL 404s
publicly). Do **not** web-fetch or verify the URL, and do **not** drop the
entry because of that. Rely on the GHSA ID provided by the user.
- Derive the description from the supplied advisory title, imperative mood and
user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`).
- These entries are **invisible to the automation**: they are not returned by
`gh.py issues`, not matched by `--compare` (step 3), not part of the PR
cross-reference (step 10), and not scanned by the anomaly-report regexes
(step 11, which only match `issues/` and `pull/` links). Add them manually.
- During pre-flight checks (step 6a) apply only the **backport/duplicate**
check: if the same GHSA already appears in an earlier version section, remove
it from the current section. Their absence from milestone cross-references
is expected, not an anomaly.
### 6. Read the current CHANGES.md
Read the top of `CHANGES.md` to understand the existing format and find the
@@ -400,6 +431,8 @@ if closed:
- ✅ Every merged milestone PR is either in the changelog or excluded by label
- ✅ PR and issue counts are internally consistent
- ✅ No false-positive PR-to-issue associations
- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the
cross-reference is intentional (see step 5b)
## Version section template
@@ -410,8 +443,12 @@ if closed:
- <fix description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See
step 5b.
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
After all edits and cross-referencing are complete, generate a structured
@@ -440,9 +477,15 @@ There are exactly two types:
release, but the PR is being released elsewhere — the fix may not
actually ship here.
2. **PR is in the milestone, but the issue it closes is in a different
milestone (or has no milestone).** The PR is being released here, but
the issue it fixes is being released in a different version (or never
tracked in a milestone) — the changelog pairing is misleading.
milestone.** The PR is being released here, but the issue it fixes is
being released in a different version — the changelog pairing is
misleading.
**Exception — issue with no milestone is NOT an anomaly.** Milestones
are only required for issues tracked in the "Main" project. A milestone
PR that closes an issue with no milestone references an issue from
another (probably private) project; that is expected and the issue is
not part of this changelog. Do not report it.
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
@@ -598,6 +641,10 @@ for pr_num in sorted(changelog_prs):
if get_pr_milestone(pr_num) != MILESTONE: continue
for issue_num in pr.get('closing_issues', []):
issue_ms = get_issue_milestone(issue_num)
# No milestone = issue from another (probably private) project —
# milestones are only required for the "Main" project. Not an
# anomaly, and the issue never belongs in this changelog.
if issue_ms is None: continue
if issue_ms != MILESTONE:
anomalies_b.append({
'pr': pr_num,
@@ -620,7 +667,7 @@ with open(OUTPUT, 'w') as f:
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in different milestone or no milestone:** {n_b}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
# --- Anomalies section ---
@@ -649,7 +696,7 @@ with open(OUTPUT, 'w') as f:
f.write('\n')
if n_b:
f.write(f'\n### PR in {MILESTONE}, closing issue in different milestone or no milestone\n\n')
f.write(f'\n### PR in {MILESTONE}, closing issue in a different milestone\n\n')
by_pr = {}
for b in anomalies_b:
by_pr.setdefault(b['pr'], []).append(b)
@@ -684,8 +731,11 @@ milestone mismatches between issues and their referenced PRs:
1. **Issue in milestone, referenced PR in different milestone or no milestone**
the changelog claims a fix here, but the PR is released elsewhere.
2. **PR in milestone, closing issue in different milestone or no milestone**
2. **PR in milestone, closing issue in a different milestone**
the PR is released here, but the issue it fixes belongs to another version.
(An issue with *no* milestone belongs to another, probably private,
project — milestones are only required on the "Main" project — so it is
neither an anomaly nor a changelog candidate.)
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
@@ -732,6 +782,14 @@ self-contained and clickable in any Markdown viewer.
Taiga description text or by searching GitHub PRs that reference the Taiga
URL. Replace the Taiga reference with the GitHub issue link and add the PR
reference if applicable.
- **Security advisory (GHSA) entries.** Advisories fixed in the release are
listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or
PR link**, even though they are not in the milestone. The GHSA ID and
description come from the user — do **not** fetch or verify the URL, and do
not drop a draft (unpublished) advisory. Precedent:
`- Fix arbitrary file read security issue on create-font-variant rpc method
(https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`.
See step 5b.
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
before making edits, don't rely on cached data.
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
@@ -754,8 +812,11 @@ self-contained and clickable in any Markdown viewer.
- **Anomaly = milestone mismatch only.** The report contains only milestone
mismatches: (1) the issue is in this milestone but the referenced PR is
in a different milestone (or unassigned), and (2) the PR is in this
milestone but the issue it closes is in a different milestone (or
unassigned). These are anomalies because the changelog pairing is
milestone but the issue it closes is in a different milestone. An
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
an anomaly: milestones are required only for the "Main" project, so such
issues come from another (probably private) project and are not changelog
candidates. These anomalies are reported because the changelog pairing is
*misleading* — the human needs to decide whether the milestone or the
changelog is wrong. All other discrepancies (exclusion labels, missing
valid issues, unmerged PR references, duplicates, stale milestone
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+41
View File
@@ -0,0 +1,41 @@
def specs: [.. | objects | select(has("tests") and has("file"))];
def dur: [.tests[].results[]?.duration // 0] | add;
specs as $s
| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed
| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky
| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped
| ($s | length) as $total
| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu
| (if ($failed | length) > 0 then "❌"
elif ($flaky | length) > 0 then "⚠️"
else "✅" end) as $icon
| "## \($icon) Integration tests\n\n"
+ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n"
+ "|---|---|---|---|---|---|\n"
+ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) "
+ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n"
+ (if ($failed | length) > 0 then
"\n### Failed\n\n"
+ ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n"
else "" end)
+ (if ($flaky | length) > 0 then
"\n### Flaky (passed on retry)\n\n"
+ ($flaky
| map({ t: "`\(.file):\(.line)` — \(.title)",
r: ([.tests[].results[]? | select(.status == "failed")] | length) })
| sort_by(-.r)
| map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_")
| join("\n")) + "\n"
else "" end)
+ (if $total > 0 then
"\n<details><summary>Slowest specs</summary>\n\n"
+ ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) })
| sort_by(-.d) | .[0:5]
| map("- \(.t) — \(.d)s") | join("\n"))
+ "\n\n</details>\n"
else "" end)
+44
View File
@@ -0,0 +1,44 @@
name: _ADHOC
run-name: >-
_ADHOC (${{ inputs.gh_ref }}${{ inputs.nitrate_ref != '' && format(' / nitrate:{0}', inputs.nitrate_ref) || '' }})
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Branch/ref to build in penpot/penpot'
type: string
required: true
nitrate_ref:
description: 'Branch/ref to build admin-console in penpot/penpot-nitrate (defaults to gh_ref)'
type: string
required: false
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: ${{ inputs.gh_ref }}
force: ${{ inputs.force }}
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: ${{ inputs.gh_ref }}
force: ${{ inputs.force }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ inputs.nitrate_ref || inputs.gh_ref }}
force: ${{ inputs.force }}
+43 -5
View File
@@ -9,6 +9,11 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this version already exists in S3'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -16,20 +21,31 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this version already exists in S3'
type: boolean
required: false
default: false
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
# by the same caller into a single shared group, and left a manual dispatch of
# the same ref in a group of its own, free to race on the same artifacts.
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
group: build-bundle-${{ inputs.gh_ref }}
cancel-in-progress: true
jobs:
# ── 1. Decide whether there is anything to build ───────────────────────
check:
name: Check current bundle
runs-on: penpot-runner-01
runs-on: penpot-standar-runner
timeout-minutes: 10
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
sha: ${{ steps.vars.outputs.sha }}
commit_title: ${{ steps.vars.outputs.commit_title }}
exists: ${{ steps.check.outputs.exists }}
steps:
@@ -44,10 +60,12 @@ jobs:
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short=12 HEAD)" >> $GITHUB_OUTPUT
echo "commit_title=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
# The uploaded zip carries its version as S3 metadata. If the
# existing object was already built from this same commit, the
# whole build job is skipped.
# whole build job is skipped. `force` bypasses this check entirely.
- name: Check if this bundle is already built
id: check
env:
@@ -55,6 +73,16 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if [ "${{ inputs.force }}" = "true" ]; then
echo "exists=false" >> $GITHUB_OUTPUT
{
echo "### 🔁 Bundle build forced"
echo ""
echo "\`force: true\` — skipping the S3 version check."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
EXISTING_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
@@ -75,7 +103,7 @@ jobs:
# ── 2. Build and upload, only when needed ──────────────────────────────
build:
name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01
runs-on: penpot-standar-runner
timeout-minutes: 90
needs: check
if: needs.check.outputs.exists == 'false'
@@ -113,10 +141,20 @@ jobs:
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
- name: Write step summary
run: |
{
echo "### ✅ Bundle built"
echo ""
echo "- Version: \`${{ needs.check.outputs.bundle_version }}\` (\`git describe --tags --always\`)"
echo "- Commit: [\`${{ needs.check.outputs.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ needs.check.outputs.sha }}) — ${{ needs.check.outputs.commit_title }}"
echo "- Built at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
} >> "$GITHUB_STEP_SUMMARY"
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-01
runs-on: penpot-standar-runner
timeout-minutes: 5
needs: [check, build]
if: failure()
+17 -1
View File
@@ -1,16 +1,30 @@
name: _DEVELOP
run-name: >-
_DEVELOP (develop @ ${{ github.sha }})
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
schedule:
- cron: '16 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -18,9 +32,11 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
build-admin-console-docker:
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "develop"
force: ${{ inputs.force || false }}
@@ -13,6 +13,11 @@ on:
type: string
required: false
default: 'develop'
force:
description: 'Rebuild and overwrite even if already built'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -24,6 +29,11 @@ on:
type: string
required: false
default: 'develop'
force:
description: 'Rebuild and overwrite even if already built'
type: boolean
required: false
default: false
secrets:
ORG_WORKFLOW_TOKEN:
description: 'Token with Actions write access on penpot-nitrate'
@@ -47,6 +57,7 @@ jobs:
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
-f gh_ref="$GH_REF" \
-f force="${{ inputs.force }}" \
-f caller_run_id="$DISTINCT_ID" \
-f caller_run_url="$CALLER_URL"
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
steps:
- name: Set common environment variables
+56 -21
View File
@@ -8,6 +8,11 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this sha is already promoted'
type: boolean
required: false
default: false
workflow_call:
inputs:
gh_ref:
@@ -15,9 +20,18 @@ on:
type: string
required: true
default: 'develop'
force:
description: 'Rebuild and overwrite even if this sha is already promoted'
type: boolean
required: false
default: false
# Literal group name: under `workflow_call`, `github.workflow` resolves to the
# caller's workflow, which put this workflow and the other reusable one called
# by the same caller into a single shared group, and left a manual dispatch of
# the same ref in a group of its own, free to race on the same artifacts.
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
group: build-docker-${{ inputs.gh_ref }}
cancel-in-progress: true
env:
@@ -32,12 +46,13 @@ jobs:
# ── 1. Resolve the build key and check the whole set at once ───────────
prepare:
name: Prepare
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
timeout-minutes: 15
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
build_key: ${{ steps.vars.outputs.build_key }}
sha: ${{ steps.vars.outputs.sha }}
commit_title: ${{ steps.vars.outputs.commit_title }}
exists: ${{ steps.check.outputs.exists }}
steps:
@@ -55,6 +70,8 @@ jobs:
run: |
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse --short=12 HEAD)" >> $GITHUB_OUTPUT
echo "commit_title=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
BUNDLE_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
@@ -63,15 +80,11 @@ jobs:
--output text)
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
# Image content = bundle + docker build context, so the build key
# combines both.
CTX_HASH=$(git rev-parse "HEAD:docker/images" | cut -c1-12)
echo "build_key=${BUNDLE_VERSION}-${CTX_HASH}" >> $GITHUB_OUTPUT
# The image set is a single block, so a single set-level check is
# enough: `promote` drops a marker object in S3 only after every
# image was built AND every branch tag was moved. Marker present
# means there is nothing at all to do for this build key.
# means there is nothing at all to do for this commit. `force`
# bypasses this check entirely.
- name: Check if this image set is already built
id: check
env:
@@ -79,15 +92,30 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if [ "${{ inputs.force }}" = "true" ]; then
echo "exists=false" >> $GITHUB_OUTPUT
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.bundle_version }}.zip"
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
{
echo "### 🔁 Image set build forced"
echo ""
echo "\`force: true\` — skipping the S3 marker check."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "markers/images-${{ steps.vars.outputs.build_key }}" \
--key "markers/images-sha-${{ steps.vars.outputs.sha }}" \
> /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Image set build skipped"
echo ""
echo "The whole set was already built and promoted for \`${{ steps.vars.outputs.build_key }}\`."
echo "The whole set was already built and promoted for \`sha-${{ steps.vars.outputs.sha }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
@@ -97,7 +125,7 @@ jobs:
# prune stale bundles while at it.
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.build_key }}.zip"
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.bundle_version }}.zip"
if [ ! -f "$ZIP" ]; then
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
@@ -107,7 +135,7 @@ jobs:
# ── 2. One build per image, in parallel, only when needed ──────────────
build:
name: Build ${{ matrix.image }}
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
timeout-minutes: 60
needs: prepare
if: needs.prepare.outputs.exists == 'false'
@@ -138,7 +166,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the 429 Too Many Requests error when downloading
# To avoid the "429 Too Many Requests" error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
@@ -169,7 +197,7 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.build_key }}.zip"
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.bundle_version }}.zip"
if [ ! -f "$ZIP" ]; then
echo "Bundle not found in host cache; falling back to S3."
mkdir -p "$BUNDLE_CACHE"
@@ -209,7 +237,7 @@ jobs:
sbom: true
# Immutable tag only; branch tags are moved atomically for the
# whole image set by the `promote` job.
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }}
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:sha-${{ needs.prepare.outputs.sha }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
@@ -220,7 +248,7 @@ jobs:
# the S3 marker guarantees the branch tags were already moved.
promote:
name: Promote image set
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
timeout-minutes: 10
needs: [prepare, build]
@@ -245,7 +273,7 @@ jobs:
for image in $ALL_IMAGES; do
docker buildx imagetools create \
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
"${{ secrets.DOCKER_REGISTRY }}/$image:build-${{ needs.prepare.outputs.build_key }}"
"${{ secrets.DOCKER_REGISTRY }}/$image:sha-${{ needs.prepare.outputs.sha }}"
done
# The marker is written LAST: its presence certifies that all five
@@ -257,17 +285,24 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
echo "${{ github.run_id }}" | aws s3 cp - \
"s3://${{ secrets.S3_BUCKET }}/markers/images-${{ needs.prepare.outputs.build_key }}"
"s3://${{ secrets.S3_BUCKET }}/markers/images-sha-${{ needs.prepare.outputs.sha }}"
- name: Write step summary
run: |
{
echo "### ✅ Image set promoted"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`build-${{ needs.prepare.outputs.build_key }}\`."
echo "- Version: \`${{ needs.prepare.outputs.bundle_version }}\` (\`git describe --tags --always\`)"
echo "- Commit: [\`${{ needs.prepare.outputs.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ needs.prepare.outputs.sha }}) — ${{ needs.prepare.outputs.commit_title }}"
echo "- Built at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`sha-${{ needs.prepare.outputs.sha }}\`."
} >> "$GITHUB_STEP_SUMMARY"
# ── 4. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
timeout-minutes: 5
needs: [prepare, build, promote]
if: failure()
+17 -1
View File
@@ -1,16 +1,30 @@
name: _STAGING
run-name: >-
_STAGING (staging)
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted'
type: boolean
required: false
default: false
schedule:
- cron: '36 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -18,9 +32,11 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
build-admin-console-docker:
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "staging"
force: ${{ inputs.force || false }}
+30 -2
View File
@@ -1,17 +1,33 @@
name: _TAG
run-name: >-
_TAG (${{ github.ref_name }} @ ${{ github.sha }})
on:
workflow_dispatch:
inputs:
force:
description: 'Rebuild and overwrite even if already built/promoted (manual re-releases only)'
type: boolean
required: false
default: false
push:
tags:
- '*'
# Keyed by ref and never cancelling: pushing 2.17.2 shortly after 2.17.2-RC1
# must not abort the release already in flight.
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
build-docker:
needs: build-bundle
@@ -19,11 +35,21 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
build-docker-admin-console:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
force: ${{ inputs.force || false }}
notify:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
needs:
- build-docker
- build-docker-admin-console
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
@@ -37,7 +63,9 @@ jobs:
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs: build-docker
needs:
- build-docker
- build-docker-admin-console
uses: ./.github/workflows/release.yml
secrets: inherit
with:
+24
View File
@@ -0,0 +1,24 @@
name: _TMP TOKENS
on:
workflow_dispatch:
schedule:
- cron: '46 5-20 * * 1-5'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "hiru-tokens-in-libs"
+1 -1
View File
@@ -34,7 +34,7 @@ permissions:
jobs:
deploy:
runs-on: penpot-runner-01
runs-on: penpot-standar-runner
steps:
- name: Checkout
uses: actions/checkout@v6
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
test-backend:
if: ${{ !github.event.pull_request.draft }}
name: "Backend Tests"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-common:
if: ${{ !github.event.pull_request.draft }}
name: "Common Tests"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
test-frontend:
if: ${{ !github.event.pull_request.draft }}
name: "Frontend Tests"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+158 -32
View File
@@ -5,11 +5,37 @@ defaults:
shell: bash
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
shards:
description: 'Shard layout (JSON array)'
type: choice
required: true
default: '[1, 2, 3, 4]'
options:
- '[1, 2, 3, 4]'
- '[1, 2, 3, 4, 5, 6]'
- '[1, 2]'
- '[1]'
workers:
description: 'Playwright workers per shard'
type: string
required: true
default: '2'
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
types:
- opened
@@ -25,25 +51,41 @@ on:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
cancel-in-progress: true
jobs:
build-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
timeout-minutes: 30
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
outputs:
bundle_key: ${{ steps.vars.outputs.bundle_key }}
steps:
# An empty `ref` makes checkout fall back to its default (the PR merge
# ref on pull_request, the pushed ref on push).
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
# The cache key must come from the SHA actually checked out: on a manual
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
- name: Extract cache key
id: vars
run: |
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build Bundle
working-directory: ./frontend
@@ -53,58 +95,142 @@ jobs:
- name: Store Bundle Cache
uses: actions/cache@v5
with:
key: "integration-bundle-${{ github.sha }}"
key: ${{ steps.vars.outputs.bundle_key }}
path: frontend/resources/public
test-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests"
runs-on: penpot-runner-02
name: "Integration Tests (${{ matrix.shard }})"
runs-on: penpot-extended-runner
timeout-minutes: 40
needs: build-integration
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(inputs.shards || '[1, 2, 3, 4]') }}
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
- /var/cache/github-runner/ms-playwright:/ms-playwright
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-integration.outputs.bundle_key }}
path: frontend/resources/public
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
# No-op once the shared volume is warm; keeps the first run working.
- name: Install Playwright Chromium
working-directory: ./frontend
run: pnpm exec playwright install chromium
# `strategy.job-total` is the matrix size, so the shard denominator
# follows the `shards` input without being hardcoded.
- name: Run Tests
working-directory: ./frontend
env:
WORKERS: ${{ inputs.workers }}
run: |
WORKERS=${WORKERS:-2}
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
pnpm exec playwright test --project default \
--workers="$WORKERS" \
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
--reporter=blob
- name: Upload blob report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-blob-report-${{ matrix.shard }}
path: frontend/blob-report/
overwrite: true
retention-days: 3
- name: Upload test result
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-${{ matrix.shard }}
path: frontend/test-results/
overwrite: true
if-no-files-found: ignore
retention-days: 3
merge-reports:
if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
name: "Merge Integration Reports"
runs-on: penpot-extended-runner
timeout-minutes: 15
needs: test-integration
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
ref: ${{ inputs.gh_ref }}
- name: Run Tests
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
- name: Download blob reports
uses: actions/download-artifact@v7
with:
path: frontend/all-blob-reports
pattern: integration-blob-report-*
merge-multiple: true
- name: Merge into HTML report
working-directory: ./frontend
env:
PLAYWRIGHT_REPORTER: list,json
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
run: |
./scripts/test-e2e
pnpm exec playwright merge-reports \
--reporter=html,json,list ./all-blob-reports
- name: Flaky summary
- name: Test summary
if: always()
working-directory: ./frontend
run: |
if [ ! -f report.json ]; then
echo "No report.json produced (the run failed early)." >> "$GITHUB_STEP_SUMMARY"
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY"
jq -r '
[ .. | objects
| select(has("tests") and has("file"))
| select(any(.tests[]; .status == "flaky"))
| "- `\(.file):\(.line)` — \(.title)"
] as $f
| "## Flaky tests: \($f | length)\n"
+ (if ($f | length) == 0 then "_none_" else ($f | join("\n")) end)
' report.json >> "$GITHUB_STEP_SUMMARY"
# Kept for 30 days so flakiness rates can be aggregated across runs
# without scraping job logs.
- name: Upload JSON report
uses: actions/upload-artifact@v7
if: always()
@@ -112,13 +238,13 @@ jobs:
name: integration-json-report
path: frontend/report.json
overwrite: true
if-no-files-found: ignore
retention-days: 30
- name: Upload test result
- name: Upload HTML report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result
path: frontend/test-results/
name: integration-html-report
path: frontend/playwright-report/
overwrite: true
retention-days: 3
retention-days: 7
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
test-library:
if: ${{ !github.event.pull_request.draft }}
name: "Library Tests"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
test-mcp:
if: ${{ !github.event.pull_request.draft }}
name: "Test MCP"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container: penpotapp/devenv:latest
steps:
+2 -2
View File
@@ -53,7 +53,7 @@ jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
@@ -95,7 +95,7 @@ jobs:
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-runner-02
# runs-on: penpot-extended-runner
# container:
# image: penpotapp/devenv:latest
#
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-plugins:
if: ${{ !github.event.pull_request.draft }}
name: Plugins Runtime Linter & Tests
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
test-render-wasm:
if: ${{ !github.event.pull_request.draft }}
name: "Render WASM Tests"
runs-on: penpot-runner-02
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
+5 -1
View File
@@ -24,6 +24,7 @@ opencode.json
!AGENTS.md
!CODE_OF_CONDUCT.md
!SECURITY.md
!HIGHLIGHTS.md
/*.png
/*.svg
/*.sql
@@ -58,6 +59,8 @@ opencode.json
/docker/images/bundle*
/exporter/target
/exporter/.shadow-cljs
/exporter/resources/wasm/
/exporter/src/app/wasm/shared.js
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
@@ -96,10 +99,11 @@ opencode.json
/.idea
*.iml
/.claude
/CLAUDE.md
/.playwright-mcp
/.devenv/mcp/
/opencode.json
/.opencode/plans
/.agents/plans
/.opencode/reports
/.opencode/prompts
/.ci-logs
+10
View File
@@ -0,0 +1,10 @@
---
description: Create a PR for the current task branch or update an existing one — loads and follows the create-pr skill
agent: build
---
Load the **`create-pr`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
+4 -36
View File
@@ -1,42 +1,10 @@
---
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
description: Execute a ready plan — task checklist, your confirmation, then all tasks with one commit (default) or step by step with a commit and a pause per task; creates issue + branch when on a base branch — loads and follows the implement-plan skill
agent: build
---
# Implement Plan
Load the **`implement-plan`** skill and follow it as your only instruction.
This command is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context — it does not take
extra arguments. Follow these steps in order.
## User input, overrides and additional context
## 1. Create the issue
Use the **`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body from the
plan. Capture the new issue's number — call it **NNNN** (needed for the branch
name and the commit reference).
## 2. Create the branch
Create and switch to a branch named after the issue:
```
git checkout -b issue-NNNN
```
(Replace NNNN with the issue number from step 1.)
## 3. Execute the plan
Implement the prepared plan from the session context. Work methodically, keeping
changes focused on what the issue requires. Do not commit — the commit happens in
step 4.
## 4. Commit with the create-commit skill
After the implementation is complete, load the **`create-commit`** skill and
follow its workflow to commit the changes. Provide a brief summary of what was
implemented and why, the issue reference (`issue-NNNN`), and the model name you
are running as so the `AI-assisted-by` trailer is set correctly.
Do not push. Pushing is handled separately by the user.
$ARGUMENTS
+10
View File
@@ -0,0 +1,10 @@
---
description: Investigate the chosen task, produce an implementation plan, and save it — loads and follows the make-a-plan skill
agent: build
---
Load the **`make-a-plan`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
+2 -36
View File
@@ -1,40 +1,6 @@
---
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
description: Resolve local git conflicts and stage the resolved files; never continues the rebase — loads and follows the resolve-git-conflicts skill
agent: build
---
# Fix Git Conflicts
Resolve conflicts in the local repository. The user handles finishing the
rebase themselves — you must **never** run `git rebase --continue`,
`git rebase --skip`, `git merge --continue`, or anything similar.
## Phase 1 — Understand the problem (read-only)
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
- Identify what each side changed and why, and how they should be combined.
## Phase 2 — Present the resolution plan
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
- What each side changed and why.
- Your proposed resolution and the reasoning behind it.
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
## Phase 3 — Execute
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
## Phase 4 — Stage and verify
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
## Phase 5 — Report
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
Load the **`resolve-git-conflicts`** skill and follow it as your only instruction.
+10
View File
@@ -0,0 +1,10 @@
---
description: Code review — review a diff, PR, or code change — loads and follows the review-code skill
agent: build
---
Load the **`review-code`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
+10
View File
@@ -0,0 +1,10 @@
---
description: Plan review — evaluate an implementation plan before executing it — loads and follows the review-plan skill
agent: build
---
Load the **`review-plan`** skill and follow it as your only instruction.
## User input, overrides and additional context
$ARGUMENTS
-25
View File
@@ -1,25 +0,0 @@
Act as a senior software engineer and perform a thorough review.
## Instructions
1. **Determine what is being reviewed** from the provided context:
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
4. Follow the loaded skill's process and produce its output format.
## Strong Rules
1. Do not invent problems. Every finding must be real and actionable.
2. Do not modify any code and do not create a commit — this command only reviews.
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
## Context
$ARGUMENTS
-39
View File
@@ -1,39 +0,0 @@
---
name: create-pr
description: Create or update a GitHub PR following Penpot conventions.
---
# Skill: create-pr
Create or update a GitHub PR. Read and follow:
- `mem:workflow/creating-prs` — title format, description structure, writing principles
- `mem:workflow/creating-commits` — commit type emojis
## When to Use
- Creating a new PR from a feature branch
- Updating an existing PR's title or description to match conventions
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
## Commands
**Create:**
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Update:**
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Verify:**
```bash
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
-271
View File
@@ -1,271 +0,0 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans that engineers or other agents can execute. Never writes
or modifies code.
## When to Use
- The user asks for a plan, design, or analysis of a feature or bug.
- The user wants to understand which parts of the codebase a task will touch.
- The user needs a step-by-step implementation plan with file paths, function
names, and test strategy.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
breakdown.
Do **not** use this skill to actually implement anything — it is read-only.
## Role
You are a Senior Software Architect working on Penpot, an open-source design
tool. Your sole responsibility is planning and analysis — you do NOT write or
modify code.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
everything they need to know: which files to touch for each task, code patterns,
tests, and how to verify correctness. Apply DRY and KISS principles.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
## Required Reading Before Planning
Before drafting any plan, work through the project's own guidance:
1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
that describes the monorepo structure and module dependency graph.
2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
4. For each affected module, note its lint, format, and test commands so the
plan can include concrete verification steps.
Skipping this step is the #1 cause of incorrect or incomplete plans.
## The Planning Process
### Phase 1: Architecture Analysis
1. Read the spec, requirements, or feature request.
2. Analyze the codebase architecture and identify affected modules.
3. Read project conventions (starting with `critical-info` and module core
memories) before drafting.
4. Map dependencies between components (see the dependency graph in
`critical-info`).
5. Identify risks, edge cases, performance implications, and breaking changes.
### Phase 2: Task Breakdown
Implementation order follows the monorepo's dependency graph:
`frontend -> common`, `backend -> common`, `exporter -> common`,
`frontend -> render-wasm`. Build shared foundations first, then layer
consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
Task 3: frontend UI component + API integration
```
Each vertical slice delivers working, testable functionality.
#### Write Tasks
Each task follows this structure:
```markdown
## Task [N]: [Short descriptive title]
**Description:** One paragraph explaining what this task accomplishes.
**Acceptance criteria:**
- [ ] [Specific, testable condition]
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Tests pass (module-specific test command)
- [ ] Lint/formatter passes (module-specific check command)
**Dependencies:** [Task numbers this depends on, or "None"]
**Files likely touched:**
- `path/to/file.clj`
- `path/to/file_test.clj`
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
or the commands noted in the module's core memory).
#### Estimate Scope
| Size | Files | Scope |
|------|-------|-------|
| **XS** | 1 | Single function, config change, or schema tweak |
| **S** | 1-2 | One handler or component method |
| **M** | 3-5 | One vertical feature slice |
| **L** | 5-8 | Multi-component feature |
| **XL** | 8+ | **Too large — break it down further** |
If a task is L or larger, break it into smaller tasks. Agents perform best on
S and M tasks.
**When to break a task down further:**
- It would take more than one focused session
- You cannot describe the acceptance criteria in 3 or fewer bullet points
- It touches two or more independent subsystems
- You find yourself writing "and" in the task title (a sign it is two tasks)
#### Order and Checkpoints
Arrange tasks so that:
1. Dependencies are satisfied (build foundation first)
2. Each task leaves the system in a working state
3. Verification checkpoints occur after every 2-3 tasks
4. High-risk tasks are early (fail fast)
Add explicit checkpoints with the relevant module commands:
```markdown
## Checkpoint: After Tasks 1-3
- [ ] All tests pass (module-specific command)
- [ ] Lint/format passes (module-specific command)
- [ ] Core flow works end-to-end
- [ ] Review with human before proceeding
```
## Requirements
- Analyze the codebase architecture and identify affected modules.
- Read project conventions before drafting (start with `critical-info` and
affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist between major phases.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code.
- The only file write you may attempt is the plan itself, saved to
`.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches.
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill.
Additionally, save the plan to:
```
.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
`.opencode/plans/` directory if it does not exist.
Always attempt the write. If the user explicitly provides a target file path,
use that path instead of the default.
### Plan Document Template
```markdown
# Plan: [Feature/Project Name]
## Context
[One paragraph: what is the problem or feature request? Why is it needed?]
## Affected Modules
[Which modules of the monorepo are involved? Reference module paths and any
`mem:` memories that were consulted.]
## Architecture Decisions
- [Key decision 1 and rationale]
- [Key decision 2 and rationale]
## Risks & Considerations
[Edge cases, performance implications, breaking changes, migration concerns,
security implications.]
## Approach
[Step-by-step implementation plan with file paths, function names, and code
shape where applicable. Group steps into atomic, ordered tasks.]
## Task List
### Phase 1: Foundation
- [ ] Task 1: ...
- [ ] Task 2: ...
### Checkpoint: Phase 1
- [ ] Tests pass, lint/formatter clean (module-specific commands)
### Phase 2: Core Features
- [ ] Task 3: ...
- [ ] Task 4: ...
### Checkpoint: Phase 2
- [ ] End-to-end flow works
### Phase 3: Polish
- [ ] Task 5: ...
- [ ] Task 6: ...
### Checkpoint: Complete
- [ ] All acceptance criteria met
- [ ] Ready for review
## Testing Strategy
[How to verify: which test commands to run per module, what cases to cover,
manual verification steps, lint/format checks. Consult each module's core
memory for the exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
## Open Questions
- [Question needing human input]
```
When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
## Verification Checklist
Before starting implementation, confirm:
- [ ] Every task has acceptance criteria
- [ ] Every task has a verification step
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task touches more than ~5 files
- [ ] Checkpoints exist between major phases
- [ ] The human has reviewed and approved the plan
+3 -6
View File
@@ -7,6 +7,7 @@ Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; m
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
- Storage abstraction, logical buckets, object lifecycle, deduplication, access, and garbage collection: `mem:backend/storage`.
- HTTP sessions, config, media processing, and file data persistence: `mem:backend/http-storage-filedata-subtleties`.
- Embedded Ladybug graph experiment, projection, incremental sync, console, and risks: `mem:backend/graph-experiment`
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
@@ -102,9 +103,5 @@ misleading linter/compiler output. See `mem:scripts/paren-repair`.
## Testing
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. JVM tests are invoked directly via `clojure -M:dev:test` — there is no pnpm wrapper. If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`. See `mem:testing` for execution discipline.
* **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
Backend test commands, coverage rules, and conventions: `mem:backend/testing`.
Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
@@ -0,0 +1,632 @@
# Graph Experiment
## Scope
- Purpose: project Penpot file data into an embedded Ladybug graph database.
- Purpose: keep the graph current with Penpot file changes.
- Purpose: expose a read-only graph console for backend debugging.
- This is an experiment, not a replacement for PostgreSQL file storage.
- The graph subsystem is off unless `:graph` is in the backend flags.
- The main Penpot frontend has no graph feature code for this subsystem.
- The graph console is a backend-served HTML template with JavaScript.
## Memory Links
- Read `mem:backend/core` for backend architecture, HTTP routes, DB rules, and test commands.
- Read `mem:backend/rpc-db-worker-subtleties` for RPC and message bus behavior.
- Read `mem:backend/http-storage-filedata-subtleties` for file data loading and realization.
- Read `mem:common/changes-architecture` for the change record vocabulary.
- Read `mem:frontend/routing-app-shell-subtleties` for the existing notification WebSocket.
- Read `mem:prod-infra/core` for Redis or Valkey message bus topology.
## Branch Surface
- The graph experiment adds about 6,336 lines and changes about 27 files.
- The graph implementation lives under `backend/src/app/graph/`.
- The graph console lives at `backend/resources/app/templates/graph-console.tmpl`.
- The existing debug page gains graph links in `backend/resources/app/templates/debug.tmpl`.
- The existing debug HTTP routes gain graph handlers in `backend/src/app/http/debug.clj`.
- The backend system passes the message bus to the debug route component in `backend/src/app/main.clj`.
- The backend adds Ladybug and Arrow dependencies in `backend/deps.edn`.
- The backend adds JVM options for Ladybug and Arrow native access.
- The common flag registry adds `:graph` in `common/src/app/common/flags.cljc`.
- The graph experiment adds `graph_sync_parity_test.clj` and `graph_binder_gate_test.clj`.
## System Model
### Storage layers
- PostgreSQL remains the source of truth for Penpot files.
- The graph database stores a projection of one file.
- A persistent graph uses a `.lbug` path under `PENPOT_GRAPH_DIR`.
- The default graph directory is `/tmp/penpot-graph`.
- A debug session uses a Ladybug `:memory:` database.
- A debug session database lives inside the backend JVM process.
- A debug session does not survive a backend restart.
- A debug session does not store file data back to PostgreSQL.
### Two graph update paths
- Cold projection reads the complete file and rebuilds the graph.
- Incremental sync reads file change records and updates the open graph.
- Both paths must produce the same graph for the same file state.
- The parity test treats cold projection as the reference path.
- A reload discards the session graph and uses cold projection again.
## Main Namespaces
### `app.graph.ladybug`
- Opens and closes Ladybug `Database` and `Connection` objects.
- Installs and loads the Ladybug JSON extension.
- Executes Cypher statements.
- Executes prepared statements.
- Binds scalar parameters.
- Formats UUID, string, integer, number, JSON, and timestamp values.
- Formats compound values such as arrays, maps, and structs.
- Converts Ladybug values back to Clojure values.
- Limits normal query results to 200 rows by default.
- Detects result truncation with `:truncated?`.
- Uses query timeout `0` by default.
- Query timeout `0` disables the timeout.
- Provides `validate-on-connection!` for parse, bind, and read-only checks.
- `exec-prepared-on-connection!` prepares every statement before the first execution.
- A prepare failure stops the batch before a mutation runs.
### `app.graph.schema`
- Provides the public schema facade.
- Exposes schema version `penpot-graph-slice-4`.
- Delegates node and relationship definitions to `app.graph.schema.nodes`.
### `app.graph.schema.nodes`
- Holds the single registry for graph node tables.
- Generates node DDL.
- Generates relationship DDL.
- Maps Penpot shape types to graph tables.
- Projects source attributes into graph attributes.
- Formats graph column values.
- Quotes reserved graph labels such as `Group` and `Boolean`.
- Defines container tables and shape tables.
- Defines `IsChildOf`, `IsInstanceOf`, `RefersTo`, and `FillsSwapSlot`.
### `app.graph.schema.contract`
- Records deliberate graph contract decisions.
- Renames graph columns such as `:revn` to `revision`.
- Drops attributes that do not belong in this graph slice.
- Records attributes that the graph does not project.
- Applies per-table dropped attributes.
- Defines type overrides for vectors, transforms, colors, maps, and JSON arrays.
- Maps selected map keys to the frontend JSON naming convention.
- `:background-blur` remains a declared unprojected attribute.
### `app.graph.schema.projection`
- Derives projected schemas from canonical Malli schemas.
- Builds the projected document schema.
- Builds projected shape schemas.
- Selects the schema for each shape type.
### `app.graph.schema.types`
- Maps Malli types to Ladybug types.
- Maps matrices to `DOUBLE[6]`.
- Maps points to `DOUBLE[2]`.
- Maps rectangles to `DOUBLE[4]`.
- Maps colors to `UINT32`.
- Maps collections to Ladybug arrays.
- Maps `:map-of` schemas to `MAP`.
- Maps closed scalar maps to `STRUCT`.
- Maps other complex values to `JSON`.
### `app.graph.schema.values`
- Coerces source values to graph column values.
- Writes fixed vectors with deterministic order.
- Packs colors into the graph color representation.
- Sorts set values when deterministic output is needed.
### `app.graph.arrow`
- Loads projection rows with Apache Arrow.
- Creates temporary staged node and relationship tables.
- Uses `COPY ... FROM (MATCH ...)` for bulk loading.
- Groups relationship loads by source and target table pair.
- Resolves relationship endpoints with joins.
- Does not use `createArrowRelTable` for UUID relationship endpoints.
- Keeps the Arrow `RootAllocator` alive until Ladybug releases staged buffers.
- Closes the allocator after the connection and database close sequence.
### `app.graph.ingest`
- Fetches a complete file with `bfc/get-file` and `:realize? true`.
- Rejects missing files.
- Rejects files without file data.
- Can run file data validation before projection.
- Creates the DDL.
- Loads nodes and edges through Arrow.
- Executes post-load transforms.
- Writes graph metadata last.
- Treats the final metadata write as the complete-build marker.
- Supports a persistent database path and an open connection.
### `app.graph.projection.document`
- Projects `Document`, `Page`, `Component`, and supported shape nodes.
- Skips the page root frame.
- Creates `IsChildOf` edges from shapes to parents.
- Creates page edges to the document.
- Creates component edges to the document.
- Stores page order in `Page.index` and edge `position`.
- Reverses the stored `:shapes` list for Penpot z-order.
- Adds `page-id` to every projected shape.
- Propagates an instance head `component-id` to descendants.
- Stops component inheritance at a non-Frame shape with its own component ID.
- Skips deleted components during cold projection.
- Logs unsupported shape types and missing shape records.
### `app.graph.projection.transforms`
- Runs after the base nodes and edges load.
- `link-component-instances` creates `IsInstanceOf` edges.
- A Frame needs `component-file` to qualify as an instance head.
- `link-shape-refs` creates `RefersTo` edges from `shape-ref`.
- Ladybug limits multi-label relationship `MERGE` statements.
- The transform emits one statement for each shape-table pair.
- `link-swap-slots` creates `FillsSwapSlot` edges.
- Swap slot IDs come from `swap-slot-<uuid>` entries in `touched`.
- The transform removes swap slot entries from `touched` after edge creation.
- The transform order matters because it reads and then changes `touched`.
### `app.graph.meta`
- Stores graph provenance in `GraphMeta`.
- Stores schema version, source revision, producer, and build time.
- The source revision identifies the file revision used for cold projection.
### `app.graph.stats` and `app.graph.report`
- `app.graph.stats` counts graph nodes and relationships from the live catalog.
- `app.graph.report` prints ingest information for REPL use.
## Cold Projection Flow
1. Get the file row and realized file data from PostgreSQL.
2. Read the file revision from the file row.
3. Build the node and edge projection.
4. Create all graph tables from the graph schema.
5. Load node rows with Arrow.
6. Load relationship rows with Arrow.
7. Run `CHECKPOINT;`.
8. Run the registered derived transforms.
9. Write `GraphMeta` as the final build step.
10. Return file ID, file revision, database path, projection stats, and transform stats.
### Projection node groups
- `Document` contains file-level attributes without the file data blob.
- `Document.options` receives file-level options from the data blob.
- `Page` contains page attributes without the page object map.
- `Component` contains component attributes without component object maps.
- Shape tables contain the supported shape attributes.
- The graph stores selected derived attributes such as `page-id`.
### Projection relationship groups
- Structural edges use `IsChildOf`.
- Page and component edges point to `Document`.
- Derived edges come from the post-load transform registry.
## Incremental Sync
### Change source
- `app.rpc.commands.files-update` persists the file update first.
- The same command publishes a `:file-change` message to the file topic.
- The topic key is the file UUID.
- The message contains the file ID, profile ID, session ID, revision, version, and changes.
- Library changes also publish a team-topic message.
- The graph session only consumes the file-topic `:file-change` messages.
### Session subscription
- `app.graph.debug/start-sync-loop!` creates a channel with a dropping buffer of 64.
- The session subscribes the channel to the file UUID topic.
- The loop reads one message at a time.
- The loop ignores message types other than `:file-change`.
- The loop stops when the channel closes.
- `destroy-session!` closes the channel and purges its message bus subscription.
### Session state
- Sessions are stored in a global `defonce` atom.
- The map key is the string form of `profile-id`.
- One profile has one graph session.
- Loading another file first destroys the old session.
- A session stores the Ladybug database and connection.
- A session stores a shared lock for graph access.
- A session stores file metadata.
- A session stores the incremental sync index.
- A session stores the message bus channel.
- A session stores load time and profile ID.
- The session keeps projection statistics but drops full projection rows after index creation.
### Sync index
- `build-index` starts from the complete cold projection.
- The index stores the graph file ID and document ID.
- The index stores the current graph revision.
- The index stores page IDs, names, and positions.
- The index stores component IDs, names, and deleted state.
- The index stores shape table, parent, position, frame, page, and component context.
- The index stores child IDs by parent ID.
- The index supports later change application without another PostgreSQL file read.
### Change application
- `apply-changes!` processes the change list in source order.
- Each supported change returns a new index and a list of Cypher statements.
- Unsupported changes enter the `:skipped` result.
- Supported changes enter the `:applied` result.
- The function collects all statements before it executes them.
- The function appends a document revision statement when at least one change applies.
- The index revision advances only when at least one change applies.
- A larger incoming revision than the index revision creates a warning.
- A revision gap does not trigger catch-up.
### Shape change rules
- `:add-obj` reuses `projection.document/denormalized-shape`.
- `:add-obj` creates the shape node and its parent edge.
- `:mod-obj` applies supported `:set` operations to graph columns.
- `:mod-obj` keeps false and zero values as values.
- `:del-obj` deletes shapes in deep post-order.
- `:mov-objects` detaches shapes from the old parent.
- `:mov-objects` closes the old sibling position gap.
- `:mov-objects` inserts shapes at the new position.
- `:mov-objects` updates `parent_id` and `frame_id`.
- `:mov-objects` rewrites container `shapes` values.
- The parent columns and child lists must match a cold projection.
### Page and component change rules
- Page add creates a projected page node and a document edge.
- Page delete removes the page subtree.
- Page modification updates supported page attributes.
- Component add creates a component node and document edge.
- Component modification updates supported component attributes.
- Component delete uses a soft-delete state.
- Component restore removes the soft-delete state.
- Component purge removes the component node and document edge.
- Component sync paths need more parity coverage than the current tests provide.
## Session Locking
- The sync loop and HTTP handlers share one lock per session.
- The lock protects one Ladybug connection from concurrent access.
- Queries acquire the lock before binder validation and execution.
- Graph data export acquires the lock before catalog reads.
- Session export acquires the lock before `EXPORT DATABASE`.
- A long query blocks sync for the same session.
- A sync batch blocks queries for the same session.
- Ladybug connection thread safety is not assumed.
## Graph Query Rules
- The console accepts Cypher text.
- Blank query text raises a validation error.
- The query first passes Ladybug prepare and bind checks.
- The query must pass the engine read-only analysis.
- A mutating query is rejected.
- The graph console does not provide a write path.
- A session graph is rebuilt from the file by Reload.
- Normal query results have a 200-row limit.
- Query results use string values for the HTML console representation.
- JSON requests receive a Transit JSON response with the query and result.
- HTML requests receive the rendered console with the result.
## Graph Data Export
### G6 data
- `/dbg/actions/graph-data` reads the live Ladybug database.
- It does not read the sync index for nodes and edges.
- It therefore shows database drift if a batch fails after index update.
- Node export covers all registered node tables.
- Relationship export reads the Ladybug relationship catalog.
- Relationship export includes source, target, relationship name, and position.
- Node and relationship export uses a 100,000-row limit.
- The response reports `truncated` when a limit cuts the result.
- The response reports buffer-manager memory usage.
### `.lbug` export
- `source=file` rebuilds the persistent graph from PostgreSQL file data.
- `source=file` runs a synchronous full ingest for each request.
- `source=session` exports the caller profile's live in-memory graph.
- Session export uses Ladybug `EXPORT DATABASE` to Parquet files.
- Session export creates a new `.lbug` database with `IMPORT DATABASE`.
- The temporary Parquet staging directory is deleted after import.
- The final session `.lbug` file remains in the system temporary directory.
- The HTTP response streams the database file to the caller.
## HTTP Routes and Access
- The graph routes live in `backend/src/app/http/debug.clj`.
- The graph route list is added only when `:graph` is enabled.
- `/dbg/graph` serves the graph console page.
- `/dbg/actions/graph-files` returns the profile file tree.
- `/dbg/actions/graph-load` loads a file into the profile session.
- `/dbg/actions/graph-unload` closes the profile session.
- `/dbg/actions/graph-reload` rebuilds the loaded file graph.
- `/dbg/actions/graph-query` runs a read-only Cypher query.
- `/dbg/actions/graph-sync-status` returns the sync state.
- `/dbg/actions/graph-data` returns nodes and edges for G6.
- `/dbg/actions/graph-export` streams a `.lbug` database.
- The `/dbg` session middleware remains active.
- The `/dbg` admin middleware remains active.
- A devenv host with a profile ID passes the debug authorization rule.
- Other hosts need a profile email in the configured admin set.
- `/dbg/actions/graph-files` lists reachable teams, projects, and files.
- The file tree query has a 500-file limit.
- The graph handlers resolve graph namespaces at call time.
- The backend requires `app.graph.debug` and `app.graph.ingest` when the flag is on.
- Ladybug native loading then fails during route initialization instead of first use.
## Console Frontend
### Page type
- `graph-console.tmpl` is a backend resource template.
- It is not a Rumext component.
- It is not part of the main frontend route table.
- The page uses browser `fetch` calls and a browser WebSocket.
- The page loads G6 version `5.1.1` from jsDelivr.
### File tree
- The page fetches `/dbg/actions/graph-files`.
- The response contains team, project, and file groups.
- The page creates the tree with DOM APIs.
- A file click submits the graph load form.
- The page shows a message when no file exists.
### Graph rendering
- The page fetches `/dbg/actions/graph-data`.
- The page converts graph nodes and edges to G6 data.
- The page skips repaint when the node and edge signature does not change.
- The page marks added, removed, and changed graph entities.
- The page supports tree, dagre, circular, force, and combo layouts.
- The page supports collapsed container combos.
- The page has render guards at 4,000 nodes and 8,000 edges.
- The `?safe` query option bypasses the render guard.
- The page shows graph size by node count and relationship count.
- The page shows buffer-manager memory in MiB.
- The page reports a CDN failure when G6 is undefined.
### Query result filtering
- A query can return `filter_*` columns with node IDs.
- The HTML result table hides columns with the `filter_` prefix.
- The JSON result keeps the full result.
- The graph view uses the hidden IDs to select matching nodes.
- The graph view re-runs the query after graph refresh.
- This keeps the query filter aligned with the current graph.
- A user column named `filter_*` follows the same hiding rule.
### Node inspector
- A node click creates a query for that node.
- The inspector calls `/dbg/actions/graph-query` with JSON negotiation.
- The inspector displays the full projected row.
- The inspector uses table and ID values from the graph data.
## WebSocket Data Flow
1. The page opens `/ws/notifications` with a random `session-id` query value.
2. The page sends `:subscribe-file` with a Transit UUID value.
3. The server makes sure that the file exists and that the profile has read permission.
4. The server subscribes the connection to the file topic.
5. `files_update` publishes `:file-change` to the same topic.
6. The graph session consumes the message from its message bus subscription.
7. The WebSocket server sends the message to the browser connection.
8. The browser adds the change to the changelog.
9. The browser fetches sync status after 150 milliseconds.
10. The browser fetches graph data after a 400-millisecond debounce.
11. The browser repaints the G6 graph when the graph data changes.
### WebSocket reconnect behavior
- The page reconnects after three seconds when the socket closes.
- The page resubscribes to the file after the socket opens.
- The page refreshes sync status after reconnect.
- The page refreshes graph data after reconnect.
- Reconnect does not recover dropped message-bus changes.
- The page shows the sync error or skipped-change state when the status reports it.
## Feature Flag and Runtime Dependencies
- `:graph` is defined in `common/src/app/common/flags.cljc`.
- The flag is off by default.
- `com.ladybugdb/lbug` version `0.19.1` is a backend dependency.
- `org.apache.arrow/arrow-memory-netty` version `18.2.0` supports Arrow `RootAllocator`.
- The JVM uses `--enable-native-access=ALL-UNNAMED`.
- The JVM uses `--add-opens=java.base/java.nio=ALL-UNNAMED`.
- The JVM uses `--sun-misc-unsafe-memory-access=allow`.
- The JVM options appear in the development alias and backend launch scripts.
- A Ladybug version change needs new binder and parity tests.
- A JDK version change needs a startup test with the graph flag enabled.
## Tests
### `backend-tests.graph-sync-parity-test`
- Uses two Ladybug `:memory:` databases.
- Does not use PostgreSQL or a live graph session.
- Projects initial file data into database A.
- Applies changes to database A through incremental sync.
- Applies the same changes to file data.
- Projects the changed file data into database B.
- Compares every node row and relationship row.
- Reports differences by table, row key, and column.
- Covers shape add, shape modification, shape deletion, movement, and page changes.
- Contains a test that injects a sync defect and expects a graph difference.
- Does not cover all component change variants.
- Does not cover every movement insertion mode.
### `backend-tests.graph-binder-gate-test`
- Creates the live graph DDL in a Ladybug `:memory:` database.
- Prepares each sync statement template without executing it.
- Detects parse errors and missing tables.
- Detects missing columns and bad label quoting.
- Reports the expected read-only classification.
- Covers reserved node labels across the node registry.
- Reports an error result for an invalid statement.
### Test gaps
- No automated HTTP handler tests cover graph routes.
- No automated session lifecycle tests cover load and unload.
- No automated WebSocket tests cover graph subscription.
- No automated export tests cover persistent and session sources.
- Component add, modify, delete, restore, and purge need parity tests.
- Page delete needs parity coverage.
- Movement with `:after-shape` needs parity coverage.
- Buffer overflow and revision gap behavior need tests.
- Partial batch failure and recovery need tests.
- Query timeout and long-query behavior need tests.
## Known Risks and Limits
### Dropped changes
- The sync channel uses a dropping buffer of 64.
- A burst can discard file-change messages.
- The sync loop logs a revision gap when it sees a larger revision.
- The sync loop does not fetch missing rows from `file_change`.
- Reload is the only built-in recovery path.
### Partial batch state
- `apply-changes!` does not provide Ladybug transaction atomicity.
- A statement failure can leave a partly changed graph.
- The in-memory index can advance before the database state is complete.
- `/dbg/actions/graph-data` reads the database and exposes this drift.
- Reload rebuilds the graph from PostgreSQL file data.
### Query resource use
- The default session query timeout is zero.
- A costly query can hold the session lock for a long time.
- The same lock blocks incremental sync.
- The graph export also holds the same lock during catalog reads.
- The graph schema has a high memory floor.
- The console reports about 115 MiB for the wide slice before file data.
### Session lifecycle
- Sessions have no TTL.
- Sessions remain until unload, replacement, or process shutdown.
- Each session owns native Ladybug memory.
- Many profiles can create many native databases.
- A profile load replaces its previous session.
- Two browser tabs for one profile share one graph session.
### Temporary files
- Session export leaves the final `.lbug` file in the system temporary directory.
- Long-lived servers can accumulate exported session databases.
- The staging directory is deleted after import.
### Browser dependency
- The graph view depends on a runtime CDN request.
- A network restriction can remove the G6 view.
- Queries and session status still use backend endpoints without G6.
### Data exposure
- The graph console can list many files available to the profile.
- The console can load complete projected file data.
- The console can export a graph database.
- The console can inspect all projected node attributes.
- The console is safe only when the `/dbg` access boundary is correct.
- The graph flag must remain off for deployments that do not need this tool.
### Contract drift
- The graph schema is a deliberate slice of the Penpot file model.
- New source attributes do not enter the graph automatically in all cases.
- Dropped and unprojected attributes need an explicit contract decision.
- `applied_tokens` key mapping depends on the JSON naming function.
- `filter_*` is a frontend convention, not a graph schema guarantee.
### Ladybug dialect coupling
- Cypher strings contain Ladybug-specific syntax.
- Label quoting handles reserved labels explicitly.
- Relationship transforms depend on Ladybug relationship limits.
- Arrow loading depends on Ladybug `COPY FROM (MATCH ...)` behavior.
- A dependency upgrade needs schema, binder, Arrow, and parity checks.
## REPL Helpers
- `app.srepl.main` resolves graph functions only when a helper runs.
- `graph-smoke-test!` runs a basic Ladybug operation.
- `graph-query-test!` runs a graph query test.
- `ingest-file-to-graph!` projects a file into a graph database.
- These helpers use `requiring-resolve` to keep the graph dependency lazy.
## Operational Invariants
- PostgreSQL file data remains authoritative.
- Cold projection and incremental sync must produce equal graph state.
- The graph revision must identify the last applied file revision.
- The document revision must update when a sync batch applies.
- A missing or skipped change must remain visible in sync status.
- A graph query from the console must be read-only.
- A graph session must serialize connection access.
- Graph routes must remain behind the `:graph` flag and `/dbg` access control.
- The Arrow allocator must outlive all Ladybug operations that use its buffers.
- `GraphMeta` must be written after the full ingest and transforms finish.
## Key Files
- `backend/src/app/graph/ladybug.clj`: Ladybug API and query gates.
- `backend/src/app/graph/arrow.clj`: Arrow bulk load.
- `backend/src/app/graph/ingest.clj`: Complete file ingest.
- `backend/src/app/graph/debug.clj`: Session lifecycle, sync loop, query, and export.
- `backend/src/app/graph/sync.clj`: Incremental change application.
- `backend/src/app/graph/meta.clj`: Graph provenance.
- `backend/src/app/graph/stats.clj`: Graph counts.
- `backend/src/app/graph/report.clj`: REPL ingest report.
- `backend/src/app/graph/projection/document.clj`: Base document projection.
- `backend/src/app/graph/projection/transforms.clj`: Derived relationship transforms.
- `backend/src/app/graph/schema/nodes.clj`: Node and relationship registry.
- `backend/src/app/graph/schema/contract.clj`: Projection contract decisions.
- `backend/src/app/graph/schema/projection.clj`: Malli projection schemas.
- `backend/src/app/graph/schema/types.clj`: Malli-to-Ladybug type mapping.
- `backend/src/app/graph/schema/values.clj`: Value coercion.
- `backend/src/app/http/debug.clj`: Graph route registration and handlers.
- `backend/src/app/http/websocket.clj`: File WebSocket subscription handlers.
- `backend/src/app/rpc/commands/files_update.clj`: File-change publication.
- `backend/src/app/main.clj`: Integrant message bus wiring.
- `backend/resources/app/templates/graph-console.tmpl`: Graph console browser code.
- `backend/resources/app/templates/debug.tmpl`: Debug page graph links.
- `common/src/app/common/flags.cljc`: `:graph` feature flag.
- `backend/test/backend_tests/graph_sync_parity_test.clj`: Cold versus sync parity.
- `backend/test/backend_tests/graph_binder_gate_test.clj`: Cypher binder gate.
## Development Commands
- Run backend commands from the `backend/` directory.
- Run focused parity tests with `clojure -M:dev:test --focus backend-tests.graph-sync-parity-test`.
- Run focused binder tests with `clojure -M:dev:test --focus backend-tests.graph-binder-gate-test`.
- Run the backend test suite with `clojure -M:dev:test`.
- Examine Clojure formatting with `pnpm run check-fmt:clj`.
- Run backend Clojure lint with `pnpm run lint:clj`.
- Write test output to a file before reading or filtering it.
+36
View File
@@ -30,10 +30,46 @@
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
## Connection Reuse Details
### `app.storage/resolve` patterns:
**1. Pool mode (default)** - `(sto/resolve cfg)`
- Returns storage abstraction from config
- Uses whatever database pool is available
- **Safe to call outside transaction context**
- Used in: `rpc/commands/media.clj:363`, `rpc/commands/auth.clj:327`, `rpc/commands/profile.clj:362`
**2. Connection reuse mode** - `(sto/resolve cfg ::db/reuse-conn true)`
- Internally calls `db/get-connection cfg` to obtain connectable
- Configures storage with the specific connection from config
- **Must be paired with transaction that owns this connection**
- Used in: `features/fdata.clj:100`, `rpc/commands/media.clj:425`, `rpc/commands/files_thumbnails.clj:307,319`, `binfile/v3.clj:722`
**3. Explicit configuration** - `(sto/configure storage conn)`
- Sets `::db/conn` on storage map directly
- Asserts `db/conn? connection` (storage.clj:349)
- Used inside `db/tx-run!` blocks where `conn` is already available
- Used in: `tasks/file_gc.clj:256`, `rpc/commands/files_thumbnails.clj:347,371`
### Key Warning (from function notes):
The improved note in `import-storage-objects` and `handle-persistence` warns:
**Do not reuse the main database connection for storage operations within a transaction.** The storage upload process can fail mid-operation, leaving orphaned objects on the backend. If the outer transaction aborts, pending storage objects become unreconciliable because the storage subsystem registers its pending state in separate transactions.
### Rule of Thumb for `sto/put-object!`:
Since `put-object!` uses backend-specific operations (`impl/resolve-backend` + `impl/put-object`) and does not directly use `::db/conn` or `::db/pool`, **all usage of `put-object!` will never run inside a common transaction** (if configured at all). The storage backend operations are independent of the database transaction boundary.
## Deduplication
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
- The lookup only considers rows with `status='valid'`; pending rows are invisible.
- A hit whose blob is missing is repaired in place: the same row/id is kept,
and `put-object!` rewrites the blob under that id. This heals all existing
references to the object. If the rewrite fails, the row is left live and
valid for a later retry.
- The lookup does not include file ID, profile ID, team ID, or organization ID.
- Objects can therefore share content across users and files within one bucket.
- Deleted objects are not reused.
+11
View File
@@ -0,0 +1,11 @@
# Backend Testing
JVM `clojure.test` (kaocha runner) under `backend/test/backend_tests/`.
- READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all JVM test runs.
- All CLI commands must be executed from the `backend/` subdirectory.
- Tests are invoked directly via `clojure -M:dev:test` (kaocha) — there is no pnpm wrapper. Kaocha auto-discovers test namespaces, so no runner registration is needed.
- Coverage: if code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
- Isolated run: `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace, or `clojure -M:dev:test --focus backend-tests.my-ns-test/my-test-var` for a specific test var.
- Regression run: `clojure -M:dev:test` to ensure no regressions in related functional areas.
- If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`.
+1
View File
@@ -15,6 +15,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- **Never edit `CHANGES.md` by hand.** The changelog is generated from GitHub milestones during the release process; update it only via the `update-changelog` skill flow or on explicit user request.
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
+2 -1
View File
@@ -6,6 +6,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
- `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`.
- `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer.
- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount.
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
## Source-of-truth files
@@ -65,7 +66,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
## CLI surface
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX] [--opencode-config-dir DIR]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). `--opencode-config-dir DIR` bind-mounts DIR at `~/.config/opencode` in-container via the optional overlay above; mount applies at container creation, so changing it requires stop + re-run.
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv`: legacy alias, ws0 non-agentic attached.
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
@@ -17,9 +17,32 @@
## Tile/render behavior
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
- `can_render_directly` paints onto Current (no Fills/Strokes blit) for plain geometry and
for stroke-free text (SrcOver, no blur/shadows). Multi-style text is fine: span styles
live in Paragraph `TextStyle`s. Text skips the `nested_fills` guard (fills are on spans).
`draw_text` only `save_layer`s when stroke-group opacity is set; plain fill paint is direct.
- Plain text fill paint reuses `TextContent.layout` paragraphs when
`has_usable_paint_layout` (paragraphs present + version match; during
interactive transforms rotation/move skips width check via
`modifier_changes_text_layout`, resize falls back to `layout_width` vs
`get_width(selrect.width())`), via `text::try_paint_from_layout_cache`.
The walker computes `text_layout_cache_rotation_only` from `tree` and
passes it into `render_shape`; stroke/shadow paths pass `false`.
- `TextContentLayout` paragraphs are `Rc`-shared on `Clone` so modifier clones
(rotate/pan) keep the paint cache; `needs_update` is paragraphs-empty only.
Decorations are skipped when no span requests underline/strike.
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
compose+present only.
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
- Zoom settle wipes the tile texture cache in `set_view_end`. Mid-zoom overlays
key tiles by scale; shape edits must `invalidate_cached_tiles_intersecting`
the oldnew extrect so those overlays do not keep pre-edit pixels.
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
+1 -1
View File
@@ -13,7 +13,7 @@ and helpers, consult:
builders, production-path change helpers
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E integration tests,
live browser verification via nREPL
- Backend — JVM `clojure.test` under `backend/test/`; see `mem:backend/core`
- `mem:backend/testing` — JVM `clojure.test` under `backend/test/`
## When to Use
+1 -1
View File
@@ -351,5 +351,5 @@ gh issue view <NUMBER> --repo penpot/penpot --json title
## See Also
- End-to-end orchestration entry point: the `create-issue` skill at
`.opencode/skills/create-issue/SKILL.md`. The skill is a thin entry
`.agents/skills/create-issue/SKILL.md`. The skill is a thin entry
point; this memory is the canonical home for all issue-creation rules.
+7 -1
View File
@@ -1,6 +1,12 @@
# Creating Pull Requests
PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<short-description>` (`fix/...`, `feat/...`, `refactor/...`, `docs/...`, `chore/...`, `perf/...`).
PR only on explicit request.
## Branch Naming
- Primary: `issue-NNNN` — one branch per GitHub issue (e.g. `issue-11525`).
- No issue: free-form descriptive name, dash-separated, no slashes (e.g. `fix-ellipse-icon-typo`, `feat-auto-link-libraries`).
- If the user already created the branch, use it as-is — never rename.
## Target Branch
+21 -1
View File
@@ -8,6 +8,9 @@
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
- **Never amend a commit that has been pushed** unless the user explicitly asks.
If the user pushes, treat that commit as final from the agent's side.
- **Never edit `CHANGES.md` by hand** in commits or PRs. The changelog is
generated from GitHub milestones during the release process; update it only
via the `update-changelog` skill flow or on explicit user request.
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
This prevents hiding test failures. See `mem:testing` for details.
@@ -40,10 +43,27 @@ Skipping this step is the #1 cause of incorrect or incomplete work.
`github.com/penpot/penpot/security/advisories/GHSA-*`, extract the GHSA ID
from the URL and run `python3 scripts/gh.py advisories <GHSA-ID>` to fetch
full advisory details before proceeding.
- **Issue or PR mentioned** — When the user mentions a penpot/penpot issue or
PR (URL like `github.com/penpot/penpot/issues/<n>` / `.../pull/<n>`, or a
bare `#<n>` when context clearly refers to this repo), fetch details via CLI
instead of WebFetch:
- Issue → `gh issue view <n> --repo penpot/penpot` (add `--comments` when
discussion context matters).
- Single PR → `gh pr view <n> --repo penpot/penpot`.
- Multiple PRs (list, file, or milestone) → `python3 scripts/gh.py prs ...`.
Do this before proceeding. Only use WebFetch if the CLI fails.
## Writing Rules
Use the `ste` skill when the user explicitly requests STE, `/ste`, or ASD-STE100.
Writing rules, from Orwell, 1946. These govern prose: docs, PR text, messages. Never touch code or technical terms; swap in everyday words only where precision survives.
1. Never use a metaphor, simile or other figure of speech which you are used to seeing in print.
2. Never use a long word where a short one will do.
3. If it is possible to cut a word out, always cut it out.
4. Never use the passive where you can use the active.
5. Never use a foreign phrase, a scientific word or a jargon word if you can think of an everyday English equivalent.
6. Break any of these rules sooner than say anything outright barbarous.
Review every prose output against these rules before delivering.
---
+133 -3
View File
@@ -1,5 +1,25 @@
# CHANGELOG
## 2.19.0 (Unreleased)
### :bug: Bugs fixed
- Fix copying text from Penpot to the clipboard not working on MS Windows [#11303](https://github.com/penpot/penpot/issues/11303) (PR: [#11305](https://github.com/penpot/penpot/pull/11305))
- Fix performance issue with WebGL render [#11240](https://github.com/penpot/penpot/issues/11240) (PR: [#11259](https://github.com/penpot/penpot/pull/11259))
- Fix comment bubbles rendering on top of workspace dropdown menus [#10283](https://github.com/penpot/penpot/issues/10283) (PR: [#11201](https://github.com/penpot/penpot/pull/11201))
- Fix inconsistent Mixed label in blur options and numeric inputs across 24 locales (by @filipsajdak) [#11148](https://github.com/penpot/penpot/issues/11148) (PR: [#11151](https://github.com/penpot/penpot/pull/11151))
- Fix overlay shifting left when shown with top-center alignment in viewer prototype (by @filipsajdak) [#9048](https://github.com/penpot/penpot/issues/9048) (PR: [#10454](https://github.com/penpot/penpot/pull/10454))
- Fix internal error when clicking the Copy button on the Access Token page (by @0xTHAC0) [#8496](https://github.com/penpot/penpot/issues/8496) (PR: [#11156](https://github.com/penpot/penpot/pull/11156))
- Fix `disable-registration` flag not preventing non-users from creating accounts in the share prototypes page (by @0xTHAC0) [#5164](https://github.com/penpot/penpot/issues/5164) (PR: [#11199](https://github.com/penpot/penpot/pull/11199))
### :sparkles: New features & Enhancements
- Make backend storage resilient to interrupted writes, missing files and stalled cleanup [#11344](https://github.com/penpot/penpot/issues/11344) (PR: [#11345](https://github.com/penpot/penpot/pull/11345))
- Implement RTL support in the text editor v3 [#11262](https://github.com/penpot/penpot/issues/11262)
- Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237))
- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958))
## 2.18.0 (Unreleased)
### :bug: Bugs fixed
@@ -11,18 +31,128 @@
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
- Fix invalid formulas being accepted in numeric inputs (by @AKnassa) [#9581](https://github.com/penpot/penpot/issues/9581) (PR: [#10659](https://github.com/penpot/penpot/pull/10659))
- Fix radial gradient handles blowing up in size when rotated on ellipses (by @AKnassa) [#10069](https://github.com/penpot/penpot/issues/10069) (PR: [#10666](https://github.com/penpot/penpot/pull/10666))
- Fix plugin API validation errors being too generic to diagnose the failure (by @AKnassa) [#10072](https://github.com/penpot/penpot/issues/10072) (PR: [#10667](https://github.com/penpot/penpot/pull/10667))
- Fix crash with referential integrity error when deleting a component inside a grid (by @Alotor) [#10101](https://github.com/penpot/penpot/issues/10101) (PR: [#10956](https://github.com/penpot/penpot/pull/10956))
- Fix component copies not preserving rotation when the main component has changes [#10109](https://github.com/penpot/penpot/issues/10109) (PR: [#10574](https://github.com/penpot/penpot/pull/10574))
- Fix text width and height staying stale after setting growType in the plugin API [#10207](https://github.com/penpot/penpot/issues/10207) (PR: [#9898](https://github.com/penpot/penpot/pull/9898))
- Fix padding not painted until expanding the 4-sides padding option [#10278](https://github.com/penpot/penpot/issues/10278) (PR: [#10602](https://github.com/penpot/penpot/pull/10602))
- Fix files with custom fonts breaking with a referential integrity error when moved between teams (by @filipsajdak) [#10496](https://github.com/penpot/penpot/issues/10496) (PR: [#10837](https://github.com/penpot/penpot/pull/10837))
- Fix clicking overlapping comment bubbles zooming to 20000% without showing the comments [#10526](https://github.com/penpot/penpot/issues/10526) (PR: [#10543](https://github.com/penpot/penpot/pull/10543))
- Fix user menu subsections in the dashboard not closing when hovering away from the parent option (by @AKnassa) [#10549](https://github.com/penpot/penpot/issues/10549) (PR: [#10639](https://github.com/penpot/penpot/pull/10639))
- Fix self-hosted env-generated config.js being cached for 7 days so PENPOT_FLAGS changes did not reach already-cached browsers (by @filipsajdak) [#10556](https://github.com/penpot/penpot/issues/10556) (PR: [#11146](https://github.com/penpot/penpot/pull/11146))
- Fix color of selected text in light theme [#10570](https://github.com/penpot/penpot/issues/10570) (PR: [#10614](https://github.com/penpot/penpot/pull/10614))
- Fix margin input order being inconsistent with padding inputs and between collapsed and expanded states [#10578](https://github.com/penpot/penpot/issues/10578) (PR: [#10797](https://github.com/penpot/penpot/pull/10797))
- Fix uncaught DOMException when writing image/svg+xml content to the clipboard (by @AKnassa) [#10596](https://github.com/penpot/penpot/issues/10596) (PR: [#10663](https://github.com/penpot/penpot/pull/10663))
- Fix tick icons not aligned in the font selector [#10597](https://github.com/penpot/penpot/issues/10597) (PR: [#10774](https://github.com/penpot/penpot/pull/10774))
- Fix incorrect padding values when multiple shapes are selected [#10598](https://github.com/penpot/penpot/issues/10598) (PR: [#10602](https://github.com/penpot/penpot/pull/10602))
- Fix integrity errors related to variants not being repaired [#10606](https://github.com/penpot/penpot/issues/10606) (PR: [#10768](https://github.com/penpot/penpot/pull/10768))
- Fix changing password showing 'Password should be at least 8 characters' error on the old password field (by @AKnassa) [#10626](https://github.com/penpot/penpot/issues/10626) (PR: [#10661](https://github.com/penpot/penpot/pull/10661))
- Fix stroke caps disappearing when dragging [#10633](https://github.com/penpot/penpot/issues/10633) (PR: [#10634](https://github.com/penpot/penpot/pull/10634))
- Fix layout padding being saved as string after invalid input in multi-selection, causing persistence errors (by @niwinz) [#10638](https://github.com/penpot/penpot/issues/10638) (PR: [#10758](https://github.com/penpot/penpot/pull/10758))
- Fix inconsistent theme handling between Penpot and plugins [#10676](https://github.com/penpot/penpot/issues/10676) (PR: [#10677](https://github.com/penpot/penpot/pull/10677))
- Fix image stroke (strokeImage) support missing in the plugin API Stroke interface [#10682](https://github.com/penpot/penpot/issues/10682) (PR: [#10683](https://github.com/penpot/penpot/pull/10683))
- Fix SVG images not working as fill in the WebGL renderer [#10705](https://github.com/penpot/penpot/issues/10705) (PR: [#10707](https://github.com/penpot/penpot/pull/10707))
- Fix background blur not working on text shapes [#10706](https://github.com/penpot/penpot/issues/10706) (PR: [#10712](https://github.com/penpot/penpot/pull/10712))
- Fix background blur not applying on strokes [#10713](https://github.com/penpot/penpot/issues/10713) (PR: [#10716](https://github.com/penpot/penpot/pull/10716))
- Fix text shape with empty content breaking workspace updates [#10725](https://github.com/penpot/penpot/issues/10725) (PR: [#10731](https://github.com/penpot/penpot/pull/10731))
- Fix missing SVG option in the file filters when adding an image fill (by @LuBoys) [#10756](https://github.com/penpot/penpot/issues/10756) (PR: [#10771](https://github.com/penpot/penpot/pull/10771))
- Update onboarding image [#10779](https://github.com/penpot/penpot/issues/10779) (PR: [#10783](https://github.com/penpot/penpot/pull/10783))
- Fix main toolbar overlapping the grid edition bar [#10788](https://github.com/penpot/penpot/issues/10788) (PR: [#10789](https://github.com/penpot/penpot/pull/10789))
- Fix WASM renderer panic when the WebGL context is restored mid-reload [#10810](https://github.com/penpot/penpot/issues/10810) (PR: [#10824](https://github.com/penpot/penpot/pull/10824))
- Fix nginx frontend forwarding the client Host header to backend/exporter, breaking Istio strict mTLS routing (by @yamila-moreno) [#10835](https://github.com/penpot/penpot/issues/10835) (PR: [#11233](https://github.com/penpot/penpot/pull/11233))
- Fix tutorial templates with components causing errors [#10839](https://github.com/penpot/penpot/issues/10839)
- Fix plugin 'Try out' flow crashing when projects have not loaded yet [#10858](https://github.com/penpot/penpot/issues/10858) (PR: [#10859](https://github.com/penpot/penpot/pull/10859))
- Fix collapsed Fill color section on the design panel for new texts [#10860](https://github.com/penpot/penpot/issues/10860) (PR: [#10972](https://github.com/penpot/penpot/pull/10972))
- Fix grid item date tooltip in the project view showing 'Will be deleted' instead of creation date (by @0xTHAC0) [#10873](https://github.com/penpot/penpot/issues/10873) (PR: [#11161](https://github.com/penpot/penpot/pull/11161))
- Merge stop and start measurement shortcut to match current behavior [#10884](https://github.com/penpot/penpot/issues/10884) (PR: [#10906](https://github.com/penpot/penpot/pull/10906))
- Fix shape size badge displayed twice when a user with Viewer permissions selects a shape [#10893](https://github.com/penpot/penpot/issues/10893) (PR: [#10985](https://github.com/penpot/penpot/pull/10985))
- Fix main menu being covered by the toolbar [#10902](https://github.com/penpot/penpot/issues/10902) (PR: [#10926](https://github.com/penpot/penpot/pull/10926))
- Fix font family typography asset persisting across files in newly created text layers [#10925](https://github.com/penpot/penpot/issues/10925) (PR: [#11134](https://github.com/penpot/penpot/pull/11134))
- Fix error raised when editing justified text [#10944](https://github.com/penpot/penpot/issues/10944) (PR: [#10945](https://github.com/penpot/penpot/pull/10945))
- Fix MCP WebSocket proxy failing after penpot-mcp container restarts due to stale nginx DNS resolution (by @780Farva) [#10946](https://github.com/penpot/penpot/issues/10946) (PR: [#10947](https://github.com/penpot/penpot/pull/10947))
- Fix verification email address being unreadable due to low-contrast text on the register success page [#10950](https://github.com/penpot/penpot/issues/10950) (PR: [#10965](https://github.com/penpot/penpot/pull/10965))
- Fix image swatches displaying a wrong format in the color picker list view [#10951](https://github.com/penpot/penpot/issues/10951) (PR: [#10975](https://github.com/penpot/penpot/pull/10975))
- Fix text editor crashing when dropping dragged text after selecting all content [#10954](https://github.com/penpot/penpot/issues/10954) (PR: [#10959](https://github.com/penpot/penpot/pull/10959))
- Fix MCP tokens being usable as API access tokens [#10960](https://github.com/penpot/penpot/issues/10960) (PR: [#10962](https://github.com/penpot/penpot/pull/10962))
- Add size limit and rate limiting to the send-user-feedback endpoint [#10979](https://github.com/penpot/penpot/issues/10979) (PR: [#10990](https://github.com/penpot/penpot/pull/10990))
- Fix main menu not keeping alignment when the left sidebar is expanded [#10981](https://github.com/penpot/penpot/issues/10981) (PR: [#10986](https://github.com/penpot/penpot/pull/10986))
- Fix update-profile-props RPC method accepting undocumented keys [#10991](https://github.com/penpot/penpot/issues/10991) (PR: [#10992](https://github.com/penpot/penpot/pull/10992))
- Fix import-binfile RPC method schema accepting a file-id parameter [#10993](https://github.com/penpot/penpot/issues/10993) (PR: [#10994](https://github.com/penpot/penpot/pull/10994))
- Fix assemble-chunks session lookup ignoring the profile-id scope [#11011](https://github.com/penpot/penpot/issues/11011) (PR: [#11012](https://github.com/penpot/penpot/pull/11012))
- Validate font-id team ownership in create-font-variant [#11013](https://github.com/penpot/penpot/issues/11013) (PR: [#11014](https://github.com/penpot/penpot/pull/11014))
- Validate team ownership on file library link endpoints [#11015](https://github.com/penpot/penpot/issues/11015) (PR: [#11016](https://github.com/penpot/penpot/pull/11016))
- Limit object size allocation in the V1 binfile parser [#11017](https://github.com/penpot/penpot/issues/11017) (PR: [#11018](https://github.com/penpot/penpot/pull/11018))
- Limit recursion depth in the Fressian reader [#11019](https://github.com/penpot/penpot/issues/11019) (PR: [#11020](https://github.com/penpot/penpot/pull/11020))
- Limit concurrent imports in the import-binfile RPC method [#11023](https://github.com/penpot/penpot/issues/11023) (PR: [#11024](https://github.com/penpot/penpot/pull/11024))
- Validate content-type on management upload endpoints [#11025](https://github.com/penpot/penpot/issues/11025) (PR: [#11026](https://github.com/penpot/penpot/pull/11026))
- Fix webhook endpoints allowing unauthorized access via creator-id fallback [#11028](https://github.com/penpot/penpot/issues/11028) (PR: [#11029](https://github.com/penpot/penpot/pull/11029))
- Escape markdown in user-controlled fields of Mattermost error notifications [#11033](https://github.com/penpot/penpot/issues/11033) (PR: [#11034](https://github.com/penpot/penpot/pull/11034))
- Enforce file read permission check on asset endpoints [#11035](https://github.com/penpot/penpot/issues/11035) (PR: [#11036](https://github.com/penpot/penpot/pull/11036))
- Add accumulated storage byte quota for media uploads [#11037](https://github.com/penpot/penpot/issues/11037) (PR: [#11038](https://github.com/penpot/penpot/pull/11038))
- Add bounding box dimension limit to exports [#11041](https://github.com/penpot/penpot/issues/11041) (PR: [#11042](https://github.com/penpot/penpot/pull/11042))
- Sanitize embedded scripts in SVG uploads [#11043](https://github.com/penpot/penpot/issues/11043) (PR: [#11044](https://github.com/penpot/penpot/pull/11044))
- Fix duplicate file ID returning inconsistent error responses [#11045](https://github.com/penpot/penpot/issues/11045) (PR: [#11050](https://github.com/penpot/penpot/pull/11050))
- Enforce permission checks in WebSocket subscription handlers [#11052](https://github.com/penpot/penpot/issues/11052) (PR: [#11054](https://github.com/penpot/penpot/pull/11054))
- Fix 'something went wrong' popup when using incremental numerical input interaction [#11053](https://github.com/penpot/penpot/issues/11053) (PR: [#10794](https://github.com/penpot/penpot/pull/10794))
- Enforce password complexity validation on the backend [#11055](https://github.com/penpot/penpot/issues/11055) (PR: [#11059](https://github.com/penpot/penpot/pull/11059))
- Normalize string inputs before processing [#11060](https://github.com/penpot/penpot/issues/11060) (PR: [#11061](https://github.com/penpot/penpot/pull/11061))
- Add cooldown to avoid sending duplicate invitation emails [#11062](https://github.com/penpot/penpot/issues/11062) (PR: [#11063](https://github.com/penpot/penpot/pull/11063))
- Enable SSRF protection for organization SSO validation [#11064](https://github.com/penpot/penpot/issues/11064) (PR: [#11065](https://github.com/penpot/penpot/pull/11065))
- Fix clone-file-media-object allowing to clone media objects from files without read access [#11087](https://github.com/penpot/penpot/issues/11087) (PR: [#11090](https://github.com/penpot/penpot/pull/11090))
- Fix 404 error page logo not visible in dark mode [#11091](https://github.com/penpot/penpot/issues/11091) (PR: [#11167](https://github.com/penpot/penpot/pull/11167))
- Fix incorrect permission handling when creating an invitation [#11098](https://github.com/penpot/penpot/issues/11098) (PR: [#11099](https://github.com/penpot/penpot/pull/11099))
- Reject zero or negative total-chunks values in upload sessions [#11103](https://github.com/penpot/penpot/issues/11103) (PR: [#11104](https://github.com/penpot/penpot/pull/11104))
- Fix import-binfile accepting unsupported version values without validation [#11105](https://github.com/penpot/penpot/issues/11105) (PR: [#11107](https://github.com/penpot/penpot/pull/11107))
- Fix sessions remaining active on other devices after account deletion [#11114](https://github.com/penpot/penpot/issues/11114) (PR: [#11115](https://github.com/penpot/penpot/pull/11115))
- Use random UUIDs for share link IDs instead of a predictable scheme [#11116](https://github.com/penpot/penpot/issues/11116) (PR: [#11117](https://github.com/penpot/penpot/pull/11117))
- Fix plugin manifest fetch hanging indefinitely without timeout [#11119](https://github.com/penpot/penpot/issues/11119) (PR: [#11120](https://github.com/penpot/penpot/pull/11120))
- Use constant-time comparison for shared key authentication [#11121](https://github.com/penpot/penpot/issues/11121) (PR: [#11122](https://github.com/penpot/penpot/pull/11122))
- Fix ESC key not closing the comment input box after posting a comment in the workspace [#11128](https://github.com/penpot/penpot/issues/11128) (PR: [#11131](https://github.com/penpot/penpot/pull/11131))
- Fix token edit modal crashing when resolving tokens with group nodes [#11143](https://github.com/penpot/penpot/issues/11143) (PR: [#11144](https://github.com/penpot/penpot/pull/11144))
- Fix text editor crashing when pasting into an empty text shape [#11149](https://github.com/penpot/penpot/issues/11149) (PR: [#11150](https://github.com/penpot/penpot/pull/11150))
- Fix comment avatars appearing on top of rulers when scrolling the canvas (by @filipsajdak) [#11163](https://github.com/penpot/penpot/issues/11163) (PR: [#11168](https://github.com/penpot/penpot/pull/11168))
- Fix infinite loop of get-teams and get-team-members calls when granting team access from an email link [#11215](https://github.com/penpot/penpot/issues/11215) (PR: [#11223](https://github.com/penpot/penpot/pull/11223))
- Fix RPC requests bypassing rate limiting with fractional bucket refill intervals [#11253](https://github.com/penpot/penpot/issues/11253) (PR: [#11254](https://github.com/penpot/penpot/pull/11254))
- Fix tempfile bucket serving objects to any authenticated user instead of only the uploader [#11269](https://github.com/penpot/penpot/issues/11269) (PR: [#11270](https://github.com/penpot/penpot/pull/11270))
- Fix increasing a value by clicking and dragging in a numeric input [#11274](https://github.com/penpot/penpot/issues/11274) (PR: [#11334](https://github.com/penpot/penpot/pull/11334))
- Fix notification pill rendering unescaped HTML in the detail section when importing tokens [#11276](https://github.com/penpot/penpot/issues/11276) (PR: [#11275](https://github.com/penpot/penpot/pull/11275))
- Fix share-link holders reading pages outside the authorized scope via the get-page RPC command [#11281](https://github.com/penpot/penpot/issues/11281) (PR: [#11284](https://github.com/penpot/penpot/pull/11284))
- Fix incorrect permission handling when managing share links on a file [#11289](https://github.com/penpot/penpot/issues/11289) (PR: [#11290](https://github.com/penpot/penpot/pull/11290))
- Fix backend session remaining valid after logout when the auth-token cookie is replayed [#11316](https://github.com/penpot/penpot/issues/11316) (PR: [#11317](https://github.com/penpot/penpot/pull/11317))
- Fix get-team-invitation-token requiring only read permissions [#11358](https://github.com/penpot/penpot/issues/11358) (PR: [#11359](https://github.com/penpot/penpot/pull/11359))
### :sparkles: New features & Enhancements
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
- Highlight the first matching font in the font list when searching (by @ai-mountain) [#3204](https://github.com/penpot/penpot/issues/3204) (PR: [#9512](https://github.com/penpot/penpot/pull/9512), [#10450](https://github.com/penpot/penpot/pull/10450))
- Preserve token references when copying and pasting properties instead of resolving them to values (by @AKnassa) [#9582](https://github.com/penpot/penpot/issues/9582) (PR: [#10665](https://github.com/penpot/penpot/pull/10665))
- Add waitForLayoutUpdate method to the plugin API [#10136](https://github.com/penpot/penpot/issues/10136) (PR: [#9898](https://github.com/penpot/penpot/pull/9898))
- Show and manage comments while designing in the workspace [#10239](https://github.com/penpot/penpot/issues/10239) (PR: [#10275](https://github.com/penpot/penpot/pull/10275))
- Simplify MCP server configuration for common MCP clients [#10355](https://github.com/penpot/penpot/issues/10355) (PR: [#10604](https://github.com/penpot/penpot/pull/10604))
- Remove misleading MCP client JSON snippet from the key-generated modal (by @Shlok1729) [#10399](https://github.com/penpot/penpot/issues/10399) (PR: [#10415](https://github.com/penpot/penpot/pull/10415))
- Preview font families in the font selector [#10403](https://github.com/penpot/penpot/issues/10403) (PR: [#10411](https://github.com/penpot/penpot/pull/10411))
- Remember expanded/collapsed state of token sets in the color tokens picker (session scope) [#10551](https://github.com/penpot/penpot/issues/10551) (PR: [#10864](https://github.com/penpot/penpot/pull/10864))
- Show token sets in reverse order by default in the color tokens picker (by @rhinocap) [#10552](https://github.com/penpot/penpot/issues/10552) (PR: [#10658](https://github.com/penpot/penpot/pull/10658))
- Add multi-selection and bulk delete support to pages in the workspace sitemap [#10580](https://github.com/penpot/penpot/issues/10580) (PR: [#10581](https://github.com/penpot/penpot/pull/10581))
- Add a grid/list view toggle for files in the dashboard [#10691](https://github.com/penpot/penpot/issues/10691) (PR: [#10692](https://github.com/penpot/penpot/pull/10692))
- Migrate Docker images to Docker Hardened Images (DHI) [#10720](https://github.com/penpot/penpot/issues/10720) (PR: [#10732](https://github.com/penpot/penpot/pull/10732), [#10733](https://github.com/penpot/penpot/pull/10733), [#10734](https://github.com/penpot/penpot/pull/10734))
- Adopt React Aria [#10802](https://github.com/penpot/penpot/issues/10802) (PR: [#10675](https://github.com/penpot/penpot/pull/10675))
- Add plugin API function for awaiting component updates beyond waitForLayoutUpdate [#10927](https://github.com/penpot/penpot/issues/10927) (PR: [#10964](https://github.com/penpot/penpot/pull/10964))
- Emit open-workspace-file audit event with file statistics on workspace load [#11106](https://github.com/penpot/penpot/issues/11106) (PR: [#11138](https://github.com/penpot/penpot/pull/11138))
## 2.17.2
### :bug: Bugs fixed
- Fix linear gradients in SVG text exports being emitted as radial gradients [#5972](https://github.com/penpot/penpot/issues/5972) (PR: [#11272](https://github.com/penpot/penpot/pull/11272))
- Fix typography token becoming detached when editing text content [#11362](https://github.com/penpot/penpot/issues/11362) (PR: [#11366](https://github.com/penpot/penpot/pull/11366))
- Fix command injection in SVG exporter via legacy fill-color (https://github.com/penpot/penpot/security/advisories/GHSA-4f36-m4hj-cv86)
## 2.17.1
@@ -3026,7 +3156,7 @@ is a number of cores)
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883)
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895)
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374)
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047)
- Increment font size by 10 with shift+arrows [#1047](https://github.com/penpot/penpot/issues/1047)
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799)
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921)
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823)
+3
View File
@@ -0,0 +1,3 @@
Read and follow the instructions in `AGENTS.md`.
Treat `AGENTS.md` as the canonical project instruction file.
+13
View File
@@ -14,6 +14,7 @@ Center](https://help.penpot.app/).
- [Reporting Bugs](#reporting-bugs)
- [Pull Requests](#pull-requests)
- [Workflow](#workflow)
- [Branch naming](#branch-naming)
- [Format](#format)
- [Title format](#title-format)
- [Description](#description)
@@ -73,6 +74,18 @@ Advisories](https://github.com/penpot/penpot/security/advisories)
4. **Format and lint** — run the checks described in
[Formatting and Linting](#formatting-and-linting) before submitting.
### Branch naming
Branch names are not enforced, but we recommend the following:
- **`issue-NNNN`** — when working from a GitHub issue, name the branch after
it (e.g. `issue-11525`). This makes each PR's origin self-evident.
- Otherwise, use a short, descriptive name with words separated by hyphens
and no slashes (e.g. `fix-ellipse-icon-typo`, `feat-auto-link-libraries`).
Since PRs are squash-merged, the branch name does not survive into the
commit history — what matters is the [PR title](#title-format).
### Format
#### Title
+26
View File
@@ -0,0 +1,26 @@
# HIGHLIGHTS
## 2.17.0
- Background blur is here
- WebGL rendering gets stronger
- MCP connection status and more
- Design tokens: more visible, more user-friendly
## 2.16.0
- Design tokens in the design panel
- Major community contributions
- WebGL rendering (beta)
## 2.15.0
- AI connected to real design context
- Multi-directional workflow
- Your stack, your model, your decision
+1 -1
View File
@@ -160,6 +160,6 @@ This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC Sucursal en España SL
Copyright (c) KALEIDOS SUBSIDIARY SL
```
Penpot is a Kaleidos [open source project](https://kaleidos.net/)
+17 -11
View File
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "7.6.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "7.7.0.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -25,7 +25,7 @@
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
com.google.guava/guava {:mvn/version "33.6.0-jre"}
com.google.guava/guava {:mvn/version "33.7.1-jre"}
funcool/yetti
{:git/tag "v11.10"
@@ -40,38 +40,44 @@
nrepl/nrepl {:mvn/version "1.7.0"}
org.postgresql/postgresql {:mvn/version "42.7.13"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.4.0"}
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
io.whitfin/siphash {:mvn/version "3.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
org.passay/passay {:mvn/version "1.6.6"}
org.passay/passay {:mvn/version "2.0.0"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
org.jsoup/jsoup {:mvn/version "1.23.1"}
org.jsoup/jsoup {:mvn/version "1.23.2"}
at.yawk.lz4/lz4-java
{:mvn/version "1.11.1"}
{:mvn/version "1.11.2"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.8"}
markdown-clj/markdown-clj {:mvn/version "1.12.9"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
software.amazon.awssdk/s3 {:mvn/version "2.54.5"}
software.amazon.awssdk/sts {:mvn/version "2.54.5"}
com.ladybugdb/lbug {:mvn/version "0.19.1"}
;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core).
org.apache.arrow/arrow-memory-netty {:mvn/version "18.2.0"}}
:paths ["src" "resources" "target/classes"]
:aliases
{:dev
{:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
"--enable-native-access=ALL-UNNAMED"
;; Arrow jars are on the classpath (unnamed module), not module-path.
"--add-opens=java.base/java.nio=ALL-UNNAMED"]
:extra-deps
{com.bhauman/rebel-readline {:mvn/version "0.1.11"}
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
;; This is an example on how it can be executed:
;; clojure -Scp $(cat classpath) -M dev/script-fix-sobjects.clj
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns user
(:require
@@ -191,7 +191,7 @@
file named “{{file-name|abbreviate:25}}”.
</p>
<p>
Since this file is in your Penpot team, you can provide access by sending a view-only link.
Since this file is in your Personal Projects, you can provide access by sending a view-only link.
This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
</p>
<p>To proceed, please click the button below to generate and send the view-only link:</p>
@@ -2,7 +2,7 @@ Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”.
Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
Since this file is in your Personal Projects, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
To proceed, please click the link below to generate and send the view-only link:
@@ -191,7 +191,7 @@
“{{file-name|abbreviate:25}}”.
</p>
<p>
Please note that the file is currently in Your Penpot 's team, so direct access cannot be
Please note that the file is currently in Personal Projects, so direct access cannot be
granted. However, you have two options to provide the requested access:
</p>
<ul>
@@ -5,7 +5,7 @@ Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access:
Please note that the file is currently in Personal Projects, so direct access cannot be granted. However, you have two options to provide the requested access:
- Move the File to Another Team:
+114
View File
@@ -190,6 +190,20 @@ Debug Main Page
</div>
</form>
</fieldset>
<fieldset>
<legend>Validate file:</legend>
<desc>Given an FILE-ID, check the referential integrity.</desc>
<form method="get" action="/dbg/actions/file-validate">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="validate" value="Validate" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
@@ -222,6 +236,23 @@ Debug Main Page
</div>
</form>
</fieldset>
{% if graph-enabled %}
<fieldset>
<legend>Export graph (Ladybug):</legend>
<desc>Given a FILE-ID, builds the graph projection and downloads
the `.lbug` database file.</desc>
<form method="get" action="/dbg/actions/graph-export">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" value="Download .lbug" />
<a href="/dbg/graph">Open graph console</a>
</div>
</form>
</fieldset>
{% endif %}
<fieldset>
<legend>Import binfile:</legend>
<desc>Import penpot file in binary format.</desc>
@@ -236,6 +267,89 @@ Debug Main Page
</div>
</form>
</fieldset>
<fieldset>
<legend>Repair file:</legend>
<desc>Given an FILE-ID, repair the referential integrity errors.
<br/>
<br/>
<b>WARNING: the reparation is not guaranteed and may cause loss of data!</b>
<br/>
<br/>
You may need to give several repair rounds until all errors are cleared.
</desc>
<form method="get" action="/dbg/actions/file-repair">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<label for="check-snapshot">Skip snapshot</label>
<input id="check-snapshot" type="checkbox" name="skip-snapshot" />
<br />
<small>
A snapshot is made just before the validation, unless skipped.
</small>
</div>
<div class="row">
<input type="submit" name="repair" value="Repair" />
</div>
</form>
</fieldset>
</section>
</main>
<main class="dashboard wide">
<section class="widget wide">
<fieldset>
<legend>Export jobs:</legend>
<desc>
Export jobs as the exporter left them in redis. Records expire an hour
after the export settles, so this is a live view, not a history.
</desc>
<form method="get" action="/dbg">
<div class="row">
<input type="text" style="width:300px" name="job-id"
placeholder="filter by job id" value="{{export-job-filter}}" />
<input type="submit" value="Filter" />
<a href="/dbg">clear</a>
</div>
</form>
<div class="scroll-box">
<table>
<thead>
<tr>
<th>JOB ID</th>
<th>STATE</th>
<th>PROGRESS</th>
<th>CMD</th>
<th>BACKEND</th>
<th>NAME</th>
<th>CREATED</th>
<th>ENDED</th>
</tr>
</thead>
<tbody>
{% for job in export-jobs %}
<tr>
<td><tt>{{job.id}}</tt></td>
<td>{{job.state}}{% if job.interrupted %} (interrupted){% endif %}</td>
<td>{{job.done}} / {{job.total}}</td>
<td>{{job.cmd}}</td>
<td>{{job.backend}}</td>
<td>{{job.name}}</td>
<td>{{job.created-at}}</td>
<td>{{job.ended-at}}</td>
</tr>
{% empty %}
<tr><td colspan="8">No export jobs.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</fieldset>
</section>
</main>
{% endblock %}
File diff suppressed because it is too large. Load diff
@@ -143,6 +143,35 @@ nav > div:not(:last-child) {
height: fit-content;
}
/* A widget that holds a table rather than a form: full width, and tall
enough to be worth scrolling inside. */
.dashboard.wide {
margin-top: 0px;
}
.widget.wide {
max-width: none;
width: 100%;
}
.widget.wide .scroll-box {
max-height: 320px;
overflow-y: auto;
margin-top: 10px;
}
.widget.wide table {
width: 100%;
border-collapse: collapse;
}
.widget.wide th {
text-align: left;
position: sticky;
top: 0;
background: white;
}
.widget input[type=submit] {
outline: none;
border: 1px solid gray;
+6 -3
View File
@@ -13,6 +13,10 @@ export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
# PENPOT_DATABASE_*, PENPOT_REDIS_URI, PENPOT_OBJECTS_STORAGE_*, AWS_*) is owned by
# docker/devenv/defaults.env and injected via the main service's env block.
if [ -f /home/selfsigned.crt ]; then
export NODE_EXTRA_CA_CERTS=/home/selfsigned.crt;
fi
# Background worker flag is per-instance. Defaults to enabled (ws0); ws1+
# overlays set PENPOT_BACKEND_WORKER=false so scheduled and async tasks only
# run on ws0, keeping notification Pub/Sub bound to a single Valkey. See
@@ -89,7 +93,8 @@ export JAVA_OPTS="\
-XX:-OmitStackTraceInFastThrow \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
--enable-native-access=ALL-UNNAMED \
--add-opens=java.base/java.nio=ALL-UNNAMED";
function setup_minio() {
if [ "${PENPOT_OBJECTS_STORAGE_BACKEND}" != "s3" ]; then
@@ -101,5 +106,3 @@ function setup_minio() {
mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q
mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q
}
+1 -1
View File
@@ -4,7 +4,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) KALEIDOS INC Sucursal en España SL
# Copyright (c) KALEIDOS SUBSIDIARY SL
import argparse
import json
+1 -1
View File
@@ -18,7 +18,7 @@ if [ -f ./environ ]; then
source ./environ
fi
export JAVA_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED --enable-preview $JVM_OPTS $JAVA_OPTS"
export JAVA_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --enable-preview $JVM_OPTS $JAVA_OPTS"
ENTRYPOINT=${1:-app.main};
+12 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth
(:require
@@ -14,10 +14,21 @@
:iterations 3
:parallelism 2})
(def ^:private weak-options
{:alg :pbkdf2+sha256
:iterations 100})
(defn derive-password
[password]
(hashers/derive password default-options))
(defn derive-password-weak
"Derives a password using a fast algorithm (pbkdf2+sha256, 100 iterations).
Intended for demo users only — they are already gated behind the
`demo-users` config flag which is disabled in production."
[password]
(hashers/derive password weak-options))
(defn verify-password
[attempt password]
(try
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth.ldap
(:require
+120 -64
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth.oidc
"OIDC client implementation."
@@ -42,31 +42,52 @@
;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- raise-invalid-sso-config
"Raise a controlled validation error for OIDC provider configuration failures."
[& {:keys [hint cause] :as params}]
(throw (ex-info (or hint "invalid-sso-config")
(-> params
(dissoc :cause)
(assoc :type :validation
:code :invalid-sso-config))
cause)))
(defn- discover-oidc-config
[cfg {:keys [base-uri skip-ssrf-check?] :as provider}]
(let [uri (u/join base-uri ".well-known/openid-configuration")
rsp (http/req cfg {:method :get :uri (dm/str uri)} {:skip-ssrf-check? skip-ssrf-check?})]
(let [uri (u/join base-uri ".well-known/openid-configuration")]
(try
(let [rsp (http/req cfg {:method :get :uri (dm/str uri)} {:skip-ssrf-check? skip-ssrf-check?})]
(if (= 200 (:status rsp))
(let [data (-> rsp :body json/decode)
token-uri (get data :token_endpoint)
auth-uri (get data :authorization_endpoint)
user-uri (get data :userinfo_endpoint)
jwks-uri (get data :jwks_uri)
logout-uri (get data :end_session_endpoint)]
(if (= 200 (:status rsp))
(let [data (-> rsp :body json/decode)
token-uri (get data :token_endpoint)
auth-uri (get data :authorization_endpoint)
user-uri (get data :userinfo_endpoint)
jwks-uri (get data :jwks_uri)
logout-uri (get data :end_session_endpoint)]
(-> provider
(assoc :token-uri token-uri)
(assoc :auth-uri auth-uri)
(assoc :user-uri user-uri)
(assoc :jwks-uri jwks-uri)
(assoc :logout-uri logout-uri)))
(-> provider
(assoc :token-uri token-uri)
(assoc :auth-uri auth-uri)
(assoc :user-uri user-uri)
(assoc :jwks-uri jwks-uri)
(assoc :logout-uri logout-uri)))
(ex/raise :type ::internal
:code :invalid-sso-config
:hint "unable to discover OIDC configuration"
:discover-uri uri
:response-status-code (:status rsp)))))
(raise-invalid-sso-config
:hint "unable to discover OIDC configuration"
:discover-uri uri
:response-status-code (:status rsp))))
(catch Throwable cause
;; Controlled raises above are ExceptionInfo and would otherwise be
;; re-wrapped by this catch, dropping fields like :response-status-code.
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw cause)
;; Wrap SSRF blocks, DNS failures, TLS errors, etc. — from the caller's
;; perspective these are all "bad/unreachable issuer URL".
(raise-invalid-sso-config
:hint "unable to discover OIDC configuration"
:discover-uri uri
:cause cause))))))
(def ^:private default-oidc-scopes
#{"openid" "profile" "email"})
@@ -107,16 +128,29 @@
(defn- fetch-oidc-jwks
[cfg jwks-uri {:keys [skip-ssrf-check?]}]
(let [{:keys [status body]} (http/req cfg {:method :get :uri jwks-uri} {:skip-ssrf-check? skip-ssrf-check?})]
(if (= 200 status)
(-> body json/decode :keys process-oidc-jwks)
(ex/raise :type ::internal
:code :unable-to-fetch-sso-jwks
:hint "unable to retrieve JWKs (unexpected response status code)"
:response-status-code status))))
(try
(let [{:keys [status body]} (http/req cfg {:method :get :uri jwks-uri} {:skip-ssrf-check? skip-ssrf-check?})]
(if (= 200 status)
(-> body json/decode :keys process-oidc-jwks)
(raise-invalid-sso-config
:hint "unable to retrieve JWKs (unexpected response status code)"
:jwks-uri jwks-uri
:response-status-code status)))
(catch Throwable cause
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw cause)
(raise-invalid-sso-config
:hint "unable to retrieve JWKs"
:jwks-uri jwks-uri
:cause cause)))))
(defn- populate-jwks
"Fetch and Add (if possible) JWK's to the OIDC provider"
"Fetch and add JWKs to the OIDC provider.
When `:strict-jwks?` is set (organization SSO), failures raise a controlled
validation error. Otherwise JWKS is best-effort: log and continue without keys
so global OIDC/GitLab providers can still initialize if JWKS is temporarily down."
[cfg provider]
(try
(if-let [jwks (when-let [jwks-uri (:jwks-uri provider)]
@@ -124,20 +158,28 @@
(assoc provider :jwks jwks)
provider)
(catch Throwable cause
(l/warn :hint "unable to fetch JWKs for the OIDC provider"
:provider (str (:id provider))
:cause cause)
provider)))
(if (:strict-jwks? provider)
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw cause)
(raise-invalid-sso-config
:hint "unable to retrieve JWKs"
:provider (:id provider)
:cause cause))
(do
(l/warn :hint "unable to fetch JWKs for the OIDC provider"
:provider (str (:id provider))
:cause cause)
provider)))))
(defn- prepare-oidc-provider
[cfg params]
(when-not (and (string? (:base-uri params))
(string? (:client-id params))
(string? (:client-secret params)))
(ex/raise :type ::internal
:code :invalid-sso-config
:hint "missing params for provider initialization"
:provider (:id params)))
(raise-invalid-sso-config
:hint "missing params for provider initialization"
:provider (:id params)))
(try
(if (and (string? (:token-uri params))
@@ -150,11 +192,13 @@
(with-meta provider {::discovered true})))
(catch Throwable cause
(ex/raise :type ::internal
:type :invalid-sso-config
:hint "unexpected exception on configuring provider"
:provider (:id params)
:cause cause))))
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw cause)
(raise-invalid-sso-config
:hint "unexpected exception on configuring provider"
:provider (:id params)
:cause cause)))))
(defmethod ig/assert-key ::providers/generic
[_ params]
@@ -322,10 +366,9 @@
[cfg params]
(when-not (and (string? (:client-id params))
(string? (:client-secret params)))
(ex/raise :type ::internal
:code :invalid-sso-config
:hint "missing params for provider initialization"
:provider (:id params)))
(raise-invalid-sso-config
:hint "missing params for provider initialization"
:provider (:id params)))
(try
(let [provider (populate-jwks cfg params)]
@@ -336,11 +379,13 @@
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
(ex/raise :type ::internal
:type :invalid-sso-config
:hint "unexpected exception on configuring provider"
:provider (:id params)
:cause cause))))
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw cause)
(raise-invalid-sso-config
:hint "unexpected exception on configuring provider"
:provider (:id params)
:cause cause)))))
(defmethod ig/init-key ::providers/gitlab
[_ cfg]
@@ -867,7 +912,10 @@
:base-uri (some-> (non-blank-uri issuer)
(str/rtrim "/")
(str "/"))
:scopes default-oidc-scopes}))
:scopes default-oidc-scopes
;; Organization SSO is configured by customers; discovery
;; and JWKS failures must surface as controlled errors.
:strict-jwks? true}))
(defn build-organization-sso-auth-redirect-uri
"Build the OIDC authorization redirect URI for an organization SSO config.
@@ -877,16 +925,24 @@
issuer (organization-sso-discovery-uri sso)
dest-url (or dest-url (str (cf/get :public-uri)))]
(when-not issuer
(ex/raise :type :validation
:code :invalid-sso-config
:hint "missing issuer"))
(let [oidc-provider (or provider (prepare-organization-sso-provider cfg sso))
state-token (tokens/generate cfg {:iss "oidc"
:dest-url dest-url
:organization-id organization-id
:issuer issuer
:exp (ct/in-future "4h")})]
(build-auth-redirect-uri oidc-provider state-token))))
(raise-invalid-sso-config
:hint "missing issuer"
:organization-id organization-id))
(try
(let [oidc-provider (or provider (prepare-organization-sso-provider cfg sso))
state-token (tokens/generate cfg {:iss "oidc"
:dest-url dest-url
:organization-id organization-id
:issuer issuer
:exp (ct/in-future "4h")})]
(build-auth-redirect-uri oidc-provider state-token))
(catch Throwable cause
(if (and (ex/error? cause)
(= :invalid-sso-config (:code (ex-data cause))))
(throw (ex-info (ex-message cause)
(assoc (ex-data cause) :organization-id organization-id)
(ex-cause cause)))
(throw cause))))))
(def ^:private probe-auth-code "penpot-sso-config-probe")
@@ -981,7 +1037,7 @@
provider (prepare-organization-sso-provider cfg sso)
_info (get-info cfg provider state code)
session (session/get-session request)
exp (ct/in-future {:minutes 15})]
exp (ct/in-future {:hours 4})]
(when (and session organization-id)
(let [props (-> (or (:props session) {})
(update :sso assoc organization-id exp))]
+9 -6
View File
@@ -2,14 +2,16 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.auth.passwords
"Password strength validation using Passay library."
(:require
[app.common.exceptions :as ex])
(:import
[org.passay CharacterCharacteristicsRule CharacterRule EnglishCharacterData PasswordData]))
[org.passay PasswordData]
[org.passay.data EnglishCharacterData]
[org.passay.rule CharacterCharacteristicsRule CharacterRule]))
(defonce ^:private passay-code->translation-key
{"INSUFFICIENT_LOWERCASE" "errors.weak-password.insufficient-lowercase"
@@ -18,12 +20,13 @@
"INSUFFICIENT_SPECIAL" "errors.weak-password.insufficient-special"})
(defonce ^:private character-characteristics-rule
(doto (CharacterCharacteristicsRule.)
(.setRules [(CharacterRule. EnglishCharacterData/LowerCase 1)
(CharacterCharacteristicsRule.
4
(into-array org.passay.rule.CharacterRule
[(CharacterRule. EnglishCharacterData/LowerCase 1)
(CharacterRule. EnglishCharacterData/UpperCase 1)
(CharacterRule. EnglishCharacterData/Digit 1)
(CharacterRule. EnglishCharacterData/Special 1)])
(.setNumberOfCharacteristics 4)))
(CharacterRule. EnglishCharacterData/Special 1)])))
(defn validate-password
"Validates password strength.
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.cleaner
"A collection of helpers for perform cleaning of artifacts; mainly
+47 -25
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.common
"A binfile related file processing common code, used for different
@@ -27,7 +27,6 @@
[app.features.file-migrations :as fmigr]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.worker :as-alias wrk]
@@ -654,27 +653,6 @@
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
(defn invalidate-thumbnails
[cfg file-id]
(let [storage (sto/resolve cfg)
sql-1
(str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
sql-2
(str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")]
(run! #(sto/touch-object! storage %)
(sequence
(keep :media-id)
(concat
(db/exec! cfg [sql-1 file-id])
(db/exec! cfg [sql-2 file-id]))))))
(defn process-file
[cfg {:keys [id] :as file}]
(let [libs (delay (get-resolved-file-libraries cfg file))]
@@ -875,8 +853,8 @@
(defn get-resolved-file-libraries
"Get all file libraries including itself. Returns an instance of
LoadableWeakValueMap that allows do not have strong references to
the loaded libraries and reduce possible memory pressure on having
all this libraries loaded at same time on processing file validation
the loaded libraries and reduce memory pressure on having
all this libraries at the same time on processing file validation
or file migration.
This still requires at least one library at time to be loaded while
@@ -888,3 +866,47 @@
(cons (:id file)))
load-fn #(get-file cfg % :migrate? false)]
(weak/loadable-weak-value-map library-ids load-fn {id file})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXTERNAL LIBRARY RESOLUTION HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn slugify-name
"Slugify a library name for cross-environment matching.
Lowercases, replaces non-alphanumeric runs with '-', strips
leading/trailing '-'."
[name]
(str/slug name))
(def ^:private sql:get-files-names
"SELECT id, name FROM file WHERE id = ANY(?)")
(defn get-files-names
"Return [{:id uuid :name string}] for the given file ids."
[cfg ids]
(db/run! cfg
(fn [{:keys [::db/conn]}]
(let [ids-arr (db/create-array conn "uuid" ids)]
(db/exec! conn [sql:get-files-names ids-arr])))))
(def ^:private sql:get-shared-files-for-team
"SELECT f.id, f.name, f.project_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE p.team_id = ?
AND f.is_shared = true
AND f.deleted_at IS NULL
AND p.deleted_at IS NULL")
(defn get-shared-files-for-team
"Return [{:id uuid :name string}] for all shared files in a team."
[cfg team-id]
(db/run! cfg
(fn [{:keys [::db/conn]}]
(db/exec! conn [sql:get-shared-files-for-team team-id]))))
(defn find-shared-files-by-slug
"Return all shared files in `team-id` whose slugified name equals `slug`."
[cfg team-id slug]
(->> (get-shared-files-for-team cfg team-id)
(filter #(= slug (slugify-name (:name %))))))
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.migrations
"A binfile related migrations handling"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.v1
"A custom, perfromance and efficiency focused binfile format impl"
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.v2
"A sqlite3 based binary file exportation with support for exportation
+268 -44
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.binfile.v3
"A ZIP based binary file exportation"
@@ -42,6 +42,7 @@
[datoteka.io :as io])
(:import
java.io.File
java.io.FilterInputStream
java.io.InputStream
java.io.OutputStreamWriter
java.lang.AutoCloseable
@@ -67,7 +68,16 @@
[:relations {:optional true}
[:vector
[:tuple ::sm/uuid ::sm/uuid]]]])
[:tuple ::sm/uuid ::sm/uuid]]]
;; TODO: rename to :links
[:external-libraries {:optional true}
[:vector
[:map
[:id ::sm/uuid]
[:name :string]
[:slug :string]
[:used-by {:optional true} [:vector ::sm/uuid]]]]]])
(def ^:private schema:storage-object
[:map {:title "StorageObject"}
@@ -217,14 +227,12 @@
(.flush writer))
(.closeEntry output))
(defn- get-file
[{:keys [::bfc/embed-assets ::bfc/include-libraries] :as cfg} file-id]
[{:keys [::bfc/export-type] :as cfg} file-id]
(when (and include-libraries embed-assets)
(throw (IllegalArgumentException.
"the `include-libraries` and `embed-assets` are mutally excluding options")))
(let [detach? (and (not embed-assets) (not include-libraries))]
(let [detach? (= export-type :detach-libraries)
embed? (= export-type :merge-libraries)]
(db/tx-run! cfg (fn [cfg]
(cond-> (bfc/get-file cfg file-id
{:realize? true
@@ -234,7 +242,7 @@
(-> (ctf/detach-external-references file-id)
(dissoc :libraries))
embed-assets
embed?
(update :data #(bfc/embed-assets cfg % file-id))
:always
@@ -371,12 +379,34 @@
(write-entry! output path encoded-tokens)))))
(defn- export-files
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
rels (if include-libraries
[{:keys [::bfc/ids ::bfc/export-type ::output] :as cfg}]
(let [original-ids ids
ids (into ids (when (= export-type :include-libraries) (bfc/get-libraries cfg ids)))
rels (if (= export-type :include-libraries)
(->> (bfc/get-files-rels cfg ids)
(mapv (juxt :file-id :library-file-id)))
[])]
[])
;; Compute external libraries: referenced by original files but
;; not included in the export set. Only relevant for :link-later.
external-libs
(when (= export-type :link-later)
(let [original-rels (bfc/get-files-rels cfg original-ids)
lib-ids (into #{} (map :library-file-id) original-rels)]
(when (seq lib-ids)
(let [lib-names (bfc/get-files-names cfg lib-ids)]
(->> lib-names
(mapv (fn [{:keys [id name]}]
(let [slug (bfc/slugify-name name)]
(when-not (str/blank? slug)
{:id id
:name name
:slug slug
:used-by (->> original-rels
(filter #(= (:library-file-id %) id))
(mapv :file-id))}))))
(filterv some?))))))]
(vswap! bfc/*state* assoc :files (d/ordered-map))
@@ -389,12 +419,14 @@
;; Write manifest file
(let [files (:files @bfc/*state*)
params {:type "penpot/export-files"
:version 1
:generated-by (str "penpot/" (:full cf/version))
:referer "penpot"
:files (vec (vals files))
:relations rels}]
params (cond-> {:type "penpot/export-files"
:version 1
:generated-by (str "penpot/" (:full cf/version))
:referer "penpot"
:files (vec (vals files))
:relations rels}
(seq external-libs)
(assoc :external-libraries external-libs))]
(write-entry! output "manifest.json" params))))
;; --- IMPORT IMPL
@@ -430,6 +462,32 @@
[^ZipFile input ^ZipEntry entry]
(.getInputStream input entry))
(defn- size-limiting-stream
"Wraps an InputStream to enforce a maximum number of decompressed bytes.
Raises :validation :max-file-size-reached when the limit is exceeded."
^InputStream
[^InputStream input ^long max-size]
(let [counter (atom 0)
on-read (fn [n]
(when (pos? n)
(when (> (swap! counter + (long n)) max-size)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str "stream exceeded max size: " max-size))))
n)]
(proxy [FilterInputStream] [input]
(read
([]
(let [b (.read input)]
(when (pos? b) (on-read 1))
b))
([^bytes buf]
(on-read (.read input buf 0 (alength buf))))
([^bytes buf off]
(on-read (.read input buf (int off) (- (alength buf) (int off)))))
([^bytes buf off len]
(on-read (.read input buf (int off) (int len))))))))
(defn- zip-entry-reader
[^ZipFile input ^ZipEntry entry]
(-> (zip-entry-stream input entry)
@@ -438,10 +496,12 @@
(defn- zip-entry-storage-content
"Wraps a ZipFile and ZipEntry into a penpot storage compatible
object and avoid creating temporal objects"
[input entry]
(let [hash (delay (->> entry
(zip-entry-stream input)
(sto.impl/calculate-hash)))]
[input entry & {:keys [max-size]}]
(let [stream-fn (fn []
(cond-> (zip-entry-stream input entry)
max-size (size-limiting-stream max-size)))
hash (delay (->> (stream-fn)
(sto.impl/calculate-hash)))]
(reify
sto.impl/IContentObject
(get-size [_]
@@ -458,7 +518,7 @@
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ _]
(zip-entry-stream input entry))
(stream-fn))
(make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented"))))))
@@ -835,6 +895,13 @@
[{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}]
(events/tap :progress {:section :storage-objects})
;; IMPORTANT: we strongly do not reuse the main connection that can
;; run inside a transaction because the storage upload process can
;; fail in the middle of uploading and leave garbage on the underlying
;; backend, if we participate in the main transaction and it aborts
;; we will lose all registry of the pending to reconcile blobs
;; what the storage subsystem registers in other parallel
;; transaction
(let [storage (sto/resolve cfg)
entries (keep (match-storage-entry-fn) entries)]
@@ -846,9 +913,9 @@
ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext)
content (->> path
(get-zip-entry input)
(zip-entry-storage-content input))]
content (zip-entry-storage-content input
(get-zip-entry input path)
:max-size (::bfc/import-max-object-size cfg))]
(when (not= (:size object) (sto/get-size content))
(ex/raise :type :validation
@@ -858,6 +925,15 @@
:expected-size (:size object)
:found-size (sto/get-size content)))
(when-let [max (::bfc/import-max-object-size cfg)]
(when (> (sto/get-size content) max)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str "storage object exceeds maximum size: " (sto/get-size content))
:path path
:max max
:found (sto/get-size content))))
(when-let [hash (get object :hash)]
(when (not= hash (sto/get-hash content))
(ex/raise :type :validation
@@ -882,6 +958,104 @@
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
(defn- add-to-file
"Add a resolved library entry to a file in the file-grouped resolution.
`key` is :done (auto-linked) or :pending (needs resolution)."
[acc file-id file-name key entry]
(update acc file-id (fn [file]
(let [file (or file {:id file-id
:name file-name
:done []
:pending []})]
(update file key conj entry)))))
(defn- compute-link-decisions
"Returns a map of {old-lib-id -> {:library-id ... :library ...}} for external
libraries that should be auto-linked (single candidate AND importer has edit
permission). Libraries with zero or multiple candidates, or where the importer
lacks permission, are excluded their refs should remain dangling."
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/profile-id] :as cfg}]
(reduce
(fn [acc ext-lib]
(let [slug (:slug ext-lib)]
(if (nil? slug)
acc
(let [matching (into [] (bfc/find-shared-files-by-slug cfg team-id slug))]
(if (not= 1 (count matching))
acc
(let [library (first matching)
perms (bfc/get-file-permissions conn profile-id (:id library))]
(if (:can-edit perms)
(assoc acc (:id ext-lib) {:library-id (:id library)
:library library})
acc)))))))
{}
(:external-libraries manifest)))
(defn- resolve-and-link-libraries
"For each external library in the manifest, resolve candidates by slug.
Auto-links single matches (creating DB rows) and builds a file-grouped
resolution map keyed by imported file-id (new UUID)."
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/timestamp] :as cfg} files-info]
(assert (uuid? team-id) "team-id should be provided")
(let [file-ids (keys files-info)
decisions (compute-link-decisions cfg)]
(reduce
(fn [acc ext-lib]
(assert (contains? ext-lib :id) "expected `:id` on ext-lib")
(assert (contains? ext-lib :name) "expected `:name` on ext-lib")
(assert (contains? ext-lib :used-by) "expected `:used-by` on ext-lib")
(assert (contains? ext-lib :slug) "expected `:slug` on ext-lib")
(let [used-by (into #{} (map bfc/lookup-index) (:used-by ext-lib))]
(cond
;; No slug → skip
(nil? (:slug ext-lib))
acc
;; Has decision → auto-link (single match + can-edit)
(contains? decisions (:id ext-lib))
(let [{:keys [library-id]} (get decisions (:id ext-lib))
used-by (filter used-by file-ids)]
(doseq [file-id used-by]
(let [rel-params {:file-id file-id :library-file-id library-id}]
(db/insert! conn :file-library-rel rel-params
{::db/on-conflict-do-nothing? true})
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))
(let [entry {:id (:id ext-lib)
:name (:name ext-lib)
:linked-to library-id}]
(reduce (fn [acc file-id]
(add-to-file acc file-id (get files-info file-id) :done entry))
acc used-by)))
;; Has candidates but no decision → multi-match or no permission → pending
:else
(let [matching-libraries (into [] (bfc/find-shared-files-by-slug cfg team-id (:slug ext-lib)))]
(if (empty? matching-libraries)
acc
(let [candidates (mapv (fn [lib]
(let [project-id (:project-id lib)
project (bfc/get-project cfg project-id)
project-name (:name project)]
{:id (:id lib)
:name (:name lib)
:project-id project-id
:project-name project-name}))
matching-libraries)
entry {:id (:id ext-lib)
:name (:name ext-lib)
:candidates candidates}]
(reduce (fn [acc file-id]
(add-to-file acc file-id (get files-info file-id) :pending entry))
acc used-by)))))))
{}
(:external-libraries manifest))))
(defn- import-files*
[{:keys [::manifest] :as cfg}]
(bfc/disable-database-timeouts! cfg)
@@ -890,18 +1064,58 @@
(import-storage-objects cfg)
(let [files (get manifest :files)
result (reduce (fn [result file]
(let [name' (get file :name)
file (assoc file :name name')]
(conj result (import-file cfg file))))
[]
files)]
;; Pre-resolve external libraries and add their id mappings to the index
;; BEFORE importing files. This allows relink-refs (inside process-file)
;; to correctly remap :component-file references to the destination library.
;; Only remap when a link will actually be created (single match + can-edit).
(let [decisions (compute-link-decisions cfg)]
(doseq [[old-lib-id {:keys [library-id]}] decisions]
(l/trc :hint "pre-resolving external library"
:old-id (str old-lib-id)
:new-id (str library-id))
(vswap! bfc/*state* update :index assoc old-lib-id library-id)))
(let [files (get manifest :files)
file-ids (reduce (fn [result file]
(let [name' (get file :name)
file (assoc file :name name')]
(conj result (import-file cfg file))))
[]
files)
;; Build map of file-id to file-name for resolution
files-info (into {} (map (fn [file-id manifest-file]
[file-id (:name manifest-file)])
file-ids
files))]
(import-file-relations cfg)
(bfm/apply-pending-migrations! cfg)
result))
(let [resolution (resolve-and-link-libraries cfg files-info)]
(bfm/apply-pending-migrations! cfg)
{:file-ids file-ids
:resolution resolution})))
(defn- invalidate-thumbnails
[cfg file-id]
(let [storage (sto/resolve cfg ::db/reuse-conn true)
sql-1
(str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
sql-2
(str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")]
(run! #(sto/touch-object! storage %)
(sequence
(keep :media-id)
(concat
(db/exec! cfg [sql-1 file-id])
(db/exec! cfg [sql-2 file-id]))))))
(defn- import-file-and-overwrite*
[{:keys [::manifest ::bfc/file-id] :as cfg}]
@@ -926,10 +1140,11 @@
(import-storage-objects cfg)
(import-file cfg file)
(bfc/invalidate-thumbnails cfg file-id)
(invalidate-thumbnails cfg file-id)
(bfm/apply-pending-migrations! cfg)
[file-id])))
{:file-ids [file-id]
:resolution {}})))
(defn- import-files
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
@@ -940,6 +1155,15 @@
(let [manifest (-> (read-manifest input)
(validate-manifest))
entries (read-zip-entries input)
_ (when-let [max (::bfc/import-max-zip-entries cfg)]
(when (> (count entries) max)
(ex/raise :type :validation
:code :too-many-zip-entries
:hint (str "zip file has too many entries: " (count entries))
:max max
:found (count entries))))
cfg (-> cfg
(assoc ::entries entries)
(assoc ::manifest manifest)
@@ -977,12 +1201,11 @@
"Do the exportation of a specified file in custom penpot binary
format. There are some options available for customize the output:
`::bfc/include-libraries`: additionally to the specified file, all the
linked libraries also will be included (including transitive
dependencies).
`::bfc/embed-assets`: instead of including the libraries, embed in the
same file library all assets used from external libraries."
`::bfc/export-type`: determines how linked libraries are handled.
Valid values: `:include-libraries` (include linked libraries),
`:merge-libraries` (embed library assets in the file),
`:detach-libraries` (treat assets as basic objects),
`:link-later` (preserve component metadata for relinking on import)."
[{:keys [::bfc/ids] :as cfg} output]
@@ -998,6 +1221,7 @@
tp (ct/tpoint)
ab (volatile! false)
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id (str id))
(binding [bfc/*state* (volatile! (bfc/initial-state))]
+11 -2
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.config
(:refer-clojure :exclude [get])
@@ -94,7 +94,11 @@
;; SSRF protection
:ssrf-allowed-hosts #{}
:ssrf-extra-blocked-cidrs #{}})
:ssrf-extra-blocked-cidrs #{}
;; Binfile import limits
:binfile-import-max-object-size (* 1024 1024 100) ;; 100 MiB
:binfile-import-max-zip-entries (* 500 1000)}) ;; 500,000
(def schema:config
(do #_sm/optional-keys
@@ -151,6 +155,10 @@
[:media-processing-service-uri {:optional true} ::sm/uri]
[:media-processing-service-timeout {:optional true} ::sm/int]
;; Binfile import limits (PENPOT_BINFILE_IMPORT_*)
[:binfile-import-max-object-size {:optional true} ::sm/int]
[:binfile-import-max-zip-entries {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::ct/duration]
[:file-clean-delay {:optional true} ::ct/duration]
[:telemetry-enabled {:optional true} ::sm/boolean]
@@ -194,6 +202,7 @@
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
[:quotes-upload-sessions-per-profile {:optional true} ::sm/int]
[:quotes-upload-chunks-per-session {:optional true} ::sm/int]
[:quotes-media-storage-bytes-per-team {:optional true} ::sm/int]
[:auth-token-cookie-name {:optional true} :string]
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
+6 -6
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.db
(:refer-clojure :exclude [get run!])
@@ -31,8 +31,8 @@
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.HikariPoolMXBean
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
io.whitfin.siphash.SipHasher
io.whitfin.siphash.SipHasherContainer
io.whitfin.siphash.SipHash
io.whitfin.siphash.SipHashContext
java.io.InputStream
java.io.OutputStream
java.sql.Connection
@@ -701,12 +701,12 @@
;; --- Locks
(def ^:private siphash-state
(SipHasher/container
(uuid/get-bytes uuid/zero)))
(SipHash/context
(uuid/get-bytes uuid/zero)))
(defn uuid->hash-code
[o]
(.hash ^SipHasherContainer siphash-state
(.hash ^SipHashContext siphash-state
^bytes (uuid/get-bytes o)))
(defn- xact-check-param
+1 -1
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.db.sql
(:refer-clojure :exclude [update])
+3 -3
View File
@@ -2,7 +2,7 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.email
"Main api for send emails."
@@ -505,13 +505,13 @@
:schema schema:request-file-access))
(def request-file-access-yourpenpot
"File access on Your Penpot request email."
"File access on Personal Projects request email."
(template-factory
:id ::request-file-access-yourpenpot
:schema schema:request-file-access))
(def request-file-access-yourpenpot-view
"File access on Your Penpot view mode request email."
"File access on Personal Projects view mode request email."
(template-factory
:id ::request-file-access-yourpenpot-view
:schema schema:request-file-access))
Loaded 100 of 2018 files, more files were not shown because too many files have changed in this diff. Show more