Files
MoviePilot/docs/rules/05-architecture.md
T

40 KiB

05 - Architecture and Modules

Directory Model

MoviePilot keeps the established product packages such as app/chain, app/agent, app/modules, app/db, app/api, app/startup and app/workflow in their original locations. The historical app/core, app/helper and app/utils roots are virtual compatibility packages only; physical Python sources must not be recreated there.

The legacy roots have no physical directories in the source tree. Current images and update flows write site resources only to app/application/site/; plugin imports under app.helper.* are resolved exclusively by the exact runtime compatibility manifest.

Capabilities migrated out of those legacy roots are organized by technical responsibility:

Entrypoints / Plugins
        |
        v
API / Agent / CLI / Scheduler / Workflow
        |
        v
Chain orchestration ---------> Application services
        |                              |
        +----------> Modules / DB <----+
                       |
                       v
             Domain / Runtime contracts
                       |
                       v
              Foundation / Adapters

Startup remains the composition root. SDK and compatibility are boundaries,
not dependencies of canonical implementation modules.

Directory grouping does not override dependency direction. The architecture gate builds the complete Python module graph and rejects cycles even when a cycle passes through an established package that was not moved.

Canonical Migrated Packages

Package Ownership
app/foundation/ Stateless, config-free and I/O-free primitives: reflection and dynamic import, crypto, DOM parsing, identity, collections, singleton, text conversion/segmentation, URL and version helpers
app/domain/ Pure MoviePilot business semantics for media, recognition, sites and torrents; live configuration, persistence, transport and acceleration are injected
app/application/ Focused stateful application services, configured capability selection and service-bound rules
app/runtime/ Process-wide config, events, complete logging runtime, cache contracts/in-memory policy, execution, background-task ownership, localization, scheduling, restart state, concurrency, GC and rate limits
app/adapters/ Concrete technical I/O and named external ecosystems, split by cache, network, system and external boundaries
app/sdk/ Stable, deliberately curated imports for plugin authors

The packages above are the only top-level roots created by the legacy-module refactor. Existing product roots remain unchanged rather than being moved only to make the directory tree look symmetrical.

Application boundaries

Path Ownership
app/application/*.py Established single-module application services and compatibility facades
app/application/subscription/ Subscription use cases: write.py owns media-to-row translation and the write port; contract.py owns shared metadata/media-key projection; query, mutation, deletion, identity and search stay in their single-word modules
app/application/search/ Search state and later search-plan use cases
app/application/download/ Download task querying/control and later submission use cases
app/application/music/ Multi-source music catalog orchestration
app/application/chain/ Injectable Chain runtime context and compatibility provider
app/application/agentdata.py Named Agent data ports; canonical Agent consumers use get_agent_*_port() and do not alias legacy proxies to Oper classes
app/application/outbox.py Durable intent and Outbox repository/dispatcher contracts for post-commit side effects
app/application/plugin/ Plugin market catalog, installation command, runtime port, folder operations and dynamic-route use cases; filenames remain single words (catalog.py, install.py, runtime.py, folders.py, routes.py)
app/application/server/ MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup
app/application/site/ Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here
app/application/messaging/ Message rendering/routing, interactions and the Agent-to-message bridge: ingress.py owns the single channel-to-host loopback boundary; interaction.py shared interaction contracts and view helpers; router.py unified interaction priority and callback dispatch; site.py/subscribe.py/skill.py per-command sessions, input parsing and views; media.py media interaction state while the business workflow stays in MediaInteractionChain; plugin.py plugin input capture and plugin button callbacks; agent.py agent choice state, callback protocol and WebAgent bridge; message.py notification rendering, templates and queue. Not a public SDK recommended for direct plugin use
app/application/security/ Authentication, authorization, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy

Application services may use domain rules and runtime contracts. They own the persistence Protocol needed by a use case, but must not import app.db, SQLAlchemy, Session, Oper classes or concrete adapters. app/db/adapters/ implements those Protocols and startup injects the implementation. Multi-domain workflows still belong in the existing app/chain/ package. Chain, Service and Manager remain class patterns; they do not create additional top-level directory categories.

Runtime boundaries

Path Ownership
app/runtime/config.py Deployment configuration and resolved runtime settings
app/runtime/topology.py Process topology policy shared by startup and offline diagnostics
app/runtime/events.py Event contracts, dispatch and resolver registration
app/runtime/event/ Event registry, explicit handler binding, dispatch barrier/concurrency and isolated error handling
app/runtime/observability/ Low-cardinality metric contracts and no-op-capable observation facade
app/runtime/log.py Complete console/plugin/file logging runtime and shutdown
app/runtime/cache.py Cache protocols, memory implementations, decorators and proxies
app/runtime/managed_resources.py Provider-neutral acquisition, observation and shutdown facade for process-owned optional resources
app/runtime/tasks.py Lifespan-scoped ownership, cancellation and bounded shutdown waiting for in-process background tasks
app/runtime/execution.py Shared sync/async execution and cross-thread submission boundary with correlation propagation
app/runtime/correlation.py Request/cross-thread correlation context and safe propagation into logs and child work
app/runtime/state.py Process restart and update state
app/runtime/extensions/ Module, plugin, configured-service and managed-resource discovery/registration/lifecycle adapters
app/runtime/compat/ Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics

app/startup/ remains the established composition root and is not nested under runtime. Its root contains only composition/, initializers/ and lifecycle/: composition constructs and injects cross-layer dependencies, initializers expose domain-scoped startup/shutdown hooks, and lifecycle orders those hooks and decides restart policy. Reusable persistence implementations belong in app/db/adapters/, not startup. Lower-level runtime modules must not import startup. Startup publishes its frozen, slotted HostRuntime through FastAPI app.state. API dependencies must narrow that object to a domain runtime (for example, AgentChatRuntime) instead of adding a string key to a global service map. Legacy registries may delegate the same object while domains migrate, but they must not construct a second set of service instances. Canonical host consumers of the process-wide module, plugin, scheduler and system-configuration runtimes must call get_module_manager(), get_plugin_manager(), get_scheduler() and get_configured_system_config() explicitly. The class-shaped ModuleManager and Scheduler application facades, the concrete plugin manager class paths and DB SystemConfigOper remain compatibility or composition boundaries; host code must not import those facades or alias a getter back to a manager/Oper class name. API, Scheduler and Chain deployment values are exposed as frozen snapshots from HostRuntime.configuration; canonical callers must not add a fresh direct settings import when the required field belongs to an existing snapshot.

app.schemas and the app.db package root are compatibility facades, not implementation dependency hubs. Host code imports concrete schema submodules; the schema root resolves its generated export manifest lazily for plugins and legacy callers. DB internals import base, decorators, engine, session, concrete models and Oper modules directly. app.db.models.load_all_models() is the explicit composition entry used before metadata creation or migration; importing one model must not import every table.

app/db/oper/ owns table-oriented SQLAlchemy access and receives a caller-owned Session. app/db/adapters/ is the concrete persistence-adapter layer: it may depend on Application-owned Protocols, UoW/Session and Oper implementations. This deliberate dependency inversion is the only DB implementation -> Application contract direction; Application must remain free of DB imports. Migrated workflow, user, interaction, messaging, music, site, media-server, download, subscribe and transfer Chain consumers use the named get_chain_*_port() functions from app/application/chain/data.py; they must not alias migration-time *PortProxy classes back to database Oper names. Those proxy classes remain compatibility boundaries while the other established Chain domains migrate independently. Agent orchestration, memory and tool implementations follow the same rule via the named get_agent_*_port() functions from app/application/agentdata.py. The legacy Agent *Port proxy classes remain import-compatible boundaries and must not be reintroduced as Oper aliases in canonical Agent modules. Monitor history checks use get_transfer_history_port() from app/application/history.py; the constructible TransferHistoryPort facade is retained only for compatibility and is not a canonical Oper substitute. Canonical Chain, API, Scheduler and Agent consumers read notification and media server configuration through the named helpers in app/application/notification.py and app/application/mediaserver.py. ServiceConfigHelper remains the parser at the startup/runtime module boundary and a plugin SDK compatibility export; it is not a second application-facing service directory.

Adapter boundaries

Path Ownership
app/adapters/cache/ Redis and filesystem cache implementations and Redis clients
app/adapters/network/ Generic HTTP, browser, DNS, Cloudflare and IP transport mechanisms
app/adapters/system/ OS/filesystem/process facilities, stdio, display, packages, resources and optional Rust acceleration
app/adapters/external/ CookieCloud, plugin market, OCR, IP-location providers and MoviePilot Server
app/adapters/web/ FastAPI-specific technical adapters, including raw dynamic plugin routes
app/adapters/observability/ Optional telemetry exporters; core code depends only on runtime observation ports
app/adapters/external/plugin/client.py Read-only plugin-market and local-repository client over the established PluginHelper implementation
app/adapters/system/plugin/ Plugin package and dependency I/O (package.py, dependency.py)
app/db/adapters/ SQLAlchemy implementations of Application-owned persistence Protocols

Generic protocol transport belongs in adapters/network; a named product or ecosystem workflow belongs in adapters/external. An adapter may depend on foundation, domain models, schemas and narrowly required runtime contracts, but must not import application services, runtime/extensions, runtime/compat or the plugin SDK.

RSS is not classified as a transport adapter merely because it uses HTTP. The current RssHelper combines feed parsing, torrent item semantics, configured site-specific URL discovery and browser fallback, so it belongs to app/application/rss.py and consumes network adapters. Likewise, the generated site extension owns the configured catalog/authentication/index capability and lives in app/application/site/; only its download and file installation mechanism remains in app/adapters/system/resource.py.

可选的进程级技术资源使用 Managed Resource 合同:实现及其 data-only capability.toml 与适配器同目录,runtime/extensions 只解释通用的同步/异步 startstop 生命周期,startup 负责构建 Capability Runtime。声明必须使用 on_first_use,普通启动只发现声明;消费者通过 app/runtime/managed_resources.py 显式获取资源。关闭路径先释放消费者,再关闭已初始化 Runtime,未使用的资源不得因关闭而物化。 应用级启动顺序使用 app/startup/lifecycle/components.py 的组件描述声明依赖、 normal/safe-mode 范围、start/stop 顺序、超时预算和失败策略。新增进程级资源不得只在 lifespan() 中追加过程代码,必须先进入可导出的生命周期清单并补顺序快照测试。 Host Module 的 stop() 可以显式返回 False 表示资源 owner 尚未收敛; HostModuleAdapter 必须将它视为 stop 失败,Capability Runtime 保留原 owner 供后续重试, ModuleManager 与 startup 组合根继续关闭其余资源但必须向上返回未收敛,不得把记录日志等同于成功。 同步和异步 Capability Runtime 的 shutdown 必须使用同一布尔收敛合同;Agent、Managed Resource 等领域关闭入口必须直接传播 Runtime 的整体结果,不得以单个能力快照或无返回包装器覆盖失败。 消息渠道模块必须通过 _MessageChannelModuleBase._stop_service_instances() 聚合多实例关闭结果; 长连接、轮询或 Socket 服务只有在真实终止后才能返回成功,超时 owner 不得清空句柄。 应用消息队列的监控线程遵守同一收敛语义:停止必须有限等待,回调阻塞导致线程仍存活时保留 owner 并向 startup 返回 False,不得用无界 join() 阻塞生命周期或把日志当作成功。 API 中允许丢失或可重建的进程内任务必须登记到 app/runtime/tasks.py;登记器先于其他 运行资源启动,并在资源释放前停止接收、取消和有限等待。需要崩溃恢复的 E2/E3 副作用仍应 进入 Outbox 或持久任务表,不能把 TaskRegistry 当成 durable queue。 Runtime 关闭后不可逆;完整应用生命周期的再次启动必须由新进程承载,不能在同一解释器中重建局部资源域。 插件需要浏览器时使用 app.sdk.browser,由宿主浏览器适配器协调资源,不直接依赖资源实现。 旧插件若直接导入有资源前置条件的第三方包,compat 在插件 import 前递归扫描源码并保守准备资源; 无法精确解析的文件按全部已登记资源降级,最终可导入性仍由 Python loader 判断。

app/foundation/crypto.py stays in foundation because it contains only generic RSA, digest and CryptoJS-compatible AES primitives and has no settings, policy, I/O or logging. Authentication, token, passkey, signing and two-factor policy still belongs in app/application/security/; callers decide how cryptographic failures are reported.

Domain subdomains

app/domain/ is a business package, not a synonym for every file whose name mentions media, site or torrent:

Subdomain Modules and ownership
Media context.py owns Context, MediaInfo and TorrentInfo; media.py owns source/ID normalization; title.py owns title-candidate and search-keyword rules; episode.py owns episode-range display; scraper.py owns Kodi-style NFO reading and metadata document generation
Recognition metainfo.py, meta/ and tokens.py parse names, paths, release groups, streaming platforms, anime, video and music metadata
Site site.py owns site-domain exceptions and interprets HTML into business states such as logged-in and checked-in; configured catalog/auth/index resources stay in app/application/site/, generic URL/DOM parsing stays in foundation and network access stays in adapters
Torrent torrent.py owns magnet-link semantics; configured download/cache/file behavior stays in app/application/torrent.py

app/domain may depend only on schemas and foundation. It must not read global settings, access DB/network/filesystem adapters, import Rust, discover services or initialize process runtime state.

StringUtils is not a canonical implementation type. Generic text, capacity, time, URL, DOM, hash and version functions live under app.foundation; media title, episode, site and torrent rules live in their owning domain modules. Host code must import those implementations directly. app.sdk.string.StringUtils only composes the complete historical static-method surface for plugins, and both app.utils.string and the retired app.domain.string resolve to that same SDK module through the compatibility manifest.

Established Packages That Stay in Place

The following roots predate this migration and must not be moved or renamed as part of migrated-capability cleanup:

  • app/agent/
  • app/api/
  • app/chain/
  • app/db/
  • app/doctor/
  • app/modules/
  • app/monitor/
  • app/plugins/
  • app/schemas/
  • app/startup/
  • app/testing/
  • app/workflow/

Necessary canonical import updates are allowed; changing their physical layout or product responsibilities requires a separate architectural decision.

Placement Decision Order

Use these questions in order before creating or moving a migrated capability:

  1. Is it generic, stateless, independent of MoviePilot state and free of I/O? Put it in app/foundation.
  2. Is it a pure MoviePilot business rule/model? Put it in app/domain.
  3. Does it read persisted configuration or coordinate one focused configured capability? Put it in app/application.
  4. Is it authentication, authorization, signing, SSRF, URL/path safety, OTP, passkey or two-factor policy? Put it in app/application/security.
  5. Is it message rendering, routing or interaction behavior? Put it in app/application/messaging.
  6. Is it process-wide configuration, events, logging, cache policy, execution, scheduling, concurrency, GC or restart state? Put it in app/runtime.
  7. Does it discover/manage modules, plugins or configured service providers? Put it in app/runtime/extensions.
  8. Does it perform concrete cache, network, OS/process, filesystem, stdio, package/resource or Rust I/O? Put it under the matching app/adapters technical boundary.
  9. Does it implement a named external product/ecosystem? Put it in app/adapters/external.
  10. Is it public to plugins or only preserving an old path? Curate it in app/sdk or map it in app/runtime/compat; never move implementation there.

Do not create generic common, helper or utils buckets. Reuse does not erase ownership.

New production Python module filenames use one lowercase word. When one topic needs multiple modules, create a topic package and keep each child filename to one word, for example runtime/event/{registry,binding,dispatch,errors}.py or application/subscription/{contract,delete,identity}.py. Established multiword public import paths may remain as compatibility exceptions after plugin/import scanning, but they are not templates for new modules. Test filenames continue to follow pytest's descriptive test_<behavior>.py convention.

Legacy module paths belong in app/runtime/compat/manifest.py. New implementation modules must not re-export old managers, helpers or Oper classes just to preserve imports or tests. A public runtime object whose path or identity is itself part of the plugin ABI stays at its established path as a thin facade; new plugin-facing symbols are exported deliberately through app/sdk and its architecture snapshot, not through incidental module globals.

Existing Chain, Module and DB Layers

Chain layer

app/chain/ implements use cases shared by API, CLI, Agent, scheduler and other entrypoints. Chains may coordinate modules, application services, injected persistence Ports, events and caches. New chain-to-chain dependencies are allowed only while the static graph remains acyclic. Backend protocol details and HTTP request objects do not belong here. Chains interact with modules exclusively through run_module dispatch on method-name contracts; direct imports of module internals (classes, exceptions, constants) are forbidden, so every module stays pluggable and a chain never names a concrete module implementation. The dispatch algorithm belongs to app/runtime/extensions/module/dispatcher.py; ChainBase remains the compatibility facade. New chains and tests inject the minimal ChainRuntimeContext from app/application/chain/context.py. No-argument Chain() remains supported through the startup-configured compatibility provider. High-frequency string methods are classified in module/contracts.py; unknown third-party plugin methods retain the frozen legacy aggregation contract, while the architecture baseline records every literal method and call site.

Underscore-prefixed files in app/chain/ are feature-domain mixins for ChainBase and concrete chains, not chains themselves: _recognition.py (RecognitionMixin), _messaging.py (MessageProcessingMixin / NotificationMixin), _interaction.py (InteractionChainMixin, the shared slash-command delegation for remote_list / parse_callback / handle_callback_interaction / handle_text_interaction), _music.py (MusicSubscribeMixin, the music single/album subscribe domain mixed into SubscribeChain) and _transfer.py (TransferChain feature mixins). Shared subscription metadata and media-key construction belongs to app.application.subscription.contract; app.chain.subscribe keeps the old helper names only as compatibility forwards and _music must not import its concrete chain owner. A concrete chain that exposes slash-command interaction inherits InteractionChainMixin, injects its handler class via _interaction_handler_type and implements only _interaction_handler; it must not re-export application-layer interaction managers.

Module layer

app/modules/ contains pluggable downloaders, media servers, metadata sources, message channels, indexers and storage providers. New direct module-to-module or module-to-chain dependencies are forbidden; cross-module orchestration belongs in a chain. Module internals stay sealed inside the module: shared constants, exceptions and value domains used by both modules and upper layers live in schemas, and module capabilities are exposed to chains only as dispatched method names. The directory remains unchanged because discovery and plugin code depend on this established runtime root.

app.modules.filemanager is a lazy compatibility entrypoint. The concrete FileManagerModule implementation lives in app.modules.filemanager.module, while the historical capability path and class module identity remain app.modules.filemanager:FileManagerModule. Storage and transfer-handler submodules must not import the concrete module implementation through the package root.

app/modules/_base/ hosts the shared template base classes for module families (downloader.py, mediaserver.py, notification.py), each combining the family mixin with _ModuleBase and typed by TService (usage: class QbittorrentModule(_DownloaderModuleBase[Qbittorrent])). The base classes carry only verbatim-duplicated boilerplate — connection test, scheduled reconnect, torrent-info reading, query-status normalization for downloaders; authentication, media-exists check, inactive-server handling for media servers; admin resolution and command registration for message channels — while subclasses keep the differentiated API calls and override small hooks such as _test_connection, _test_server and _is_inactive. Discovery already skips the package (module discovery only enumerates first-level submodules and skips underscore-prefixed names), so no new exclusion rules are needed; do not grow this package with per-module business logic.

Channels and storages that need login management or temporary-parameter initialization follow one generic contract instead of per-target APIs: modules implement channel_manage(channel, action, **params) or storage_manage(storage, action, **params), route by the requested target identifier (returning None for other targets, accepting both enum members and plain strings), and interpret actions from the shared schemas.types.NotificationAction / StorageAction vocabulary plus opaque form parameters themselves. All results use the unified {"success": bool, "message": ..., "data": ...} shape. NotificationChain.manage_channel and StorageChain.manage_storage forward transparently and must stay free of any channel/storage-specific names or logic; new channels or storages adopt the same contract without touching the chains. The endpoint layer exposes this as two generic endpoints (POST /api/v1/notification/manage, POST /api/v1/storage/manage) taking the common schemas.ManageRequest body (target + action + params) and must never define target-specific names, parameters or response fields — the frontend supplies them and the endpoint passes them through untouched.

LLM providers follow the same contract: LLMProviderManager.provider_manage dispatches actions from the shared schemas.types.LlmProviderAction vocabulary, seals default-value filling, key sanitization and error rewriting inside, and the endpoint layer exposes a single POST /api/v1/llm/manage with the same ManageRequest body. The only exception is the named OAuth callback route (GET /api/v1/llm/provider-auth/callback/{provider_id}), which stays named because external browsers redirect to that URL; the endpoint builds the callback URL from that route name and injects it as an action parameter.

DB / Oper layer

SQLAlchemy models stay under app/db/models/; the data access classes live in app/db/oper/ and mirror them one-for-one (models/subscribe.pyoper/subscribe.py), so a filename carries only the entity and the package name carries the role. Two verified aggregation exceptions exist: the site family (Passkey, SiteIcon, SiteStatistic, SiteUserData) is consolidated in oper/site.py, and AgentTaskRun lives in oper/agenttask.py. DB adapters use Oper classes instead of issuing SQLAlchemy queries directly. Application and Chain code reaches persistence through named Ports/Protocols; concrete DB adapters are the layer that adapts those Ports to Oper classes. Every schema change requires an Alembic migration under database/versions/.

Oper classes take and return persistence values, not domain objects. Translating MediaInfo / MetaBase into a row is business logic and belongs in app/application/ — see application/subscription/write.py and application/history.py for the two write paths. Column-type coercion (numeric year to string, boolean switches to integers) stays in the Oper because it follows the column, not the caller.

Invariants that must hold for every write are enforced at the mapper rather than at each call site: app/db/models/_identity.py normalizes media_source / media_id on before_insert / before_update, so a new write path cannot forget them. Identity representation rules themselves (alias folding, trimming, rejecting zero) live in app/schemas/media.py alongside the two identity mixins; app/domain/media.py keeps only source policy. app/db therefore has no dependency on app/domain.

Durable post-commit side effects have a separate boundary:

  • app/application/outbox.py owns the Outbox intent, repository and dispatcher contracts. An Application command stages the business mutation and its durable intent in the same transaction.
  • app/db/adapters/outbox.py implements the persistence port with SQLAlchemy; app/startup/composition/subscription.py and the other composition modules provide the concrete repository, UoW and handlers.
  • The dispatcher claims an intent with a lease, executes the topic handler, and records retry/dead-letter state. Handlers must be idempotent and must not rely on a live request object.
  • app/runtime/tasks.py is only the in-process TaskRegistry boundary. It owns cancellation and bounded shutdown waiting, but it is not a durable queue and must not replace an Outbox or persistent task table.

Composition and Compatibility Boundaries

  • Startup registers concrete cache factories before decorated business modules are imported. Cache contracts remain in app/runtime/cache.py; Redis/file implementations remain in app/adapters/cache/backends.py.
  • app/runtime/log.py is a dependency leaf with no app.* imports. Foundation emits no runtime logs; upper-layer owners decide whether failures are operationally relevant.
  • app/adapters/system/resource.py only reports whether installation occurred; app/startup/initializers/modules.py supplies the loaded site-resource versions and decides whether to restart. The adapter never imports the site application service.
  • Configured notification discovery lives in app/application/notification.py. Web Push subscription and manual-send HTTP behavior stays in app/api/endpoints/message.py.
  • app/runtime/compat stores string mappings and resolves aliases lazily. It may not eagerly import canonical MoviePilot modules.
  • 已删除的 app.db.<entity>_oper 路径继续由精确模块映射提供给旧插件;其中订阅写入、 整理历史写入和拆分后的用户认证依赖通过 app.sdk._legacy 薄门面委托 canonical Application/Oper,不把领域对象或 HTTP 依赖重新引回 DB 层。
  • 物理模块仍存在但公开符号已经迁走时(例如 app.domain.media 的身份原语、 app.schemas 的整理工作项),兼容 Finder 在标准 Loader 执行后叠加白名单符号路由; canonical 模块不得为兼容而反向 import app.runtime.compat
  • Canonical implementation packages may not import app/runtime/compat or app/sdk.
  • Host code uses canonical paths. Only app/plugins/ and compatibility tests may use app.core, app.helper, app.utils or app.log.
  • New plugins use app.sdk. In DEBUG mode, a legacy plugin import remains functional and emits one actionable warning per plugin and legacy module.
  • Delayed imports are not accepted as a way to hide dependency cycles.

Permitted Call Directions

Direction Status
entrypoint -> chain / application / injected persistence Port Allowed according to workflow complexity
chain -> module (only via run_module dispatch) / application / injected Port / canonical capability Allowed; direct chain -> module and chain -> Oper imports forbidden
chain -> agent implementation Forbidden; chains reach Agent runtime only through app/application/agent.py; app/startup/initializers/agent.py registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used
agent.tools -> api / scheduler / command Forbidden; tools use app/application/plugin/routes.py, plugin/folders.py, scheduling.py and commands.py application services
api -> factory Forbidden; the FastAPI route adapter is injected into app/application/plugin/routes.py by the composition root after creation
api / chain -> app.workflow Forbidden; workflow consumers use app/application/workflow.py, while only app/workflow/** and app/startup/initializers/workflow.py access the concrete runtime
application -> domain / runtime contract Allowed
application -> DB / Oper / concrete adapter Forbidden; define a Protocol in Application and inject an implementation
db.adapters -> application persistence Protocol / db.oper / UoW Allowed; this is dependency inversion, not an upper-layer use-case call
module -> canonical capability / Application persistence Port Allowed; direct Oper imports are forbidden for new code
module -> module / chain Forbidden for new code
adapter -> application / runtime.extensions / sdk / compat Forbidden
domain -> runtime / adapter / application / DB Forbidden
foundation -> other app packages Forbidden
canonical implementation -> sdk / compat Forbidden
compat -> canonical implementation at module import time Forbidden
Any import that creates a module-level cycle Forbidden

Key File Locations

Path Purpose
app/application/agent.py Agent orchestration facade (get_agent_manager / get_prompt_manager / capability queries / prompt builders); lightweight providers register through app/startup/initializers/agent.py, with no static application -> agent edge
app/agent/runtime_loader.py Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under app/agent/
app/application/subscription/write.py Subscription media translation and sync/async write-port orchestration
app/application/outbox.py Durable intent, topic handler and Outbox repository contracts
app/db/adapters/outbox.py SQLAlchemy Outbox persistence, claim/lease and retry state adapter
app/application/scheduling.py Runtime scheduler facade for Agent tools and endpoints; Scheduler class registered by app/startup/initializers/scheduler.py
app/application/commands.py Command registry facade for Agent tools and endpoints; Command class registered by app/startup/initializers/command.py
app/application/workflow.py Workflow use cases plus the runtime port consumed by API and Chain; WorkFlowManager is registered by app/startup/initializers/workflow.py
app/db/adapters/ SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols
app/startup/composition/ HostRuntime, configuration snapshots and cross-layer adapter wiring
app/startup/initializers/ Domain-scoped initialization and shutdown hooks
app/chain/agent.py AgentChain(ChainBase): the chain-layer entry for Agent sessions; Agent runtime stays in app/agent/
app/runtime/config.py ConfigModel, Settings and deployment configuration
app/runtime/tasks.py TaskRegistry owner, cancellation and bounded shutdown waiting
app/runtime/execution.py Shared execution/thread-boundary helpers and context propagation
app/runtime/correlation.py Correlation ID context and propagation boundary
app/runtime/topology.py Single-worker full-runtime policy and safe-mode topology validation
app/runtime/events.py EventManager/Event compatibility facade and global eventmanager identity
app/runtime/event/registry.py Event subscriptions, enable/disable state and dispatch snapshots
app/runtime/event/binding.py Explicit module/plugin/host handler resolvers; unresolved classes are diagnosed and skipped, never implicitly constructed by the bus
app/runtime/event/dispatch.py Chain/broadcast ordering, concurrency, target-plugin filtering and isolated delivery
app/runtime/event/errors.py Handler failure notification and non-recursive SystemError downgrade policy
app/runtime/extensions/module/dispatcher.py Plugin-first invocation, short-circuit, list merge, signature relay and sync/async execution
app/runtime/extensions/module/contracts.py High-frequency method families and frozen legacy fallback contract
app/application/chain/context.py Injectable Chain dependencies and no-argument compatibility provider
app/startup/lifecycle/components.py Declarative normal/safe-mode lifecycle manifest, ordering and timeout budgets
app/runtime/extensions/module_manager.py Module discovery and lifecycle
app/runtime/extensions/plugin_manager.py Plugin discovery and lifecycle
app/runtime/extensions/plugin/monitor.py Plugin file-change aggregation and monitor-thread lifecycle
app/runtime/extensions/plugin/projection.py Plugin commands, APIs, services, modules and actions projected from a running-registry snapshot
app/runtime/extensions/plugin/storage.py Injected plugin configuration/data persistence port; runtime code does not import DB Oper classes
app/application/plugin/catalog.py Plugin-market mapping, concurrent collection, generation merge and source/version deduplication
app/application/plugin/install.py Compatibility, package installation, reporting, installed-list persistence and runtime reload command
app/application/plugin/routes.py Dynamic plugin-route registry protocol and registration/removal use cases; plugin response payloads remain raw unless the plugin chooses its own envelope
app/application/plugin/folders.py Plugin-folder cleanup use case, compatible with current dictionary and legacy list storage shapes
app/application/plugin/runtime.py Plugin runtime port consumed by API, Agent and Workflow; the concrete PluginManager is registered only by startup
app/application/module.py Host module runtime port consumed by entrypoints; the concrete ModuleManager is registered only by startup
app/application/scheduling.py Scheduler runtime port consumed by API/Agent/application commands
app/application/server/report.py Server reporting use cases over injected local readers and transport callbacks
app/application/server/share.py Server sharing use cases over injected repositories and transport callbacks
app/adapters/external/plugin/client.py Plugin-market read adapter and cache-refresh boundary
app/adapters/system/plugin/package.py Plugin package installation adapter
app/adapters/system/plugin/dependency.py Plugin dependency inspection and installation adapter
app/runtime/extensions/managed_resource_adapter.py Data-only managed-resource registry and sync/async lifecycle adapters
app/runtime/managed_resources.py Lightweight acquisition, state observation and shutdown facade
app/foundation/reflection.py Generic reflection and Python module discovery
app/adapters/network/http.py Shared synchronous and asynchronous HTTP clients
app/adapters/network/browser.py Browser launch facade and browser session implementation
app/adapters/system/display/ On-first-use virtual display resource and legacy DisplayHelper facade
app/application/rss.py Configured RSS retrieval and parsing
app/application/site/sites.* Generated site catalog, authentication and index capability plus its colocated data bundle
app/runtime/cache.py Cache contracts, memory backend, decorators and proxies
app/adapters/cache/backends.py Redis and filesystem cache adapters
app/adapters/system/resource.py Runtime resource detection/download/installation
app/adapters/system/fsproxy.py Timeout-guarded local filesystem operations in a killable subprocess (with colocated fsworker.py)
app/adapters/external/wechat_crypt.py WeChat enterprise-message XML encryption/decryption protocol
app/application/rules.py Rule domain: user rule-group config access (RuleHelper), built-in torrent filter rule set and rule parser
app/adapters/external/market.py Plugin repository discovery and installation
app/application/security/url.py URL/path validation, SSRF protection and signed image policy
app/application/mediaserver.py Configured media-server discovery and identity matching
app/runtime/compat/manifest.py Exact legacy-to-canonical import manifest
app/sdk/ Stable plugin imports, including provider-neutral browser launch functions

Run tests/test_architecture_dependencies.py after every ownership or import change. It rejects physical legacy or retired canonical sources, forbidden upward dependencies, SDK/compat backreferences, any strongly connected component containing a migrated module, module-to-module or module-to-chain imports, entrypoint (api/agent/monitor/workflow/doctor) imports of app.modules internals, chain imports of app.modules internals (chains reach modules only through run_module dispatch), and downloader SDK (qbittorrentapi, transmission_rpc) imports inside app/chain.

Last Updated: 2026-08-24