refactor(channel,settings): connection entity + preference scopes (07-16 §5.2/§5.3) - #698
Draft
kaizhou-lab wants to merge 187 commits into
Draft
refactor(channel,settings): connection entity + preference scopes (07-16 §5.2/§5.3)#698kaizhou-lab wants to merge 187 commits into
kaizhou-lab wants to merge 187 commits into
Conversation
added 30 commits
July 22, 2026 14:12
added 27 commits
July 24, 2026 16:44
…) into feat/multi-account-user-scope
…for AionPro adoption adopt_system_default_data discovered ownership tables only by a user_id column, silently skipping tables whose owner column is owner_user_id (project_explorer, channel assistant_users/pairing_codes/plugins) — on an AionUi->AionPro upgrade those rows stayed on system_default_user while their paired roots moved. - Adoption now sweeps both ownership column names. - New adoption-coverage gate (aionui-db tests): every table in the live schema must be classified as ownership root / declared parent-scoped (parent column verified) / declared machine-global (with rationale) / infrastructure. An unclassified new table — including ones merged from main — fails the suite until a conscious adoption decision is made. - Upgrade e2e: first external user adopts the projects/explorer pair together; a second external account closes the window in either order.
…r delta table The mixed-scope agent_metadata catalog let one agent_id have a global template row plus per-user copy-on-write copies. Machine-level startup probes (unified in main #678) took the no-acting-user write path, which minted system_default_user copies on every AionPro boot — the catalog's single source of truth forked, and identity fields could drift out of sync across copies. - agent_metadata is now UNIQUE(agent_id): exactly one row per agent (builtin = user_id NULL; custom = creator's id as an ownership attribute). No copies. - New agent_user_state(user_id, agent_id) holds every per-user delta: enabled toggle, command/env overrides, session-scoped availability, handshake caches. Reads LEFT JOIN and overlay user-first; the availability snapshot is taken whole from the winning side so its fields never mix. - Startup probes UPDATE the catalog row in place (never insert); session outcomes / manual checks upsert the user's delta row. - AionPro startup no longer runs the default-user generated-assistant reconcile or ingests the legacy shared skill directory (unattributable files); each real user materializes generated assistants lazily. Channel default-assistant resolution triggers that lazy materialization via a composition-layer port so the bare fallback still resolves. - Gate e2e: after a full AionPro boot, no ownership column outside users holds a system_default_user row. Handshake/binding tests updated to the single-row model.
Replace the SHA256-hash .users/{hash} directory scheme (and the flat
root special-case for the default user) with a type-first layout:
skills/users/{user_dir}/ for every user, including the default user.
user_dir is derived via aionui_common::user_dir_name, keeping the
scheme consistent across domains.
is_user_scoped_skill_storage_path now matches the "users" path
component instead of ".users".
…/{dir}
Thread user_id through the avatar read/write/display path so avatar files
land in and are read from assistant-avatars/users/{user_dir}/. Generated
assistants share bare assistant_ids across users, so a flat avatars dir
collided; per-user dirs fix that. Reconcile and cleanup stay default-user
scoped; response builders pass the acting user.
New auto-provisioned conversation/team workspaces now land under
conversations/users/{user_dir}/{Y}/{M}/{D}/{leaf}. is_auto_workspace and the
delete/prune path accept BOTH the new per-user layout and the legacy userless
one, so pre-existing conversations keep working and are not misclassified as
custom (no historical migration needed — workspace paths are keyed by unique
conversation/team id). Threads user_id through the ConversationPort
create_team_temp_workspace trait.
…Pro adoption
When DB adoption re-owns the local default user's rows to the first external
(AionPro) user, physically move the corresponding per-user files
(skills/assistant-rules/skills/avatars) from users/system_default_user/ to
users/{adopter_dir}/ and rewrite skills.path, so the upgraded account sees its
files. Implemented as aionui_extension::fs_adopt (same-volume rename, EXDEV
copy fallback, idempotent) behind a SystemDefaultFilesystemAdopter port wired
in the composition layer. Best-effort; never fails provisioning.
…ackend
Boots real AppServices in AionPro mode and asserts against the production
filesystem: two users importing the same-named skill get distinct on-disk
roots (skills/users/{dir}) and materialization never crosses users; and
provisioning the first AionPro user physically moves system_default_user's
files to the adopter's root and rewrites skills.path.
…ser layout
The HTTP-driven e2e tests asserted the old flat/encode paths (assistant-rules/{id},
assistant-avatars/{file}, skills path .users/{hash}). Point them at the new
type-first users/{dir} layout; meaning preserved (still assert distinct per-user
on-disk storage and correct file contents).
…tests Left dangling after the single-row refactor removed the custom_params test helper; tripped -W unused-imports / CI.
…t state table An agent is a machine-level resource: one CLI binary + login per device, shared by every Core User. Its enable/disable state gates whether the registry starts the agent (machine-level singleton reads it from the catalog), so it belongs on the single agent_metadata catalog row, not a per-user overlay. The frontend has no agent-enable entry point; per-user enable/disable happens one layer up on assistants (assistant_overrides). - 029 migration: remove the agent_overrides (per-user delta) table; the catalog row holds ALL agent state (capabilities, availability probe, command/env overrides, enabled). - sqlite_agent_metadata: drop the per-user LEFT JOIN from the read projection (AGENT_METADATA_FROM); enabled comes straight from the catalog. set_enabled_for_user now UPDATEs the catalog after an owner-visibility check; delete no longer cleans a per-user table. - registry: handshake writes always target the catalog (no system_default_user special-case). - custom service: reload_one after enable/delete/upsert is now unconditional (machine-level cache refresh), fixing stale machine cache when a non-default user toggled a builtin agent. Full workspace: 7682 passed.
Review doc: docs/superpowers/2026-07-27-multi-account-user-scope-review.md
Must-fix 1 — adoption file-move can now re-run after a partial failure:
- IUserRepository::is_sole_external_user exposes the adoption-window
predicate (sole external user == the caller).
- AuthProvisionService gates fs_adopter on the window predicate instead of
"rows adopted this call", so leftover files under
users/system_default_user/ are picked up on a later login. The window
closes with a second external user, so files can never leak.
Must-fix 2 — is_auto_workspace matches by path structure:
- session_context::is_auto_workspace now wildcards the users/{dir} segment
and the {Y}/{M}/{D} date (leaf still carries the globally-unique
conversation id), mirroring the delete-side matcher. Fixes two
misclassifications: workspaces created on an earlier day, and adopted
workspaces still under users/system_default_user/.
Channel sessions (review §3.1):
- get_or_create_session INSERT now requires a bound conversation to belong
to the same Core owner (NULL stays allowed); new cross-account test.
- adoption_coverage gate grows a NON_CORE_USER_ID_TABLES category so
assistant_sessions.user_id (a channel-user FK, not a Core owner) is a
conscious classification instead of a false positive.
Dead-code removal (review §4.1, verified no callers incl. AionUi/aionpro):
- Drop ICronRepository::{update_system, delete_system,
delete_by_conversation_system}; tests moved to the _for_user paths.
Conversation-delete cascade is handled by the DB FK, not the removed
method.
Fallback observability (review §4.3-4.5, kept per decision):
- The no-repo skill discovery fallback and the six no-dispatcher
assistant-rule/skill route fallbacks now log a warn when taken; they are
test/degraded-mode paths that production wiring never hits.
Review §6 (docs/superpowers/2026-07-27-multi-account-user-scope-review.md):
- Real cleanup-hook e2e (new session_revoke_cleanup_e2e.rs): boots the
production router (real session_revoked_hook, not an injected double),
revokes an AionPro session over HTTP, and asserts observable cleanup —
the revoked token stops working and the user's channel sessions are
cleared from the DB by the hook's async task (polled). Legs that would
need live runtimes/websockets to observe are listed as not-covered in
the test header.
- CROSS_ACCOUNT_REFERENCE over HTTP (cron_e2e): user B creates a cron job
bound to user A's conversation and gets a real 409 with the exact error
code; no job row leaks to either user.
- Two-user filesystem isolation: avatars (assistants_e2e) land under each
owner's assistant-avatars/users/{dir}/ with their own bytes and per-user
serving, and cross-user assistant-id reuse is rejected; auto-provisioned
conversation workspaces (conversation_e2e) live under distinct
conversations/users/{dir}/ roots on disk.
Also updates gitignored design docs under docs/superpowers/ (not committed):
matrix rows 9/22 + §6.3 in the 2026-07-17 root-scope design (agent is
machine-level incl. enabled; cron_jobs is an ownership root), the
2026-07-24 fs-isolation design (delete keeps files; adoption file-move
reinstated with window-gated re-run; invalid-id rejection dropped), and a
disposition table appended to the review doc itself.
…account-user-scope
…ge truth Settings-dedup B1 (plans/2026-07-27-channel-settings-refactor-split-plan.md): - SettingsService now proxies language through client_preferences: reads prefer the 'language' preference (JSON or raw string tolerated, non-string ignored with a warn) and fall back to the legacy system_settings.language column; PATCH writes the preference as the stored truth and keeps the column convergent for pre-B1 readers. - /api/settings and /api/settings/client now agree on one stored value; covered by a cross-route integration test plus service-level tests for pref-wins, column fallback, raw-string tolerance, and non-string reject. - Add consumer inventory + column disposition doc for B2 (docs/superpowers/2026-07-27-settings-dedup-b1-inventory.md).
…to channel_connections Channel refactor A1 (plans/2026-07-27-channel-settings-refactor-split-plan.md, carrying 07-16 §5.2 forward): - Migration 030 rebuilds assistant_plugins as channel_connections: the id becomes a generated meaning-free connection id, the platform type moves to plugin_key, PRIMARY KEY (owner_user_id, id) stays the composite identity for later composite FKs, and a unique index keeps the phase-1 one-instance-per-(owner, plugin_key) rule. Backfill preserves the legacy identity as plugin_key with migration-fatal row-conservation checks. - Repository speaks connections: get/upsert/delete/update_status by connection id plus get_connection_by_plugin_key as the bridge for callers that still address channels by platform. - ChannelManager keys its runtime map by (owner_user_id, connection_id), resolves or generates the connection on enable, and stamps owner_user_id + connection_id onto every incoming message; public send/edit/is-running APIs keep platform addressing until sessions carry connection ids (A2/A3). - UnifiedIncomingMessage and PluginStatusResponse expose connection_id (plugin_id stays the platform key so the UI is unchanged). - Fix disable route guard that looked up extension connections by plugin_key through the id lookup, which would have turned disable into a silent no-op for persisted extension channels. - Tests: 030 migration backfill test, phase-1 unique-index and plugin-key lookup coverage, manager/e2e assertions updated to the generated-id model.
…soft revoke and hashed codes
Channel refactor A2 (plans/2026-07-27-channel-settings-refactor-split-plan.md):
- Migration 030 segment 2:
* assistant_users -> channel_users: rows attach to their connection via
composite FK (owner_user_id, connection_id) -> channel_connections,
platform_user_id becomes external_user_id (platform now derived from
the connection), revocation becomes a soft delete (status
active|revoked + revoked_at) so authorization history survives for
audit, and the semantics-free session_id column is dropped. Orphan
platforms get a synthesized disabled connection so no authorization
silently disappears; row-conservation checks abort the migration on
mismatch.
* assistant_pairing_codes -> channel_pairing_requests: surrogate id,
connection linkage, and code_hash (server-side HMAC-SHA256) — the
plaintext code never touches the database. Legacy 10-minute codes are
dropped by design. Partial unique indexes: one pending request per
(owner, connection, external user) and per (owner, code hash).
* assistant_sessions rebuilt so its user FK follows channel_users.
* conversations gains UNIQUE(user_id, id) — the shared foundation for
composite cross-account FKs (07-16 §5.4), consumed by segment 3.
- Repository: create_user reactivates revoked identities (active
duplicate stays a conflict), revoke_user soft-deletes and removes the
user's sessions explicitly, user reads join the connection for the
derived platform and filter active rows; pairing CRUD keyed by
surrogate id + pending-by-code-hash lookup.
- PairingService hashes codes with a key shared with channel credential
encryption; approve/reject accept a PairingSelector (stored id from the
cold-loaded list, or the transient plaintext code from the WS event);
approvals record approved_channel_user_id.
- API: pending pairing listings expose id instead of the (no longer
recoverable) code; approve/reject accept {id} or {code}; the
channel.pairing-requested event carries both the id and the transient
code. Frontends approving from a cold-loaded list must switch to id.
- Tests adapted to the new model at equal or stricter strength: plaintext
absence, soft-revoke audit rows, reactivation on re-pairing, FK-backed
connection seeding, migration backfill coverage.
…indings Channel refactor A3 (plans/2026-07-27-channel-settings-refactor-split-plan.md): - Migration 030 segment 3: assistant_sessions -> channel_conversation_bindings (owner_user_id + connection_id + channel_user_id derived from the channel user, chat_id -> external_chat_id, last_activity -> last_active_at) with a composite FK into channel_users (ON DELETE CASCADE) and one binding per (owner, connection, channel user, external chat). agent_type/workspace are dropped: agent configuration is owned by channel settings + the conversation snapshot, and workspace had no reader. - Cross-account conversation bindings are made unrepresentable by INSERT and UPDATE triggers raising CROSS_ACCOUNT_REFERENCE. Triggers instead of a composite FK to conversations(user_id, id): a composite ON DELETE SET NULL would null owner_user_id together with conversation_id, and NO ACTION would block conversation deletion; the single-column FK keeps ON DELETE SET NULL. The service-layer EXISTS guards remain as defense in depth. - Repository INSERT derives owner/connection from the ACTIVE channel user row, so forged owner/connection values in the input row are ignored and a revoked user cannot open new bindings (both covered by tests). - SessionManager loses the agent_type/workspace parameters; update_agent_type is removed (no production caller). Action responses now display the currently configured agent from channel settings instead of a value frozen at session creation. - ChannelSessionResponse keeps agent_type/workspace as deprecated optional fields (omitted from JSON) for API compatibility. - adoption_coverage: the assistant_sessions non-core-user_id exception is cleared — the binding table's channel_user_id no longer masquerades as an ownership column and owner_user_id classifies it as a normal owned root. - revoke_user's session cleanup gains an owner predicate (defense in depth). - Migration test covers backfill inheritance, column renames, dropped columns, and both cross-account triggers.
Settings-dedup B2 (plans/2026-07-27-channel-settings-refactor-split-plan.md): - Migration 031 rebuilds client_preferences with scope (device|account): device rows are machine-level (user_id NULL, one value per key via a partial unique index), account rows stay per-user. Keys confirmed device-level by the B1 inventory (system.closeToTray, keepAwake, autoPreviewOfficeFiles, pet.*) are promoted latest-write-wins and their per-user copies dropped. The four system_settings switches are materialized as normalized account keys (system.notificationEnabled, cron.notificationEnabled, system.commandQueueEnabled, system.saveUploadToWorkspace) with a migration-fatal read-back check; existing preference rows are never clobbered. - Repository gains device-scope operations; the existing per-user methods are account-scoped. - ClientPrefService routes keys by scope: device keys are readable and writable machine-wide (written by one user, visible to all), account keys stay isolated. keepAwake is device state now, so the startup restore no longer needs a user and runs in both identity modes. - SettingsService extends the B1 language pattern to all four switches: preferences are the read truth (column fallback), writes land in preferences with the legacy row kept convergent — /api/settings and /api/settings/client agree on one stored value for every field, making the B3 proxy/drop step mechanical. - adoption coverage: device rows (user_id NULL) are machine-global and excluded from adoption, account rows adopt normally — pinned by test.
Channel refactor A4 (plans/2026-07-27-channel-settings-refactor-split-plan.md):
- ChannelSettingsService resolves preference keys through the platform's
connection: writes land on assistant.{connection_id}.* whenever the
platform has a connection (falling back to the legacy platform key for
platforms configured before their plugin is first enabled), and reads
prefer the connection key with a legacy platform-key fallback so
pre-refactor settings keep working. Phase 1 keeps one connection per
(owner, platform), so old and new keys map one-to-one.
- Composition injects the channel repository into the settings service;
without it the service keeps the legacy platform-only addressing
(existing unit tests pin that behavior unchanged).
- Tests: writes land on the connection key and not the platform key;
reads prefer the connection key over a stale platform value; legacy
platform values still resolve; a connection-less platform stays on the
platform key.
Ship the combined channel/settings refactor as one migration file: 031_client_preference_scope.sql becomes segment 4 of the renamed 030_channel_settings_refactor.sql (order preserved — the settings segment never touched the channel tables). Migration tests updated to run the combined version; the immutability gate passes (030 is not on main).
kaizhou-lab
marked this pull request as draft
July 28, 2026 03:58
…-refactor # Conflicts: # crates/aionui-ai-agent/src/factory/acp.rs # crates/aionui-ai-agent/src/registry.rs # crates/aionui-api-types/src/channel.rs # crates/aionui-app/src/router/state.rs # crates/aionui-app/tests/channel_e2e.rs # crates/aionui-app/tests/extension_e2e.rs # crates/aionui-app/tests/session_revoke_cleanup_e2e.rs # crates/aionui-channel/src/action.rs # crates/aionui-channel/src/channel_settings.rs # crates/aionui-channel/src/manager.rs # crates/aionui-channel/src/message_service.rs # crates/aionui-channel/src/pairing.rs # crates/aionui-channel/src/plugins/dingtalk/plugin.rs # crates/aionui-channel/src/plugins/lark/plugin.rs # crates/aionui-channel/src/plugins/telegram/plugin.rs # crates/aionui-channel/src/plugins/weixin/plugin.rs # crates/aionui-channel/src/routes.rs # crates/aionui-channel/src/session.rs # crates/aionui-channel/src/types.rs # crates/aionui-channel/tests/manager_integration.rs # crates/aionui-channel/tests/orchestrator_test.rs # crates/aionui-channel/tests/pairing_integration.rs # crates/aionui-channel/tests/session_action_integration.rs # crates/aionui-channel/tests/telegram_integration.rs # crates/aionui-conversation/src/service.rs # crates/aionui-db/src/models/channel.rs # crates/aionui-db/src/models/client_preference.rs # crates/aionui-db/src/repository/channel.rs # crates/aionui-db/src/repository/client_preference.rs # crates/aionui-db/src/repository/sqlite_channel.rs # crates/aionui-db/src/repository/sqlite_client_preference.rs # crates/aionui-db/src/repository/sqlite_user.rs # crates/aionui-db/tests/adoption_coverage.rs # crates/aionui-db/tests/channel_repository.rs # crates/aionui-db/tests/feedback_diagnostics_repository.rs # crates/aionui-db/tests/user_scope_migration.rs # crates/aionui-realtime/src/handler.rs # crates/aionui-system/src/client_pref.rs # crates/aionui-system/src/settings.rs # crates/aionui-system/tests/settings_routes.rs # crates/aionui-team/src/service.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the deferred structural work from the 2026-07-16 multi-account database design (§5.2 channel tables, §5.3 settings dedup), scoped by the 2026-07-27 gap analysis and split plan. Based on
feat/multi-account-user-scope; ships as one migration file (030_channel_settings_refactor.sql, four segments).Channel: connection entity (A1–A4)
assistant_plugins→channel_connections: generatedconn_*ids, platform moves toplugin_key, phase-1 one-instance-per-(owner, platform) unique index. Runtime map, incoming messages, and repository address channels by(owner_user_id, connection_id);PluginStatusResponsegainsconnection_id(UI addressing unchanged). Also fixes a latent no-op in the disable route for persisted extension channels.assistant_users→channel_users(composite FK into connections, revocation is a soft delete with audit retention,external_user_idrename) andassistant_pairing_codes→channel_pairing_requests(surrogate id, HMAC-SHA256code_hash— plaintext codes never persisted, pending partial uniques,approved_channel_user_id).conversationsgainsUNIQUE(user_id, id)as the §5.4 composite-FK foundation.assistant_sessions→channel_conversation_bindings: owner/connection derived from the active channel user (forged input ignored; revoked users cannot open bindings),agent_type/workspacedropped (owned by channel settings + conversation snapshot), cross-account conversation bindings made unrepresentable via triggers.assistant.{platform}.*→assistant.{connection_id}.*with legacy read fallback.Settings: dedup groundwork (B1–B2)
client_preferences['language']becomes the single language truth;/api/settingsproxies reads/writes (column kept as convergent fallback).client_preferencesgainsscope (device|account): machine-level keys (system.closeToTray,keepAwake,autoPreviewOfficeFiles,pet.*) promoted to single device rows; the foursystem_settingsswitches materialize as normalized account keys and/api/settingsserves all five fields preference-first — leaving B3 (frontend switch + table drop) mechanical.Frontend-facing API notes
idinstead ofcode; approve/reject accept{id}or{code}; thechannel.pairing-requestedevent carries both.ChannelSessionResponse.agent_type/workspaceare deprecated optional fields (omitted from JSON).Verification
Full workspace test suite, clippy
-D warnings, and fmt pass on the merged branch; migration segments carry row-conservation and cross-account checks that abort the migration on mismatch, plus dedicated migration tests (backfill inheritance, hashed-code storage, device-key promotion, trigger rejections).Out of scope per plan: multi-instance productization,
channel_chats,code_encryptedredisplay, B3.🤖 Generated with Claude Code