Releases: Windshiftapp/core
Release list
Windshift v0.8.1
Windshift v0.8.1 — "Shiplaunch"
Windshift 0.8.1 puts your workspace in your pocket. The headline is a purpose-built mobile surface — a PWA you install to your home screen and use one-handed, with push notifications, search and create, full item detail, and an AI chat that knows what you're looking at. Underneath, this release is also a serious performance and stability pass: we hunted down the request fan-outs that made busy boards feel sluggish, replaced them with batched endpoints, and closed a cluster of Postgres concurrency bugs that could surface as 500s under load.
Highlights
Windshift on your phone
0.8.1 adds a dedicated mobile experience at /m, built for touch instead of squeezing the desktop UI onto a small screen:
- Install it like an app. An Add to Home Screen affordance uses the native install prompt where the browser supports it and walks you through it on iOS. On a phone, signing in drops you straight onto the mobile surface.
- Push notifications. Web Push means you get notified about comments, mentions, and assignments even when the app isn't open.
- Find and create from anywhere. Search your tickets, and create new ones from a floating action button and create dialog — no detour through the desktop layout.
- Real item detail. The mobile item view shows status and assignee pickers, sub-items, a parent breadcrumb to navigate up from a sub-task, the worklog timer with the full worklog dialog, and the commits/PRs and coding-agent panels. Pull to refresh to pick up the latest, and it reloads live as things change.
- AI chat that knows the context. The in-product AI chat runs full-height on mobile with the composer pinned to the bottom, and understands the work item you're viewing.
Snappier under load
Busy boards and dashboards made far too many small requests — one per card, per row, per level. 0.8.1 replaces those with batched endpoints, so a heavily populated workspace loads with a handful of requests instead of hundreds:
- Batch item fetch. A new
/items/batchendpoint kills the per-id load behindgetMany(). - Batch links. Board cards no longer fire one links request each — a single batched links endpoint (with fewer DB round-trips, and test-case links folded in) covers the whole board.
- Breadcrumbs in one call. The map builds breadcrumbs from an
/items/{id}/ancestorsendpoint instead of walking parents level by level. - Board transitions preloaded. A workspace transition-matrix endpoint loads every board's allowed transitions in one request.
- Fewer redundant loads. Collections are fetched once per load, item-type and config-set lookups are de-duplicated when an item opens, dashboard iteration progress is batched, and the portal returns field counts inline.
Steadier under pressure
A set of fixes makes Windshift hold up when several people — or several agents — hit it at once, especially on Postgres:
- No more concurrent-create 500s. Workspace item-number and fractional-index allocation are now serialized, so two people creating items at the same moment can't collide.
- Bounded connection pool. The Postgres pool is bounded and each user gets an in-flight concurrency cap, so a burst of requests can't exhaust the database with "too many clients already."
- Off-thread maintenance. Custom-field option cleanup and Postgres index builds moved off the request thread, so they no longer block the response.
- Resilient agent PRs. Opening a pull request now retries on transient failures instead of giving up.
Also in 0.8.1
- New CLI search.
ws task searchandws page searchsubcommands. - Action editor config gaps closed — the editor now exposes the backend-supported configuration fields it was missing.
- Item-attachment upload over bearer tokens — the v1 surface gained attachment upload and delete (WI-72).
Bug fixes
- The milestone status picker shows its options again when a value is pre-selected.
- Comboboxes filter on the visible label, keep their testids, and no longer close on hover or mid-transition.
- The setup flow no longer caches
setup_completedin session storage, and the post-login redirect on mobile lands on the right place.
Upgrade notes
0.8.1 is a drop-in upgrade from 0.8.0 — no new required configuration. The mobile surface is served at /m and needs no setup; Web Push uses the browser's standard notification permission. If you front Windshift with a reverse proxy, make sure it passes the PWA assets (manifest and service worker) through unchanged.
Windshift v0.8.0
Windshift v0.8.0 — "Shiplaunch"
Windshift 0.8.0 takes the coding agents introduced in 0.7.2 and turns them into a real platform. Agent runs now execute on runner pools — fleets of machines you register with one command — while every credential a run needs (Git access, model API keys, secrets) stays on the server and is never copied to a runner. Agents can continue their own pull requests, be started with a simple @mention, carry a persona and a library of skills, and reach a much bigger toolbox. And it's not only about agents: 0.8.0 adds two-way Todoist sync for personal tasks, a faster Pages module, a refreshed admin, and a long list of security and integration work.
Highlights
Run agents anywhere with runner pools
In 0.7.x, agent runs executed on the Windshift host itself. With 0.8.0 the server orchestrates runs and dispatches them to runner machines you register:
- One-command install. Mint a registration token and the dialog hands you a copy-paste command that installs and starts a runner on any machine with Docker. The runner registers itself, picks up work, streams progress back, and shuts down cleanly.
- Built for fleets. Each pool has a concurrency limit, reports its queue depth (useful as an autoscaling signal), and runs can be cancelled remotely. If a runner machine dies mid-run, Windshift notices, marks the affected runs as failed, and removes the dead runner — nothing hangs forever.
- Single host is still one command. You don't need a separate fleet to get started: the deployment bundle can co-locate a runner alongside Windshift on the same host and the same Docker network, so a one-box install behaves just like before — it's simply the first member of the same system.
Your credentials never leave the server
Runner machines — and the agents running on them — never receive your actual credentials. When a run starts, Windshift works out exactly what that run is allowed to touch, and every sensitive operation goes through the server:
- Git access is proxied: the agent can fetch its repository and push its work branch, and nothing else, without ever holding a Git token. Each push is checked branch-by-branch against what the run was granted.
- Model API calls are proxied with the provider key added server-side — no key ever enters the agent's container.
- Secrets and outbound web requests are brokered and allow-listed the same way.
A run's Git activity now also carries the right identity: when someone triggers an agent on a workspace connected via OAuth, the branch and draft pull request are created as that person, not as a shared service identity. Expiring Git OAuth tokens are refreshed automatically, so long-lived agent setups don't fail on a stale token.
Agents that continue their own pull requests
Agent work is no longer one-and-done. When you @mention a bound agent on an item that already has an open pull request, the agent continues that PR — it pushes new commits to the existing branch instead of opening a competing one, and the comment you wrote becomes its instructions for the follow-up run. The agent's closing summary is posted as a note on the pull request and as a comment on the work item, so the next reader sees what changed and why. Mention the agent on a PR comment and Windshift picks that up too.
Shape your agents
- Skills library. Each workspace can maintain a library of Markdown knowledge packs and attach them to agents. Agents see a short index of their skills and fetch the full text only when relevant — so a large library doesn't slow anything down.
- Custom instructions. Give each agent binding a persona or extra ground rules. These are added on top of the standard operating rules, never instead of them.
- @mention to start a run. Mentioning a bound agent in a comment starts a run on that item — lighter than assignment, with no assignee or status change. Duplicate mentions and self-mentions are handled sensibly, and an agent already working on the item won't be started twice.
A bigger toolbox for agents
Agents and integrations can now do far more through the API and the MCP server, with each tool gated on its own token scope (behind mcp:access):
- Links — list, add, and remove links between items, pages, and test cases.
- Tests — read test cases, start runs, and record results.
- Comments and users — edit and delete comments, and look up users (with emails and roles masked for non-admins).
- Stable addressing — every item-scoped tool now accepts a human-readable item key (e.g.
WI-42) as well as a numeric id, and agent writes are audit-tagged with their source. - Richer context — the in-product AI chat now understands the work item you're looking at, and tools expose full item descriptions with an explicit truncation marker.
The ws CLI grew to match: new ws label, ws search, and ws task history verbs, a ws task parent command with parent/children shown in ws task get, and --custom-field, --iteration, and --project flags on ws task create/edit.
See what your agents are doing
- A live Agent log tab on the work item shows the run's progress as it happens.
- Runs that hit an unrecovered tool-use failure are flagged for review with a banner that explains what went wrong, and an item-editor can re-run the agent from the same tab in one click.
- Assignment pickers now show whether an agent is actually available to take work.
- A runner pool health panel in admin Diagnostics shows each pool's runners, capacity, and queue.
- Configuration problems surface early: Windshift checks an agent's Git and model access at trigger time and fails visibly with a clear error instead of starting a run that was never going to work.
Two-way Todoist sync for personal tasks
Connect your Todoist account and keep your personal task list in step with Windshift. Tasks created, edited, or completed in either place are reflected in the other on a five-minute cycle, with a Sync now button and a last-sync status when you want it immediately. Enable it per-user under your personal settings; choose to sync everything or just a single Todoist project.
Safer networking out of the box
Agent containers run on a dedicated egress network so they can't reach anything you haven't allowed. The simplest setup needs no firewall at all — when Windshift and its runner are co-located, the network is declared internal, which lets agents reach Windshift and nothing else. For runners on a separate host, a bundled helper sets up a firewall allowlist so agents can only reach the destinations you choose. Server-side request forgery (SSRF) protection covers all outbound traffic, with a single operator switch (--allow-local-connections) for deployments that genuinely need to reach private addresses.
More than coding agents
The same dispatch system now also runs plain container jobs from action automations, so scheduled or triggered containers can execute on a runner pool too.
Smaller wins worth noting
- Faster Pages. Large page trees load and scroll noticeably faster — bodies are kept out of the tree query and only the visible rows are rendered.
- Refreshed workspace admin. Settings move from tabs to a folded sidebar, and agent bindings and skills are now managed through consistent modal dialogs.
- Command palette: recently viewed. Open the palette and the first entry jumps you into your recently viewed work items.
- Desktop app. Settings and About now open as in-app modals, including a Pomodoro settings panel.
- New model provider. Z.AI's Coding Plan endpoint is selectable alongside the existing Z.AI provider.
- Subpath deployments. Windshift can be served under a path prefix (for example
https://example.com/windshift/) behind a reverse proxy.
Also in 0.8.0
- Built for integrations — the REST API is ready to back third-party embeds: a
collections:readtoken scope for read-only report access, a ready-made entry in the OAuth client setup, and a newexclude_personalAPI parameter so integrations can guarantee your personal-workspace items never appear on shared surfaces. - Asset management API & CLI — assets get full REST endpoints, token scopes, CSV import, and new
ws assetcommands (deleting via API is opt-in). - Jira import grows — the importer now covers Jira/Insight assets, boards and sprints, and custom fields; a migration-readiness analyser reports what will and won't come across; and an import can be re-run safely without creating duplicates.
- Time tracking integrity — one active timer per user is now enforced, worklogs require permission to view the item, and several permission gaps in projects and categories were closed.
Security
This release includes fixes from dedicated security reviews — of the remote runner system and across the whole codebase:
- Runner registration tokens are single-use, expire by default, and require HTTPS; each runner gets its own credential.
- A finished run can't be tampered with afterwards: late events and repeated finalization are rejected, and Git pushes are checked branch-by-branch against what the run was granted.
- OAuth hardening: the authorization flow now requires PKCE
S256(plain and omitted methods are rejected), disabling an OAuth client immediately revokes the tokens issued through it, and public decode paths are size-capped. - Attachment downloads are confined to the storage root (no symlink or
..escapes) and filenames are formatted safely intoContent-Disposition. - SSRF protection now covers all outbound traffic, with one explicit operator switch (
--allow-local-connections) for deployments that genuinely need to reach private addresses. - Portal magic-link tokens are stored hashed, secrets no longer appear in the host process table, and several cross-workspace information leaks were closed.
- User input is consistently sanitized across ...
Windshift v0.7.2
Windshift v0.7.2 — "Shiplaunch"
Windshift 0.7.2 introduces the coding-agent harness — Windshift can now run a coding agent against a work item on assignment — alongside CLI improvements that make Windshift a first-class backend for external task tools.
Highlights
Coding-agent harness
Windshift can spawn a per-assignment coding agent in an isolated git worktree with the ws CLI, pi, and the task extension baked in. The harness covers the full round trip: a walking-skeleton runner and worktree manager, per-run minted tokens, pi RPC plumbing and production runner wiring, workspace bindings with an assignee-change trigger, and SCM round-trip + pull-request creation against both GitHub and Gitea. An acting-identity security gate, a global-admin security surface, and a centralized-service-user allowlist (with a reason-input dialog on the Security page) govern which identities an agent may act as. A runs HTTP API and JS clients back the harness UI.
Workspace-scoped priority listing for integrations
New ws priority ls command and GET /rest/api/v1/workspaces/{id}/priorities endpoint expose a workspace's priority catalog (id, name, sort order, default), scoped to the workspace's configuration set and mirroring ws item-type ls / ws status ls. This lets external task tools resolve priority names to ids for the right workspace — for example, the pi-tasks Windshift backend uses it to offer priority editing.
Improvements
- LLM model refresh now covers all providers, with a new diagnostics widget.
- Board view supports swimlane grouping by item type.
- The request portal persists in-progress request drafts.
Bug fixes
- SSO: allow configured private OIDC endpoints.
- Activity: use a window function for cleanup limits (Postgres-safe).
- Items: color linked-item status badges.
Upgrade notes
- New agent-run and workspace-agent-binding tables are created automatically on startup.
- The coding-agent harness is inactive until a workspace has an agent binding configured; running agents also requires Docker on the runner host.
ws priority lsand the new/workspaces/{id}/prioritiesendpoint require the v0.7.2 server; older clients are unaffected.
Windshift v0.7.1
Windshift v0.7.1 — "Knowledge Base"
Windshift 0.7.1 introduces workspace Pages: a built-in knowledge base for specs, runbooks, design notes, and long-lived project documentation. Pages are available in the web app, REST API, ws CLI, links, and AI agent tooling.
Highlights
Workspace Pages
Create Markdown pages inside each workspace, organize them into a tree, attach labels, and maintain immutable revision history. Pages are designed for team documentation that should live alongside work items instead of being scattered across external files.
Page permissions and revision recovery
Pages support per-page ACLs, inheritance toggles, and admin-gated archive/restore flows. Revision history can be inspected and restored, including recovery of archived pages.
CLI and API support
The ws page and ws page-label commands cover page creation, editing, moving, archiving, labels, history, restore, permissions, and inheritance. The v1 bearer-token API now exposes matching page, revision, ACL, and label endpoints for automation.
AI agent page tools
Built-in agents can now search, read, create, update, move, archive, restore, and manage permissions for workspace Pages, subject to the same workspace roles and page ACLs as human users.
Improvements
- Pages can be linked to work items using the Page link type.
- Page labels are included in CLI/API page responses and can be filtered from the CLI.
- Page history is paginated and available to automation clients.
- Page archive now records revision entries for affected subtree pages.
- Archived page content is removed from the search/chunk index until restored.
- Agent tokens minted by default include page scopes.
Bug fixes
- Fixed the page history drawer showing an empty state despite existing revisions.
- Fixed restore so archived pages can be recovered by authorized admins.
- Fixed v1 page responses advertising a permissions URL that was not implemented.
- Fixed restore revision lookup to stay inside the restore transaction.
- Closed an archive permission-check race by authorizing the locked subtree inside the archive transaction.
- Fixed CLI structured JSON output dropping page
_links. - Fixed frontend Pages type-check warnings.
Upgrade notes
- New page, revision, ACL, chunk, and label tables are created automatically on startup.
- Existing agent/CLI tokens may need to be re-minted if they predate page scopes and need access to Pages.
- Archive is still a soft-delete operation. Restoring a revision unarchives the addressed page; subtree recovery should be performed page-by-page where needed.
Windshift v0.7.0
Windshift v0.7.0 — "Shiplaunch"
Suitable for small-scale production use.
Windshift is maturing and can now support small teams in production. APIs, data formats, and configuration may still change between releases, so please keep backups and test upgrades before rolling them out.
Shiplaunch is a polish and platform release. It adds safer automation credentials, passkey login for portal customers, richer milestone planning, and a smoother AI-assisted workflow.
Highlights
Safer automation credentials
Automation recipes no longer need pasted tokens in each action. Store credentials once, choose them from a picker, and keep sensitive values out of action definitions.
AI-assisted action editing
The AI agent can now inspect and update actions for you. The editor refreshes as changes happen, and several action-node save, delete, and execution issues have been fixed.
Portal passkeys
Portal customers can register a passkey and sign in without waiting for an email link.
Items can belong to multiple milestones
Items are no longer limited to one milestone. This makes roadmap, release, and sprint planning more flexible across the app and CLI.
Portable configuration sets
Configuration sets can be exported and imported between Windshift instances, making setup and migration easier.
Better command palette
The command palette has been rebuilt with grouped results and better workspace-aware navigation.
More capable integrations
- Jira import now handles both scoped and legacy API tokens more clearly.
- SCM workflows can create milestone automation from tags.
- OpenRouter is available as an LLM provider with a refreshable model list.
Recent improvements
- Image paste and upload now works from the create-item screen.
- AI chat refreshes open views as soon as an agent finishes work.
- Backlog filters and board column limits behave more reliably.
- Item history now shows clearer AI agent attribution.
- Admin user creation includes an Active toggle.
- Iteration tables have been tightened up visually, including non-wrapping type badges.
Security and reliability
This release includes a broad hardening pass:
- Automation runs with stricter network isolation by default.
- Portal sessions are tied to the correct portal channel.
- Hidden portal request types and custom fields are enforced server-side.
- Attachment access is checked more consistently across item and branding uploads.
- Deactivated users stop authenticating immediately.
- Long-running background jobs shut down more cleanly and recover from panics.
- Failed migrations now stop startup instead of being logged and skipped.
- Releases include signed packages, checksums, provenance, and vulnerability checks.
Bug fixes
This release also fixes issues around portal customer access, item moves, inactive workspace selectors, profile labels, system theme handling, markdown rendering, action-editor URLs, collection builder state, admin modal shortcuts, test folders, notification handling, and frontend type-check warnings.
Upgrade notes
- Back up before upgrading. Items now use
milestone_idsinstead of a singlemilestone_id; the migration runs on first start. - Action credentials use the existing SSO secret for encryption. Existing inline credentials still work, but the admin scanner will help you move them into the credential vault.
- Action networking is stricter. Capabilities that relied on unrestricted host networking may need explicit egress configuration.
- Default configuration sets are protected. They cannot be exported or overwritten during import.
- Portal passkey tables are created automatically. No manual setup is required.
Windshift v0.6.1
Windshift v0.6.1
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
A point release covering approval workflow fixes, surfacing approval activity in the item Comments and History tabs, attachment previews with a lightbox, consolidation of the in-app and MCP AI tool surfaces, and a permission tightening for MCP.
Features
Approval activity in Comments and History
Approval decisions and any comments attached to them now appear inline with regular comments and history.
- The History tab lists each approval decision (
requested,approve,reject,cancel,comment,delegate,escalate,completed) in chronological order alongside field changes. The tab uses a one-line per entry layout with a smaller avatar and the full timestamp on hover. - The Comments tab includes the comment text from approve, reject, comment, and cancel decisions. Approval-sourced rows show a Shield icon. Comments authored by users with
is_agent = trueshow a Bot icon. Both icons have tooltips. Edit and delete are restricted to rows authored by a human. - AI chat has a new
get_item_approvalstool that returns the request status, the approver pool, and the decision audit trail with user names resolved.
Attachment previews and lightbox
Image attachments render a 40x40 thumbnail using the existing /api/attachments/{id}/thumbnail endpoint. PDFs render a 40x40 tile labeled PDF. Clicking either opens a fullscreen lightbox: images at full resolution, PDFs in the browser's built-in viewer via iframe. Backdrop click, the close button, or the Escape key dismisses the lightbox.
Approval card UX
Approve, Reject, and Cancel use the existing ConfirmDialog component instead of window.confirm. The decision buttons sit above the optional comment box and wrap on narrow sidebars. After a decision is recorded, the item is reloaded so status_id reflects the new server-side state.
AI tool registry consolidation
The in-app LLM agent and the external MCP server now share a single tool registry under internal/aitools/. Each tool is defined once with a typed Args struct and a Run function. The two adapters translate that into their respective protocol shapes. Schemas are derived once at registration via google/jsonschema-go.
Tools added to the MCP surface in this release: get_item_approvals, transition_item, list_milestones, list_iterations, list_custom_fields, list_recent_activity, and log_time (string durations such as "1h30m").
Tools added to the in-app LLM agent: create_item, delete_item, get_item_children, list_labels, set_item_labels, start_timer, stop_timer.
Bug fixes
On-leave approver with no substitute
When an approval step used on_leave_strategy = use_substitute and the only resolved approver was on an active leave without a configured substitute_user_id, the engine dropped the approver and snapshotted an empty pool. The request opened but no one could act on it. The engine now keeps the original approver in the pool when no substitute is configured. Existing stuck requests can be unstuck by cancelling them and re-triggering the transition.
Security hardening
MCP applies item.view per workspace
The MCP adapter built its workspace access list via repository.GetAccessibleWorkspaceIDs, which returns every active non-personal workspace unconditionally. For workspaces with explicit role assignments, a non-member would fail permission_service.HasWorkspacePermission(item.view) but the workspace was still in the list. Registry-driven read tools (get_item, search_items, get_item_children, list_comments, get_item_approvals) returned data the deleted MCP per-family handlers used to block via canViewItem.
The MCP env now lists active workspaces and keeps only those where HasWorkspacePermission(item.view) succeeds. Bearer tokens with mcp:access scope can no longer read items, comments, or approvals from gated workspaces they are not a member of.
Upgrade notes
- No schema migrations and no config changes.
- MCP
tools/listadvertises additional tools. Existing clients are unaffected.
Windshift v0.6.0 — "Formation"
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
If you encounter issues or have ideas, please open an issue. Your feedback at this stage is incredibly valuable.
This release is about organising the people who do the work. Teams ships its full UI on top of the backend that landed late in 0.5.x — on-call schedules, rotation layers, manual overrides, swap requests, and per-team identity. OAuth 2.0 with authorisation-code-plus-PKCE turns Windshift into a proper identity provider for third-party integrations. Labels become first-class for personal and shared use. Underneath, the codebase gets its largest cleanup pass to date.
Features
Teams and on-call
The Teams backend that landed in 0.5.x is now wired end-to-end. /teams is a routed page in the main nav with list and detail views, gated by the teams.manage global permission and a per-team admin role.
The detail view is a tabbed shell:
- Overview — inline-edit name and description, plus an Identity card with icon picker, colour, and optional avatar upload.
- Members — direct members with role select, a
UserPickerfor staged adds, and a resolved-members table that flags who is on leave and surfaces their substitute. - Groups — mapped groups with
GroupPickerfor staged attachments. Member counts now reflect direct + group-resolved correctly. - On-call — full schedule CRUD with rotation layers, manual overrides, swap requests, and a "Currently on-call" card per schedule.
Escalation policies are deferred to a future release — they need notification-service wiring before they would actually dispatch.
Profile leave with on-call substitute
/profile gets a Leave tab so any user can manage their own leave windows. The form takes start, end, optional notes, and an optional substitute. The substitute is the on-call coverage piece: when a member is on leave during their shift, the team's "Currently on-call" card resolves to whoever they nominated, so the on-call view always reflects who is actually reachable.
OAuth 2.0 server
Windshift now stands up its own OAuth 2.0 server so third-party applications can act on a user's behalf via a Windshift-issued agent identity. The flow is authorisation-code with PKCE (mandatory for public clients), refresh-token rotation with hashed storage and replay-cascade revocation, and exact redirect_uri matching against per-client allowlists. Authorisation codes are short-lived and single-use; granted scopes are intersected with each client's allowlist so a client cannot request more than it was registered for.
A new sysadmin page at /admin/oauth-clients exposes the full client lifecycle — create, list, edit, rotate secret, delete. Secrets are bcrypt-hashed; the plaintext is shown exactly once on create or rotate and is never echoed back afterwards.
The user-facing consent page at /oauth/authorize shares its component with the existing CLI-authorise page, and both renderers escape the client display name, so an admin who can register a client cannot smuggle markup into the consent screen.
Personal and shared labels
Labels are now a first-class organisational primitive, with separate personal and shared scopes. Personal labels are private to a user; shared labels live within a workspace and respect its permission model. Items, lists, and CQL queries all understand the new shape.
Admin-editable email templates
Transactional emails are no longer hardcoded. Admins can edit subject, HTML body, and plain-text body for magic_link, email_verification, invitation, portal_reply, and notification_batch. Senders look up the template by name at send time and fall back to the embedded defaults if no row exists, so an empty install ships with sane copy. The admin EmailTemplateManager page renders a live preview that runs the same enrichment pipeline the production sender uses.
The same surface ships substantial channel-security hardening:
- A shared SSRF-safe dialer (extracted from the IMAP guard) is now used by SMTP dispatch, the channel-test endpoint, and webhook HTTP clients. This closes the validate-then-dial DNS-rebinding window.
- SMTP and IMAP passwords are encrypted at rest in the channel config. Legacy plaintext rows continue to work, so deployments can encrypt rolling without re-issuing every channel.
- Webhook URLs are validated on save (defense-in-depth on top of the existing send-time check).
- An empty or typoed encryption-mode is rejected with a clear error rather than silently degrading to plaintext AUTH PLAIN.
Homepage dashboard widgets
The homepage now has a proper dashboard layout. A new Personal Tasks widget surfaces items from the user's personal workspace alongside the existing cross-workspace Assigned to me widget. The outer section-card wrapper is removed so widgets sit directly on the page surface — the card-in-card nesting from the previous refactor is gone, and the dashboard reads as one cohesive view.
Collections visual builder state
Collections now persist their visual builder state separately from the CQL string. A collection saved in builder mode reopens in builder mode without best-effort repar...
Windshift v0.6.0
Windshift v0.6.0 — "Formation"
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
If you encounter issues or have ideas, please open an issue. Your feedback at this stage is incredibly valuable.
This release is about organising the people who do the work. Teams ships its full UI on top of the backend that landed late in 0.5.x — on-call schedules, rotation layers, manual overrides, swap requests, and per-team identity. OAuth 2.0 with authorisation-code-plus-PKCE turns Windshift into a proper identity provider for third-party integrations. Labels become first-class for personal and shared use. Underneath, the codebase gets its largest cleanup pass to date.
Features
Teams and on-call
The Teams backend that landed in 0.5.x is now wired end-to-end. /teams is a routed page in the main nav with list and detail views, gated by the teams.manage global permission and a per-team admin role.
The detail view is a tabbed shell:
- Overview — inline-edit name and description, plus an Identity card with icon picker, colour, and optional avatar upload.
- Members — direct members with role select, a
UserPickerfor staged adds, and a resolved-members table that flags who is on leave and surfaces their substitute. - Groups — mapped groups with
GroupPickerfor staged attachments. Member counts now reflect direct + group-resolved correctly. - On-call — full schedule CRUD with rotation layers, manual overrides, swap requests, and a "Currently on-call" card per schedule.
Escalation policies are deferred to a future release — they need notification-service wiring before they would actually dispatch.
Profile leave with on-call substitute
/profile gets a Leave tab so any user can manage their own leave windows. The form takes start, end, optional notes, and an optional substitute. The substitute is the on-call coverage piece: when a member is on leave during their shift, the team's "Currently on-call" card resolves to whoever they nominated, so the on-call view always reflects who is actually reachable.
OAuth 2.0 server
Windshift now stands up its own OAuth 2.0 server so third-party applications can act on a user's behalf via a Windshift-issued agent identity. The flow is authorisation-code with PKCE (mandatory for public clients), refresh-token rotation with hashed storage and replay-cascade revocation, and exact redirect_uri matching against per-client allowlists. Authorisation codes are short-lived and single-use; granted scopes are intersected with each client's allowlist so a client cannot request more than it was registered for.
A new sysadmin page at /admin/oauth-clients exposes the full client lifecycle — create, list, edit, rotate secret, delete. Secrets are bcrypt-hashed; the plaintext is shown exactly once on create or rotate and is never echoed back afterwards.
The user-facing consent page at /oauth/authorize shares its component with the existing CLI-authorise page, and both renderers escape the client display name, so an admin who can register a client cannot smuggle markup into the consent screen.
Personal and shared labels
Labels are now a first-class organisational primitive, with separate personal and shared scopes. Personal labels are private to a user; shared labels live within a workspace and respect its permission model. Items, lists, and CQL queries all understand the new shape.
Admin-editable email templates
Transactional emails are no longer hardcoded. Admins can edit subject, HTML body, and plain-text body for magic_link, email_verification, invitation, portal_reply, and notification_batch. Senders look up the template by name at send time and fall back to the embedded defaults if no row exists, so an empty install ships with sane copy. The admin EmailTemplateManager page renders a live preview that runs the same enrichment pipeline the production sender uses.
The same surface ships substantial channel-security hardening:
- A shared SSRF-safe dialer (extracted from the IMAP guard) is now used by SMTP dispatch, the channel-test endpoint, and webhook HTTP clients. This closes the validate-then-dial DNS-rebinding window.
- SMTP and IMAP passwords are encrypted at rest in the channel config. Legacy plaintext rows continue to work, so deployments can encrypt rolling without re-issuing every channel.
- Webhook URLs are validated on save (defense-in-depth on top of the existing send-time check).
- An empty or typoed encryption-mode is rejected with a clear error rather than silently degrading to plaintext AUTH PLAIN.
Homepage dashboard widgets
The homepage now has a proper dashboard layout. A new Personal Tasks widget surfaces items from the user's personal workspace alongside the existing cross-workspace Assigned to me widget. The outer section-card wrapper is removed so widgets sit directly on the page surface — the card-in-card nesting from the previous refactor is gone, and the dashboard reads as one cohesive view.
Collections visual builder state
Collections now persist their visual builder state separately from the CQL string. A collection saved in builder mode reopens in builder mode without best-effort reparsing the raw query. Legacy collections still open in raw mode, with a "Reset to builder" toggle when needed. iteration is also added to the default system screen fields so the iteration picker appears on the default item-detail screen without manual screen configuration.
Security hardening
Fail-closed primitives
Three code paths that used to swallow setup or configuration errors now fail closed:
- Sessions reject the request when there is no client IP, instead of skipping the IP-binding check.
- OIDC state lookup rejects expired or missing state, instead of proceeding as though validation had passed.
- Failed-login audit rows hash the attempted identifier rather than logging it in the clear, so the audit table cannot itself be a source of credential leakage.
SSO and SAML
- The SSO secret-encryption key is now derived via HKDF rather than raw SHA-256, with the SHA-256 path retained as a fallback so existing encrypted configurations keep working through a rolling rotation.
API token expiry default
API tokens minted by non-admin users now default to a 90-day expiry when the request omits one. Admin-issued tokens and any token with an explicit expiry are unchanged. The change closes the case where a user could create a perpetual token by simply not setting an expiry.
Tests page
- "All Tests" replaces "No Folder" as the lead entry. The previous lead counted only unassigned cases and was offset slightly to the left, making sibling folders look nested. "All Tests" counts every case in the workspace and aligns with root folders.
- Folder collapse fixed. Folders now collapse and expand reliably, and the collapse chevron is no longer an invalid nested button.
App and infrastructure
setup_completedcached insessionStorage. Every cold app load was hitting the rate-limited status endpoint just to check whether the install was past first-run. The flag now caches after the first hit, dropping a request from every navigation.- Welcome page hotkeys use the standard
keyboardHintprop onButton, so the rendering matches the rest of the app. - Channels: handler logic pushed into the service layer. The same operations are now callable from internal flows without going through HTTP.
- Jira import — round-1 bug-hunt fixes. Field mapping, attachment fetch, and worklog import all behave more predictably on real exports.
- E2E: dialog/picker z-index layering and Playwright-targetable testids. Pickers no longer disappear behind their host dialogs, and the headline frontend surfaces have stable test hooks.
Upgrade notes
- Logbook is not bundled in this release. This is because of a license change on the Kreuzberg Library used by us. The Docker image and
docker-compose.ymlno longer include the logbook binary. Existing deployments that rely on the bundled logbook should pin to v0.5.9 until logbook ships again. - OAuth 2.0 server is enabled by default but has no clients out of the box. Visit Admin → OAuth Clients to register clients. Sysadmin permission is required.
- API token default expiry for non-admin tokens is now 90 days. Existing tokens already in the database are unchanged on upgrade.
is_activeis no longer accepted on create-user. Integration scripts that set it should drop the field — it is silently ignored either way.- Five unused tables dropped from the schema. No migration runs against existing databases; rows (if any) remain in place but the application no longer references them.
- Email channel passwords are encrypted at rest going forward. Existing plaintext rows continue to work; new and edited channels are written encrypted. No manual rotation step is required.
Windshift v0.5.9
Windshift v0.5.9
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
If you encounter issues or have ideas, please open an issue. Your feedback at this stage is incredibly valuable.
Features
Smart commits on pull-request merge
When a linked pull request transitions to merged during the periodic SCM sync, Windshift now parses Jira-style smart-commit directives out of the PR body and each commit message, and applies them against the detected work items.
Two directives are recognised — #comment <text> posts the remainder of the line as a comment, and #<transition-slug> runs the named workflow transition (slugs match statuses case-insensitively with dashes in place of spaces). A single line can reference multiple item keys and multiple commands; the cross-product is applied, mirroring Jira's semantics. Hashes inside URLs are skipped via a word-boundary check.
Each command runs under the committer, not the triggering user. The committer's git email is resolved to an internal user with item.edit on the workspace; transitions fire through WorkflowService.PerformTransition so conditions are still enforced, and comments go through CommentService.Create with the normal mention + notification flow. If the committer email doesn't map to a user, or that user lacks item.edit, the directive is skipped.
Idempotency is persistent, not in-memory — the PR body is marked as processed via item_scm_links.smart_commits_applied_at, and each commit SHA is recorded in a new scm_processed_commits ledger so that a sync tick or container restart never re-applies the same action twice.
Because git does not authenticate the committer email (a contributor can set it to any value), smart-commit processing is off by default and must be enabled per SCM connection via Workspace settings → SCM → Enable smart commits. The toggle renders a yellow callout that spells out the trust assumption so the choice is knowing.
Segmented SCM development panel
The work-item SCM panel used to render pull requests, branches, and commits as one flat list, which scanned badly once the item had more than a handful of links. The panel is now split into three labelled sub-sections with their own count chips, and a rollup status lozenge next to the pull-requests header (OPEN if any are open, else MERGED if any merged, else DECLINED). Mirrors the semantics of Jira's development panel.
Live comments via shared notification bus
Comments on a work item used to refresh only on the item's own poll tick, so a comment or mention from another user could take up to 30s to show up. The notifications store now carries an app-wide adaptive poller (30s active / 5m idle / paused on hidden tab) and exposes a pub/sub bus that emits each newly-arrived notification exactly once. The comments view subscribes to that bus and refreshes the thread the moment a comment or mention notification arrives for the open item. If the user is scrolled away from the bottom when new comments land, a small count badge tracks the arrivals until it's clicked.
The underlying polling loop has been extracted into a reusable usePoller composable so the work-item poller and the new notification poller share a single cadence + activity-tracking implementation.
SCIM explicit disconnect
Revoking a SCIM token used to leave every scim_managed user and group stuck in that state forever: admin-side edits and deletes were blocked by the SCIM guards, and no path cleared the flag at scale. Two new endpoints address this:
GET /admin/scim/disconnect-previewreturns the counts so the UI can render a confirmation such as "this will release N users and M groups".POST /admin/scim/disconnecttransactionally revokes every active SCIM token, clearsscim_managed/scim_external_idon every user, group, and group membership, and writes an audit entry.
Per-token revocation keeps its current semantics, so rotating credentials without disconnecting remains a single-token operation.
Security hardening
Four write paths that previously trusted the request body for their scope have been rebound to URL-derived scope so a caller cannot cross workspace or channel boundaries by editing a payload.
Channel-scoped request type and asset report writes
PUT / DELETE on request types, their fields, their visibility, and the asset-report equivalents were behind auth() only — any logged-in user could update the field schema of any request type, delete arbitrary asset reports, or re-point either resource at a different workspace via the body's workspace_id. UpdateVisibility was nominally wrapped in channelMgmt, but the middleware was reading the request-type / asset-report id as though it were a channel id, so the gate never fired correctly.
All writes now live under /channels/{channel_id}/...:
PUT | DELETE /channels/{channel_id}/request-types/{id}
PUT /channels/{channel_id}/request-types/{id}/fields
PUT /channels/{channel_id}/request-types/{id}/visibility
PUT | DELETE /channels/{channel_id}/asset-reports/{id}
PUT /channels/{channel_id}/asset-reports/{id}/fields
PUT /channels/{channel_id}/asset-reports/{id}/visibility
Each handler reads channel_id from the URL, scopes its existence check and SQL UPDATE / DELETE with WHERE id = ? AND channel_id = ?, and returns 404 on zero rows affected. workspace_id is dropped from every SET clause so a body cannot reposition the resource. Reads (GET .../{id} and friends) stay flat since the existing handlers don't expose a write surface.
Workspace-scoped milestone and iteration writes
PUT /milestones/{id} and PUT /iterations/{id} decoded the request body and then checked workspace permission against the value the caller supplied — a user with edit on workspace B could send PUT /milestones/{X_in_A} with body {"workspace_id": B} and the permission check passed against B while the UPDATE ran on milestone X. The same body was then written back, so the milestone could also be relocated into the attacker's workspace.
Write paths now live under URL scopes the existing middleware already gates:
PUT /workspaces/{workspaceId}/milestones/{id} (workspaceItemEdit)
PUT /global/milestones/{id} (RequireGlobalPermission)
Iterations mirror the same shape. workspace_id / is_global are ignored on the body and dropped from the SET clause, so moving a milestone or iteration between scopes is no longer possible through Update. Zero rows affected surfaces as 404, collapsing the cross-scope hijack into the same response as a stale id.
Form submission request-type enforcement + safe embed
SubmitForm previously accepted submissions without a request_type_id — the fallthrough created a generic item bypassing per-form require_auth, field validation, and item-type resolution. It now rejects a missing request_type_id with 400 and verifies the referenced request type belongs to the form's channel in a single query. An unparseable config JSON is treated as an empty config rather than failing the whole submission.
On the rendering side, PublicFormPage / FormRenderer now read brand, theme, and logo from the flattened public-channel fields the backend actually serves (channel.theme, channel.brand_color, channel.logo_url), and the submit button label comes from formConfig.submit_button_text so channel customisation actually flows through. When the form is loaded inside an iframe (?embed=...), the page measures its document height on every layout and posts ws-form-resize to the parent so the widget-host iframe can match its height to the content.
The security-headers middleware now sets CSP frame-ancestors: * and omits X-Frame-Options for the /forms/* path prefix so customers can embed those pages on their own websites. All other routes keep SAMEORIGIN framing.
SCIM write guards on non-SCIM users
The SCIM handlers took an internal user id and happily operated on any row, including local or admin accounts the IdP never provisioned. That meant a leaked or misconfigured SCIM credential could deactivate any user by id. DeleteUser, PatchUser, and ReplaceUser now check scim_managed up front — non-managed users get 404 with an audit entry recording the refusal reason. POST's adoption-by-email path is untouched, so the legitimate route to bring a local user under SCIM management still works.
Enhancements
Time tracking
- Time Entry and Time Reports summary rows (Total Time: Xh) now sit on
--ds-surfaceto match the table header, instead of the semi-transparent neutral background that read as a detached block and looked off in dark mode. - The daily-hours chart pulls its line colour from
--ds-accent-blueinstead of a hard-coded#3b82f6, so the chart tracks the theme.
Collections board
- On the board view, the
(N remaining)count next to the Load more button is now hidden while a sprint filter is applied — the count reflects the unfiltered collection total, so showing it while filtering was misleading.
Workspace context chrome
- When a workspace gradient is active, the navigation reads interactive-active and inactive text colours against the gradient rather than falling back through
--ds-text(which becomes near-white-on-white in some gradient presets and near-invisible). The glass nav now uses explicit white + translucent-white values so legibility is consistent regardless of gradient.
SCIM offboarding notification
- The admin notification for a SCIM-initiated deactivation always said "was deactivated via SCIM" regardless of whe...
Windshift v0.5.8
Windshift v0.5.8
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
If you encounter issues or have ideas, please open an issue. Your feedback at this stage is incredibly valuable.
Features
Smart commits on pull-request merge
When a linked pull request transitions to merged during the periodic SCM sync, Windshift now parses Jira-style smart-commit directives out of the PR body and each commit message, and applies them against the detected work items.
Two directives are recognised — #comment <text> posts the remainder of the line as a comment, and #<transition-slug> runs the named workflow transition (slugs match statuses case-insensitively with dashes in place of spaces). A single line can reference multiple item keys and multiple commands; the cross-product is applied, mirroring Jira's semantics. Hashes inside URLs are skipped via a word-boundary check.
Each command runs under the committer, not the triggering user. The committer's git email is resolved to an internal user with item.edit on the workspace; transitions fire through WorkflowService.PerformTransition so conditions are still enforced, and comments go through CommentService.Create with the normal mention + notification flow. If the committer email doesn't map to a user, or that user lacks item.edit, the directive is skipped.
Idempotency is persistent, not in-memory — the PR body is marked as processed via item_scm_links.smart_commits_applied_at, and each commit SHA is recorded in a new scm_processed_commits ledger so that a sync tick or container restart never re-applies the same action twice.
Because git does not authenticate the committer email (a contributor can set it to any value), smart-commit processing is off by default and must be enabled per SCM connection via Workspace settings → SCM → Enable smart commits. The toggle renders a yellow callout that spells out the trust assumption so the choice is knowing.
Segmented SCM development panel
The work-item SCM panel used to render pull requests, branches, and commits as one flat list, which scanned badly once the item had more than a handful of links. The panel is now split into three labelled sub-sections with their own count chips, and a rollup status lozenge next to the pull-requests header (OPEN if any are open, else MERGED if any merged, else DECLINED). Mirrors the semantics of Jira's development panel.
Live comments via shared notification bus
Comments on a work item used to refresh only on the item's own poll tick, so a comment or mention from another user could take up to 30s to show up. The notifications store now carries an app-wide adaptive poller (30s active / 5m idle / paused on hidden tab) and exposes a pub/sub bus that emits each newly-arrived notification exactly once. The comments view subscribes to that bus and refreshes the thread the moment a comment or mention notification arrives for the open item. If the user is scrolled away from the bottom when new comments land, a small count badge tracks the arrivals until it's clicked.
The underlying polling loop has been extracted into a reusable usePoller composable so the work-item poller and the new notification poller share a single cadence + activity-tracking implementation.
SCIM explicit disconnect
Revoking a SCIM token used to leave every scim_managed user and group stuck in that state forever: admin-side edits and deletes were blocked by the SCIM guards, and no path cleared the flag at scale. Two new endpoints address this:
GET /admin/scim/disconnect-previewreturns the counts so the UI can render a confirmation such as "this will release N users and M groups".POST /admin/scim/disconnecttransactionally revokes every active SCIM token, clearsscim_managed/scim_external_idon every user, group, and group membership, and writes an audit entry.
Per-token revocation keeps its current semantics, so rotating credentials without disconnecting remains a single-token operation.
Security hardening
Four write paths that previously trusted the request body for their scope have been rebound to URL-derived scope so a caller cannot cross workspace or channel boundaries by editing a payload.
Channel-scoped request type and asset report writes
PUT / DELETE on request types, their fields, their visibility, and the asset-report equivalents were behind auth() only — any logged-in user could update the field schema of any request type, delete arbitrary asset reports, or re-point either resource at a different workspace via the body's workspace_id. UpdateVisibility was nominally wrapped in channelMgmt, but the middleware was reading the request-type / asset-report id as though it were a channel id, so the gate never fired correctly.
All writes now live under /channels/{channel_id}/...:
PUT | DELETE /channels/{channel_id}/request-types/{id}
PUT /channels/{channel_id}/request-types/{id}/fields
PUT /channels/{channel_id}/request-types/{id}/visibility
PUT | DELETE /channels/{channel_id}/asset-reports/{id}
PUT /channels/{channel_id}/asset-reports/{id}/fields
PUT /channels/{channel_id}/asset-reports/{id}/visibility
Each handler reads channel_id from the URL, scopes its existence check and SQL UPDATE / DELETE with WHERE id = ? AND channel_id = ?, and returns 404 on zero rows affected. workspace_id is dropped from every SET clause so a body cannot reposition the resource. Reads (GET .../{id} and friends) stay flat since the existing handlers don't expose a write surface.
Workspace-scoped milestone and iteration writes
PUT /milestones/{id} and PUT /iterations/{id} decoded the request body and then checked workspace permission against the value the caller supplied — a user with edit on workspace B could send PUT /milestones/{X_in_A} with body {"workspace_id": B} and the permission check passed against B while the UPDATE ran on milestone X. The same body was then written back, so the milestone could also be relocated into the attacker's workspace.
Write paths now live under URL scopes the existing middleware already gates:
PUT /workspaces/{workspaceId}/milestones/{id} (workspaceItemEdit)
PUT /global/milestones/{id} (RequireGlobalPermission)
Iterations mirror the same shape. workspace_id / is_global are ignored on the body and dropped from the SET clause, so moving a milestone or iteration between scopes is no longer possible through Update. Zero rows affected surfaces as 404, collapsing the cross-scope hijack into the same response as a stale id.
Form submission request-type enforcement + safe embed
SubmitForm previously accepted submissions without a request_type_id — the fallthrough created a generic item bypassing per-form require_auth, field validation, and item-type resolution. It now rejects a missing request_type_id with 400 and verifies the referenced request type belongs to the form's channel in a single query. An unparseable config JSON is treated as an empty config rather than failing the whole submission.
On the rendering side, PublicFormPage / FormRenderer now read brand, theme, and logo from the flattened public-channel fields the backend actually serves (channel.theme, channel.brand_color, channel.logo_url), and the submit button label comes from formConfig.submit_button_text so channel customisation actually flows through. When the form is loaded inside an iframe (?embed=...), the page measures its document height on every layout and posts ws-form-resize to the parent so the widget-host iframe can match its height to the content.
The security-headers middleware now sets CSP frame-ancestors: * and omits X-Frame-Options for the /forms/* path prefix so customers can embed those pages on their own websites. All other routes keep SAMEORIGIN framing.
SCIM write guards on non-SCIM users
The SCIM handlers took an internal user id and happily operated on any row, including local or admin accounts the IdP never provisioned. That meant a leaked or misconfigured SCIM credential could deactivate any user by id. DeleteUser, PatchUser, and ReplaceUser now check scim_managed up front — non-managed users get 404 with an audit entry recording the refusal reason. POST's adoption-by-email path is untouched, so the legitimate route to bring a local user under SCIM management still works.
Enhancements
Time tracking
- Time Entry and Time Reports summary rows (Total Time: Xh) now sit on
--ds-surfaceto match the table header, instead of the semi-transparent neutral background that read as a detached block and looked off in dark mode. - The daily-hours chart pulls its line colour from
--ds-accent-blueinstead of a hard-coded#3b82f6, so the chart tracks the theme.
Collections board
- On the board view, the
(N remaining)count next to the Load more button is now hidden while a sprint filter is applied — the count reflects the unfiltered collection total, so showing it while filtering was misleading.
Workspace context chrome
- When a workspace gradient is active, the navigation reads interactive-active and inactive text colours against the gradient rather than falling back through
--ds-text(which becomes near-white-on-white in some gradient presets and near-invisible). The glass nav now uses explicit white + translucent-white values so legibility is consistent regardless of gradient.
SCIM offboarding notification
- The admin notification for a SCIM-initiated deactivation always said "was deactivated via SCIM" regardless of whe...
Windshift v0.5.7
Windshift v0.5.7
Suitable for small-scale production use.
Windshift is maturing and can now be used for small-scale production workloads. Be aware that APIs, data formats, and configuration may still change between releases without guaranteed migration paths. We recommend keeping backups and testing upgrades in a staging environment before applying them.
If you encounter issues or have ideas, please open an issue. Your feedback at this stage is incredibly valuable.
Features
Action execution actor
Workspace actions no longer run with whichever permissions the triggering user happens to hold. Two new concepts address the gap:
actor_user_idonactions— nullable override. When null the action continues to run under the triggering user, preserving prior behaviour. When set, every node executes under the named user's permissions and side-effects (comment authorship, item history, cascade events) are attributed to them.action.set_actorglobal permission — required to set or changeactor_user_id. Seeded with no default role assignment; onlysystem.adminor an explicit grant can configure an override. The permission is global-scope because an actor override grants cross-workspace reach and cannot be bounded by workspace-scopedaction.managealone.
The execution engine now centralises this as EffectiveActorID on ExecutionContext and threads it through every node executor (set_field column and custom, set_status, add_comment, notify_user, round_robin_assign, create_asset, update_asset), plus the downstream WorkflowService.PerformTransition, CommentService.Create, NotificationService.NotifyUsers, and cascade ActionEvent / AssetActionEvent emissions.
Per-node permission enforcement
Previously, node authorisation was inconsistent: create_asset / update_asset checked asset-set RBAC, but set_field, set_status, add_comment and round_robin_assign wrote through without a permission check. The effective actor is now checked against the workspace before each mutating node runs — item.edit for set_field, set_status, and round_robin_assign; item.comment for add_comment. Asset mutations still go through the existing asset-set check, unchanged. notify_user remains unchecked because it mutates no workspace state.
Authorisation failures fail the node, mark the action failed, and record the missing-permission error in the execution trace. A missing permission-service wiring refuses closed rather than silently skipping the check.
Action execution audit trail
action_execution_logs gains trigger_user_id and effective_actor_user_id so the per-run record distinguishes who caused the event from whose rights governed the run. Every set-or-change of an action's actor also writes a dedicated automation.set_actor entry to the generic audit log with the previous and new actor IDs and the administrator who made the change.
Enhancements
Action flow editor
- A run-as picker sits above the node palette. Users with
action.set_actorcan choose any user (or clear back to "run as triggering user"); users without the permission see a read-only label showing the currently configured actor or a hint explaining the default. - New nodes added from the palette now land at the centre of the visible canvas rather than a fixed coordinate region that frequently sat outside the viewport. Placement is computed from the live viewport (tracked via
onmove, keeping SvelteFlow in uncontrolled mode sodefaultViewportstill governs first render) and offset by half a node footprint. A small random jitter keeps successive clicks from stacking pixel-perfectly. - The minimap now colour-codes nodes by type, mirroring the accent colours used on the canvas (trigger amber,
set_fieldpurple,set_statusteal,add_commentorange,notify_usermagenta,conditionyellow,update_assetteal,create_assetgreen). The minimap shell itself picks up a surface-raised background, border, shadow and rounded corners so it reads as editor chrome rather than a bare overlay.
Security page
- API token creation exposes the full scope set as a grid with resource rows and read / write / delete columns. Preset buttons (Read-only, Read + Write, Clear) cover common picks; an Admin grid renders only for system administrators. The prior hardcoded three-scope default is pre-selected; Create is disabled until at least one scope is chosen.
Minor UX
- Service-user checkbox on the user-create modal no longer wraps its label when the descriptive hint is long; the hint is free to wrap below.
- Spacing fix on the user profile page.
Upgrade notes
actionsgains anactor_user_idcolumn andaction_execution_logsgainstrigger_user_idandeffective_actor_user_id. Existing rows migrate with these fields null; behaviour for actions without an override is unchanged.- The new global permission
action.set_actoris seeded on upgrade but not granted to any role. Assign it explicitly to administrators who need to configure actor overrides. - Workspace actions that previously succeeded by relying on the triggering user's lack-of-enforcement on
set_field/set_status/round_robin_assign/add_commentwill now fail if the effective actor lacks the corresponding workspace permission. Review audit logs forautomation.executefailures after upgrade; the most common fix is to grant the triggering useritem.edit/item.commenton the workspace, or to set an explicitactor_user_idon the action.