cordis-rs
Discovered★ 48Runtime-agnostic Rust port of Cordis, the plugin framework at the core of DeepSeek Harness — scoped dependency injection, lifecycle-owned effects, events, structured logging. Zero dependencies.
Cordis
English | 简体中文
Cordis is a typed runtime for long-lived, plugin-oriented Rust applications. It gives application components one model for lifecycle, service dependencies, typed events, resource cleanup, and explicit isolation boundaries.
Cordis is useful when your program is more than a collection of short-lived function calls: plugins can appear and disappear, services can become available or unavailable, configuration can change, and runtime resources must still be cleaned up deterministically.
Install
For applications, keep the historical package and import identity:
[dependencies]
cordis-rs = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use cordis::Context;
cordis-rs is now a thin application-facing facade over the v3 runtime contract.
Framework and plugin authors may depend on that contract directly:
[dependencies]
cordis-core = "0.2"
Optional capabilities stay explicit semantic dependencies:
cordis-timer = "0.2"
cordis-loader = "0.2"
Cordis v3 requires Rust 1.88 or newer and uses Rust 2024 Edition.
Migrating from 0.6.x
cordis-rs 0.8.x is the current application-facing v3 release line. The v3
architecture first shipped on the 0.7.x line. The 0.6.x implementation remains
on the legacy/0.6 maintenance branch for
critical bug and security fixes. The v3 transition is intentionally breaking;
see MIGRATION.md and docs/v3-migration.md.
The mental model
Five types carry most of the public model:
Context— a cheap immutable view into one Cordis Runtime.Plugin— reusable behavior with typed source configuration and runtime input.FiberHandle— the lifecycle handle for one admitted non-root Fiber.Service— a typed, named capability published into an exact service realm.Event— a typed runtime communication contract with explicit routing.
A Plugin enters the Runtime through a deliberate boundary:
Config
│
│ Plugin::prepare()
▼
Input
│
│ PreparedPlugin::from_input(...)
▼
PreparedPlugin
│
│ Context::spawn(...)
▼
FiberHandle / Fiber
prepare() runs before lifecycle admission. spawn() is the first operation
allowed to create Runtime lifecycle state.
Quick start
The smallest complete flow is: define an Event, define a Plugin, prepare and spawn it, dispatch the Event, then explicitly dispose the returned FiberHandle.
use std::convert::Infallible;
use cordis::event::{ListenerRegistrationError, observer_sync};
use cordis::{BoxError, Context, Event, Plugin, PreparedPlugin, Routing};
struct Ping;
impl Event for Ping {
const NAME: &'static str = "ping";
type Args = String;
type Output = ();
}
struct Echo;
struct EchoInput;
impl Plugin for Echo {
type Config = ();
type Input = EchoInput;
type PrepareError = Infallible;
type ApplyError = ListenerRegistrationError;
fn prepare(&self, (): ()) -> Result<Self::Input, Self::PrepareError> {
Ok(EchoInput)
}
async fn apply(
&self,
ctx: Context,
_input: &Self::Input,
) -> Result<(), Self::ApplyError> {
let _listener = ctx.on::<Ping, _>(observer_sync(|_, name| {
println!("hello, {name}");
Ok::<_, Infallible>(())
}))?;
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), BoxError> {
let ctx = Context::new();
let plugin = Echo;
let input = plugin.prepare(())?;
let prepared = PreparedPlugin::from_input(plugin, input);
let fiber_handle = ctx.spawn(prepared).await?;
ctx.emit::<Ping>(Routing::Unscoped, "world".into()).await?;
fiber_handle.dispose().await?;
Ok(())
}
Run the repository's complete version with:
cargo run -p hello_plugin
Lifecycle and convergence
A successful Context::spawn() returns a FiberHandle only after the new Fiber has
settled for the current service snapshot. The stable result is normally:
- Active — all required Services are available and
apply()succeeded. - Pending — a required Service is currently unavailable;
apply()has not run.
Requirements are declared with InjectSpec. They are lifecycle prerequisites,
not constructor injection. When an exact required Service publication appears or
disappears, Cordis converges affected Fibers toward their new stable state.
A FiberHandle exposes the main lifecycle operations:
ready()waits for the current stable state.restart()reapplies the current committed input on the same Fiber.update(PreparedChange)attempts a precommit-controlled typed input replacement; a committed update keeps the same Fiber.era_swap(PreparedChange)performs identity-breaking replacement; a successful successor has a fresh Fiber identity.dispose()ends the Fiber and runs its cleanup.
Dropping a FiberHandle does not dispose the Fiber. Lifecycle ownership is explicit.
Resources registered through a Plugin's apply Context are owned by that apply
generation. Listener registrations, Service publications, tasks, effects, and
timer operations can therefore be cleaned up with the generation instead of
being manually threaded through application code.
Services: exact placement, not fallback lookup
A Service is identified by its semantic Service name and resolved from one exact slot:
(Service, ServiceRealm)
By default a Context uses the Runtime's default realm. Isolation changes the realm selected for specific Service names.
To give one Service a fresh private slot:
let tenant_a = root.with_isolated_service(Database::NAME);
To isolate several Services, chain the operation:
let tenant_a = root
.with_isolated_service(Database::NAME)
.with_isolated_service(Cache::NAME)
.with_isolated_service(Ledger::NAME);
Each call changes only that Service's placement. Other Service mappings are inherited.
For explicit sharing and joining, allocate opaque realms and map Service names to them:
let shared_metrics = root.new_service_realm();
let tenant_a_db = root.new_service_realm();
let tenant_b_db = root.new_service_realm();
let tenant_a = root.with_service_realms([
(Database::NAME, tenant_a_db),
(Metrics::NAME, shared_metrics.clone()),
])?;
let tenant_b = root.with_service_realms([
(Database::NAME, tenant_b_db),
(Metrics::NAME, shared_metrics),
])?;
Now the tenants resolve different Databases but the same Metrics slot.
A ServiceRealm is only an opaque Runtime-local placement identity. It has no
hierarchy, parent lookup, textual rendezvous, or fallback rule. If a Context maps
Database to a private realm and that realm has no visible Database publication,
lookup is unavailable; Cordis does not fall back to the default realm.
Events: typed communication with explicit Scope routing
An Event declares a Runtime-local semantic name together with typed Args and
Output. Listener adapters make the listener role explicit:
- Observer — notification side effect.
- Responder — may answer a query.
- Mapper — transforms a waterfall payload.
- Around — onion-style middleware with a consuming
Next.
Dispatch always chooses routing explicitly:
ctx.emit::<Ping>(Routing::Unscoped, payload).await?;
ctx.emit::<Ping>(Routing::Scoped(request_scope), payload).await?;
Routing::Scoped(scope) reaches scoped registrations on the target Scope itself
and its ancestors, plus global registrations. Siblings and descendants are not
reached. Routing::Unscoped does not apply Scope eligibility filtering.
This makes Scope useful for questions such as:
Which behavior should be able to hear this Event?
Typical Scope boundaries are a tenant, request, workflow, session, or plugin-local event pipeline.
Scope and Service isolation are independent
A Context carries independent axes:
Context
├─ current Fiber
├─ isolate → which exact Service realm each Service resolves from
├─ Scope → which listeners are eligible for scoped Event dispatch
└─ intercept → ordered ConfigurableService configuration layers
Use Scope for Event reachability. Use Service isolation for Service placement.
| Question | Use |
|---|---|
| Which listeners may receive this Event? | Scope |
| Keep tenant A events out of tenant B's event subtree? | Scope |
| Which Database should this Plugin resolve? | Service isolation |
| Give two tenants different Caches? | Service isolation |
| Share Metrics while isolating Database? | explicit ServiceRealm mappings |
The axes do not imply one another. Two Contexts may share the same Service realm while living in different Scopes, or share the same Scope while resolving a Service from different realms.
When a Plugin is spawned, its Service dependency edges are resolved against the spawning Context's isolate mapping, while the new Fiber receives its own child Scope. This lets sibling Plugins share exact Services without accidentally sharing one Event seat.
Crates
| Crate | Role |
|---|---|
| cordis-rs | application facade preserving the historical cordis import |
| cordis-core | canonical Context, Plugin/FiberHandle lifecycle, Services, Events, effects, logging, runtime observation |
| cordis-timer | generation-owned sleep, interval, and timeout operations |
| cordis-loader | immutable declarative load plans and synchronous typed target resolution |
cordis-core deliberately does not depend on serde/serde_json or Tokio's time
driver. Declarative loading and time operations stay in optional leaf crates.
Examples
Every example is standalone, headless under CI, and exits on its own.
cargo run -p hello_plugin
cargo run -p gateway
cargo run -p worker_daemon
cargo run -p scopes_tenants
cargo run -p logging_exporters
cargo run -p chat_capstone
What they demonstrate:
| Example | Focus |
|---|---|
| hello_plugin | smallest correct Plugin/Event lifecycle |
| gateway | declarative JSON boot, scoped routing, typed updates, timeout |
| worker_daemon | failure/recovery, Context::run, restart/update, sleep/interval |
| scopes_tenants | Service realms and Event Scope as independent axes |
| logging_exporters | logging and runtime observation |
| chat_capstone | the full composition, including Pending convergence, update, and era replacement |
Start with hello_plugin; use the other examples as focused tours of the public
surface.
Design boundaries worth knowing
Cordis intentionally does not model a general Context hierarchy or a nested DI container. Context derivation changes explicit axes only.
That means:
- Scope ancestry is Event routing, not lifecycle ownership.
- Service realms are placement identities, not namespaces.
InjectSpecdeclares lifecycle requirements, not lookup fallback.- spawn origin records provenance, not parent/child ownership.
- update preserves Fiber identity; era replacement deliberately does not.
These boundaries keep event routing, service placement, and lifecycle semantics independent instead of letting one hidden tree control all three.
Project status
The v3 semantic crates began at 0.1.0 and now publish on the 0.2.x line; the
historical application package entered v3 at cordis-rs 0.7.0 and now publishes
on 0.8.x. The workspace uses Rust 2024 Edition with MSRV 1.88.
As a pre-1.0 project, the public API may still evolve.
Cordis is a Rust port and redesign in the lineage of
cordiverse/cordis.
License
MIT. See LICENSE.
Similar plugins
by a1113622001
DeepSeek Harness (cordis) plugin: self-update for the harness launcher - checks npm for a newer @deepseek-ai/dsh, stages it, and applies it on harness exit (or update-and-restart from the web panel).
★ 3
MIT
JavaScript
Aug 22, 2026
dsh plugin --profile web add dsh-auto-updateby ZSeven-W
DeepSeek Harness (DSH) plugin: a read-only ledger for the plugins you already have installed — a capability inventory with file:line evidence, declared-vs-detected reconciliation, cross-profile versio
★ 24
↓ 44/wk
MIT
JavaScript
Sep 11, 2026
dsh plugin --profile web add @zseven-w/dsh-harborAgent preset for DeepSeek Harness that layers the self-referential Cordis toolset (dynamic plugins, runtime inspection) and preset-authoring skills onto the PTC Code-Mode preset; materialized files ca
★ 0
↓ 501/wk
dsh plugin --profile web add dsh-ptc-cordis-presetby ZK-Andy
Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state refined from session trajectories, with a benchmark-driven validation loop.
★ 19
MIT
TypeScript
Sep 4, 2026
dsh plugin --profile web add dsh-continual-evolveby gwsbhqt
洞察 — read-only insight panel for a DeepSeek Harness profile: where every plugin, service, tool and model came from, which config layer inserted or disabled it, and what is running right now.
★ 3
↓ 57/wk
MIT
TypeScript
Sep 10, 2026
dsh plugin --profile web add @gwsbhqt/dsh-insightby cyanseek
Deterministic fault injection and autonomous resilience tests for DeepSeek Harness tools
★ 4
MIT
JavaScript
Sep 12, 2026
dsh plugin --profile web add dsh-tool-chaos