* feat(users): limit resource visibility by user group
Admins can create groups and assign members. Skill packages, knowledge
bases, connectors, models, and named storage backends can then be
restricted to those groups without granting extra edit rights.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(users): gate connector and storage use by group and show the catalog
Replace per-instance connector and storage grants with one feature-level control, list every resource kind on the group page, and make skill-package sharing follow a single audience choice.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Scheduled jobs can run once and then stop, personalization tabs can be granted separately, and rotated logs use a date-before-extension name.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(skills): export selected skills and skill packages as ZIP
Workspace skills can be checked and downloaded together, and a skill package
exports in the same top-level folder layout the ZIP importer already accepts. (#260)
* feat(skills): let skill packages disable ZIP download
Creators and admins can turn off package export. Other users can still view
and mount the package, but cannot download it.
* feat(skills): default package download from the copy policy
* test(db): expect schema v21 after package download migration
---------
Co-authored-by: 薄生 <67680641+Bosheng0422@users.noreply.github.com>
* fix(weixin): keep a durable send token and repair old cron columns
Rename legacy cron_jobs.trigger to schedule_spec on startup. Store the WeChat context token on the session so cron can still deliver after a restart, and drop tool preambles on team WeChat streams.
* refactor(weixin): keep the send-token rules in one place
The session table no longer imports the gateway package. Token merge and team-stream preamble filtering behave the same.
* chore: require octop-gateway 1.0.2
The WeChat reply fixes and the QQ single-bubble replies ship in this gateway release.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Desktop startup was saving a hardcoded bind address and could launch a second Octop when the preferred port was busy. Chat uploads dropped folders, shared peer experts are no longer offered for delete, and the state backend stays available only for experts that already use it.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
skill_packages gains copy_policy (snapshot default / lock / deny), set by the
creator or admin via PATCH. deny rejects the workspace copy endpoint with
SKILL_PACKAGE_COPY_DENIED and disables the dashboard button (can_copy). lock
stamps each copied SKILL.md with origin/locked frontmatter; the existing
package-only write guard now also rejects writes to stamped copies for
everyone but the origin package creator (admins keep an escape hatch for
orphaned copies). Copy attempts are audited. Agent-tool-level write
protection for locked copies needs harness support, tracked separately.
Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com>
* feat: add Octop↔Octop Bridge for remote agent access
Enable admin-managed peer connections with WS tunnel, path allowlist,
hello_ack handshake, SSRF guards, and Chat shadow-agent hydrate so
operators can probe and use remote agents from the local dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: serialize bridge turn chunks and flag remote experts
LangChain HumanMessage objects in peer turn.chunk frames crashed
json.dumps; add a bridge JSON default. Show a 远端/Remote badge on
shadow experts in the picker and sidebar lists.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: bridge avatars, named remote badges, and auto-reconnect
Keep bundled / CDN icon URLs for shadow experts and only proxy uploaded
avatars. Show the bridge display name on remote badges. Add a default-on
auto-reconnect switch that backs off on disconnect and disables itself
after repeated failures, recording the reason in last_error.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add card/table views for bridge connections
Match the storage page card layout and add a Segmented toggle so
admins can switch between a card grid and a table of remote links.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: render bridge expert icons correctly on the chat page
Encode bridge agent ids in avatar URLs the same way as chat WS paths,
fall back to Lucide when the image fails, size portraits to fill the
sidebar avatar, and refresh AgentContext when icon_url changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: tunnel peer models and knowledge bases for bridge chat
Allow read-only GET of peer providers/resolved, active-model, and
knowledge-bases (plus capability) through the Bridge tunnel, expose
local bridge connection routes, and point the chat composer at those
sources when talking to a shadow expert. Hide local connectors for
remote sessions. Also allow agent subagents paths on the tunnel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: scope bridge chat expert picker to peer agents
When the active expert is a Bridge shadow, the composer expert picker
and @-mentions only list peers on that connection (not local experts).
Encode bridge agent ids for skills and subagents list/install paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: hint remote chat pickers to manage settings on the peer
In Bridge sessions, replace local “manage …” footers for models,
skills, knowledge bases, experts, and subagents with a muted
“edit on remote” tip and a toast instead of navigating to local admin.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: polish bridge remote UX and tunnel agent status
Allowlist GET …/status so hubs can read real peer harness state (peers need
this build). Keep history-migration hub-local, skip noisy bridge polls in the
UI, move Bridge under Settings, and mark remote experts with cable + name.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: tunnel remote expert catalogs and polish bridge connection UX
Hub can edit peer subagents, memory, channels, tools, plugins, and MBTI
through the tunnel; surfaces that cannot hop show an in-drawer hint. Peer
must run this build so the new allowlist takes effect.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: group cloud-collab experts and unify header-tunneled composer APIs
Keep the selected remote expert on disconnect, gate peer-only surfaces in the UI, and stop forking composer/task requests through dedicated bridge endpoints.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add search to the Experts toolbar
Filter my experts, teams, and the library from the existing button row so long catalogs stay scannable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: keep inbound cloud-collab cards peer-owned
Inbound links can only edit name/icon/notes; disconnect, delete, and auto-reconnect stay disabled. Peer offline shows as 离线, peer delete removes the card, and the default inbound name is the short connection id.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: polish inbound cloud-collab UX and unify 对端 copy
Peer-initiated links can only edit display info and cannot redial;
offline cards may be deleted. Local-only pages hint when a peer
expert is selected.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: map peer team speakers onto Bridge shadows
Peer team chats stamped local member ids, so the hub fell back to the host
avatar. Rewrite roster and stream speakers onto bridge:{cid}:{id} shadows,
and keep the local team picker from mixing in peer experts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: stop rewriting team thread ids in the Bridge tunnel
Member job threads are {room}~{agent}. A blanket JSON replace of the peer
agent id turned those into hub shadows, so history 404ed on the peer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: rewrite peer media URLs and keep bridge turn composer fields
Live send_file_to_user frames still pointed at peer-local /api/agents/{id}.
Map every teammate id in tunneled JSON, and copy the full dashboard turn
body so knowledge bases and HITL policy are not dropped on the peer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: rename bridge probe tests to avoid pytest module clash
tests/unit/bridge/test_probe.py collided with tests/unit/mobile/test_probe.py
during collection (same basename).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: collapse extra experts on Bridge cards and fill avatars
Show at most four experts on a connection card, with the rest behind
expand. Portraits fill the 40px ring without a tinted background.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: lulinzhi37-alt <lulinzhi37@gmail.com>
Collapse the dual user_role_id / role model so users.role and invites.role
hold the template public id, block deleting roles still in use, and keep
the chat composer visible under mobile browser visualViewport.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Raw 018 SQL alters user_invites even when the table is missing on pre-invite backups. Route v18 through _ensure_user_role_schema and bump watermark assertions to 18.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(users): add role templates, invite snapshots, and profile avatars
Roles fill defaults for create, edit, and invite without rewriting existing users. Account type stays independent of the selected role.
Co-authored-by: Cursor <cursoragent@cursor.com>
* style: format role and invite repositories for ruff
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: narrow invite role name before lookup
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(agents): group thin modules into domain subpackages
Move plugin helpers, memory, settings stores, persona/MBTI, and avatar
bootstrap code under plugins/, memory/, settings/, persona/, and experts/
while keeping top-level import shims so existing call sites stay stable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(agents): drop compatibility shims after subpackage move
Call sites already import the new package paths; remove the top-level
re-export stubs so the agents tree only shows real modules.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(agents): move thread helpers into threads/
Group artifact shaping, fork, and context breakdown under
agents/threads so top-level only keeps lifecycle/config adapters.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(agents): fold profile/runtime/tool catalog into settings/
Move the remaining config-surface helpers under settings/ and drop the
skill_package id-list aliases in favor of dump/parse_id_list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(agents): group workspace_dir and execute_env under workspace/
Keep path resolution and harness execute-env injection together as one
workspace package; leave only manager and conversation_mode at top level.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(infra): fix history/SkillHub ownership and drop thin providers/
Move history_projection under history/, share SkillHub host/limits and
route expert skill downloads through skills.skillhub_market, and fold
Codex OAuth helpers into agents.providers to remove the tiny providers/
package.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: sync AGENTS.md and guides with infra/agents layout
Reflect subpackages (settings/workspace/threads/persona, history/, skills/)
and drop stale runtime.py / mbti_profiles paths after the agents reorg.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep tool-approval bypass on the conversation (allow this tool / allow all)
without changing global security settings. Also let expert create carry
welcome quick prompts, and replace the login forgot-password tip with a
platform-specific CLI dialog.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(chat): add Ask / Plan / Craft conversation modes
Keep mode sticky per thread, enforce Ask/Plan in harness-agent, and let Plan write a workspace plan before Craft executes it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Update pyproject.toml
Allow experts and skill packages to carry composer defaults so that
installed/created agents start with the author's chosen knowledge bases
and MCP servers. Defaults are persisted as agent profile columns and
validated against the installing user. Adds the per-user policy resource
table (migration 014) and wires the dashboard composer fields plus
locale strings.
- Add knowledge_base_ids and mcp_servers to the agent profile schema
- Validate KB/MCP ids on expert/skill-package install and create paths
- Persist defaults via migrate.py helper and 014_user_policy migration
- Surface composer default fields in the dashboard expert/skill UI
Add a `user_policies` table (migration v14) backing per-user `workspace_root_dir`
(restricts local backend roots) and `token_quota` (lifetime token limit). Token
quota is enforced in the agent runtime via TokenQuotaMiddleware. Extract slash
command parsing into infra/gateway/slash/parser.py and expose policies on the
users API. Frontend adds admin policy UI, storage/chat/experts restyle, and a
shared TabLabel / PageLoading component.
* feat(usage): date-range filter and Excel export for token stats
Add RangePicker presets/custom ranges, multi-sheet Excel export with
localized headers and charts, and filter coverage for #154.
* fix(usage): export detail sheet in ascending time order
Keep the newest N rows when truncating, then reverse so Excel opens
oldest-first for chronological reading.
* fix(usage): sort export detail ascending in SQL
Order detail rows with a newest-N subquery then ts ASC so Excel opens
oldest-first; restart-sensitive Python reverse was easy to miss.
* fix(usage): keep Excel TOTAL row out of Sort range
Leave a blank spacer above the SUM TOTAL so Excel contiguous Sort
does not move 合计 with the filtered detail rows.
Migrate immediately after overlay and repair folded v13 connectors schema so existing backups stay usable. Refuse newer schema versions with BACKUP_SCHEMA_INCOMPATIBLE. Also drop the knowledge-bases Beta nav badge.
Co-authored-by: Cursor <cursoragent@cursor.com>
Synthesize tool-call-only parents, stamp turn/duration metadata, and fall
back to content-size estimates so Turns/Calls/Duration toggles work on
legacy and burst-stamped events. Gather harness turn context and rename
conflicting trajectory unit test modules.
Move cron delivery out of Gateway so dashboard text jobs write canonical history, AI jobs notify consistently, and inbound turns stay serialized with the published harness APIs.
* feat(cron): add name field to cron jobs and update related interfaces
- Introduced a `name` field to the `OctopCronRow`, `OctopCronCreateBody`, and `OctopCronPatchBody` interfaces.
- Updated localization files to include validation messages for the new `name` field.
- Added a constant for maximum name length.
- Enhanced the UI to display the job name in the cron jobs table and detail views.
- Updated API documentation to reflect the new optional `name` parameter in cron job creation and updates.
- Implemented database schema changes to support the new `name` field in cron jobs.
This change improves the user experience by allowing users to assign descriptive names to their cron jobs, making them easier to identify.
* feat(cron): restrict cron job management to owners only
- Updated the cron job management logic to ensure that only the owner of a cron job can create, view, update, or delete it.
- Modified the API endpoints to return empty lists for non-owners attempting to access cron jobs.
- Enhanced the frontend logic to reflect these changes, ensuring a consistent user experience across the application.
- Updated related tests to verify that cron jobs are correctly scoped to their owners, preventing unauthorized access.
This change improves security and clarity in the management of cron jobs within shared agents.
* refactor(cron): improve code readability in cron management functions
- Reformatted the `_assert_cron_manage` function for better readability by adjusting the parameter layout.
- Simplified the invocation of the `create` function in the test case for clarity.
These changes enhance the maintainability of the code and improve the overall structure of the cron management logic.
* fix(db): resolve duplicate schema v10 migrations skipping projection tables
Two 010_*.sql files raced: kb_max_documents bumped the watermark first and
skipped thread_message_projection. Keep a single 010 and repair missing tables.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(agents): disable memory in harness create/close unit tests on Windows
Avoid Windows access-violation when memory GC races HarnessAgentManager.close
under pytest-xdist; unrelated to the schema v10 fix but was failing PR CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* 长session卡死优化
* chore: cut 0.9.29b0 and pin harness-agent 0.9.27
Long-session testers pull this branch from source; bump above 0.9.28 and
lock vacuum-safe harness-agent / harness-memory so Codex installs pick them up.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: retag beta as 0.9.27b0 so PyPI 0.9.28 still looks newer
Lets non-editable installs click Update to leave the long-session trial.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: cut 0.9.29 for merge (drop beta tag)
Current main is 0.9.28; this branch ships as the next stable version.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: workspace backend sandboxing, execute-env injection, and ephemeral image refs
Workspace file-I/O hardening across backend and dashboard:
- Host directory sandboxing via root_dir allow-list and denied prefixes
- Execute-environment defaults injected into harness backend specs
- Ephemeral workspace image rematerialization for model calls
- Attachment hint expansion and inbound store improvements
- Portable memory backend and skill workspace catalog updates
- Dashboard UI for agent backend fields and root-dir selection
- Bump harness-agent 0.9.23 / harness-memory 0.9.7; docs + install notes
* feat: add user invitation codes with admin management and redeem flow
- Add user_invites table (migration 009) plus SQLite/Postgres reconcile paths
- Add InviteRepo and InviteService for create/revoke/list/redeem
- Expose public validate/redeem endpoints and admin create/revoke/list routers
- Extract default-agent bootstrap into octop.infra.agents.default_agent
- Add invite error codes and en/zh i18n strings
- Dashboard: invite drawer, login redeem UI, users list, and locales
* fix: add Windows cross-platform guards for pre-existing sandbox/rootfs tests
- Guard POSIX/container-only tests (rootfs-absolute /.octop/... workspace
paths) with posix_only in test_thread_artifacts.py and test_agent_manager.py
- Normalize the rendered workspace path assertion in test_octop_builtin_skills.py
(forward slashes on Windows, not str(tmp_path) backslashes)
- Canonicalize ws_dir before the prefix check in attachment hint/path tests
(tempfile may yield an 8.3 short path on Windows while the resolved
attachment path uses the long form)
These failures predate the user-invites commit and are unrelated to it; the
product path logic is correct.
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
* fix: inject Admin env into running agents and sum turn token usage
Running shells and Docker sandboxes now pick up ~/.octop/env (and workspace .env) without a full reload; MCP stdio no longer inherits the entire host environment. Usage accounting adds every model call in the turn, and workspace I/O survives the harness rebuild window.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: schema v7 identity, expert profiles, chat artifacts, and KB citations (#346)
* feat: schema v7 identity, expert profiles, chat artifacts, and KB citations
Fold integer PKs plus public string ids, document folders, agent profile columns, and thread artifacts into v7 SQL (not only Python helpers). Experts get workspace avatars and a single welcome_message; chat keeps file changes across sessions, shows knowledge citations, and can drag workspace files.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: drop cron JobDrawer UX from the schema v7 PR
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: prefer tool-arg paths when recording chat artifacts
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add per-user module permissions (RBAC) with admin bypass
Introduce a module-level permission system so non-admin users can be
scoped to a subset of dashboard modules. Adds a `permissions` TEXT column
on the `users` table (migration 006, SQLite + PostgreSQL), a central
permission catalog in `infra/users/permissions.py`, backend ACL checks
across routers, and dashboard gating via `RequirePermission` /
`ForbiddenPage` keyed by nav module. `admin` always bypasses every gate;
read access and chat/agent use are never gated.
* fix: gate connector install-cli by connectors permission (not admin-only)
The host CLI installer is a connectors-module operation and should be
gated by the `connectors` module permission, consistent with the rest of
the connector routes, instead of being admin-only.
The earlier RBAC change demoted this route from `current_admin` but the
test `test_install_cli_forbidden_for_non_admin` still failed because the
`create_user` test helper grants `BASELINE_PERMISSIONS` (which includes
`connectors`) by default, so the "non-admin" user actually held the
permission. Gate the route with `require_permission("connectors")` and
create the test user with `permissions=[]` so it genuinely lacks the
key and is rejected with 403.
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Consolidate unreleased develop migrations 005–009 into one v5 file with
ensure/clamp paths for pre-squash local DBs; add HITL catalog picker,
shared runtime package install for ONNX/desktop, and short-id list UX for
knowledge bases and skill packages.
Co-authored-by: Cursor <cursoragent@cursor.com>
Register routers/repos/migrations, OpenAPI tags, i18n/error codes, sidebar routes, and docs for the new platform capabilities.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce document indexing, embedding (ONNX/remote), and chat/IM injection with default-open selection in the dashboard composer.
Co-authored-by: Cursor <cursoragent@cursor.com>
Let owners publish an agent workspace as a reusable template, refresh/unpublish it, and let others install a private fork. Also adds expert color/emoji pickers used by subagents and publish UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add is_shared so owners can expose a runtime agent for others to chat with while keeping config writes owner-only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep dashboard chat turns attachable after reconnect, reuse one terminal
session store across Workbench and the chat dock, and migrate legacy
hard-cut thread titles to clipped titles with ellipsis.
* feat: add global skill packages with local ZIP import and personalization layout
Introduce instance-level skill packages (DB + disk), mount them on agents, and
support SkillHub/URL/ZIP import. Merge related schema into migration v2 and
cover the zip import path with frontend and API tests.
* fix: hot-sync skill package mounts and dark-mode package cards
Avoid full agent rebuild when mounting packages so Experts UI no longer
hits AGENT_NOT_FOUND; accept workspace-scoped local backends on Windows;
use theme token for package list card background.
* feat: PostgreSQL dual-backend for control plane
Add SqlitePool|PostgresPool behind DatabasePool, parallel PG migrations,
setup wizard defer/bind, pg_dump backups with cross-engine refusal, and
memory defaulting to the control-plane DSN when using PostgreSQL.
* fix(chat): delete checkpoint data when a conversation thread is deleted
Deleting a thread only removed Octop's own metadata row; the actual
message history stayed in the agent's LangGraph checkpointer forever,
making "delete conversation" cosmetic. Also confirm before deleting a
session in the UI, since the action is now genuinely destructive.
Checkpoint deletion runs before the metadata row is removed: if it
fails outright (not just "nothing to delete"), the thread stays
visible/retryable instead of orphaning data with no remaining handle.
* fix(memory): stop suggesting pg_dump for portable export of postgres memory
The 501 hint told users to "use pg_dump on the memory schema instead".
That advice predates the shared-table layout: memory for ALL agents now
lives in one harness_memory schema isolated by a namespace column, so a
schema-level pg_dump would export every agent's memory, not just the
requester's — a cross-agent data exposure if followed on a multi-user
install.
Document the two migration models where the refusal is implemented:
sqlite memory is per-agent files moved via pack/adopt; postgres memory
is shared, and the supported way to use it from another host (e.g.
OpenClaw's harness-memory bridge with --backend postgres) is pointing
that host at the same DSN and namespace — shared, not migrated.
* fix(test): update test_slash_compact to renamed SqlitePool
The /compact test module still imported the pre-rename DBPool, which
broke collection (ImportError) on this branch after develop's compact
feature was merged in — DBPool was renamed to SqlitePool (the concrete
class) with DatabasePool now the Protocol.
DBPool -> SqlitePool at all three sites: the import, the _agent_manager
type hint, and the fixture's DBPool(path) instantiation. SqlitePool is
the byte-for-byte successor of DBPool and matches how every other test
constructs a pool; DatabasePool cannot be used at the instantiation
site (a Protocol is not instantiable).
pytest -m live now collects cleanly (31 selected, 0 errors);
test_slash_compact 3 tests pass.
* fix(test): make memory_backend db_path assertion separator-agnostic
test_explicit_sqlite_overrides_postgresql_control_plane compared the
resolved db_path against the literal "/tmp/ws/memory.sqlite", which the
code never produces on Windows: it builds the path with pathlib, so the
Windows CI job got "\tmp\ws\memory.sqlite" and the string equality failed.
Compare as Path objects so the assertion holds on every OS separator,
matching the .as_posix() discipline already used in test_config.py.