Writ coordinator v1.0.3
Personas can now sign themselves in, so an authenticated crawl recovers from an expired session unattended.
This release includes a schema migration (0017_persona_login_workflow). It is applied automatically on start by the container entrypoint, so the upgrade is unchanged:
git pull && docker compose up -d --buildIf you run the coordinator with a custom command: that bypasses entrypoint.sh, apply it yourself with alembic upgrade head.
Added
A persona can be linked to a login workflow, and sign itself in. Until now a persona's warm session could only ever be captured from something that had already logged in — a persona created with credentials alone had no way to establish one, so authenticated crawls using it were rejected indefinitely. Record the sign-in once, attach it to the persona, and the coordinator re-runs it on demand and whenever the session expires. Credentials and 2FA are folded in from the persona, and the captured session is written back to it when the run completes.
Concurrent shards are serialised: one caller takes a lock and performs the sign-in while the rest wait for that result and reuse it. Without this, a crawl starting N shards against an expired session would drive N simultaneous logins at the same account — which is exactly the pattern that trips a site's abuse defences and gets the account locked.
A crawl workflow is rejected as a login workflow. It is orchestrator machinery rather than a recorded sign-in, and pointing a persona at one would make a crawl that needs a login re-enter the very path that asked for it.
Fixed
The language picker always showed "English", whatever language the app was actually displaying. i18next only sets resolvedLanguage for a language it has a dictionary for, and English ships none — it is the source strings. So resolvedLanguage || 'en' read as English even while the UI rendered in French, and the <select> sat on the wrong option until you re-picked the language you were already using. A new activeLanguage() returns the true active base code and is now the single source for both pickers and for date formatting.
A freshly loaded dictionary did not repaint mounted components. The re-emit after a lazy dictionary load compared the exact language tag, but the browser detector supplies regional tags — fr-FR on a French machine — so the match failed and already-mounted components kept their English strings until some unrelated render refreshed them. The comparison is now on the normalised base code.
Linking a login workflow to a persona made the schema unsortable. automation_workflows.default_persona_id already referenced personas, so the new personas.login_workflow_id closed a foreign-key cycle between the two tables and SQLAlchemy could no longer order them — breaking metadata.create_all / drop_all, which SQLAlchemy warns "may raise an error in a future release". The constraint is now emitted with use_alter so it takes no part in the sort. Existing installs were never affected: the migration adds a column to a table that already exists and never sorts the schema as a whole.
Security
React Router upgraded to 7.18.2, clearing three advisories against the 6.x line:
- CVE-2026-53669 — open redirect via backslash in
<Link>anduseNavigate - CVE-2026-53668 — open redirect leading to XSS
- CVE-2026-53666 — arbitrary constructor injection via
deserializeErrors()during SSR hydration
react-router-dom 6.x had no patch available for the second of these, so the major upgrade was the only route to a clean tree.
Verification
Tagged only after scripts/release-e2e.sh passed against this commit: docker compose up → create the owner → install the published writ-agent release through the same one-liner the UI prints, checksum verified → record a workflow on a real browser → replay it → call it over REST and over MCP.
Dependency audits are clean on both sides — npm audit 0, pip-audit 0 — and bandit reports nothing at medium severity and confidence.
You can run the same gate yourself with ./scripts/release-e2e.sh.
The tag is signed — git verify-tag v1.0.3.
Full changelog: CHANGELOG.md · v1.0.2…v1.0.3