Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ jobs:
run: python3 scripts/release.py check
- name: Check
run: cargo check --workspace --all-targets --all-features --locked
- name: Verify application builder embedding example
run: |
cargo fmt --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --check
cargo clippy --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --all-targets --locked -- -D warnings
- name: Documentation links
run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --all-features --no-deps --locked
- name: Test
Expand Down Expand Up @@ -138,4 +142,6 @@ jobs:
with:
toolchain: 1.97.0
- name: Check Rust 1.97 minimum
run: cargo check --workspace --all-targets --all-features --locked
run: |
cargo check --workspace --all-targets --all-features --locked
cargo check --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --all-targets --locked
1 change: 1 addition & 0 deletions cookbook/support/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[package]
workspace = ".."
name = "cellule-cookbook-support"
version.workspace = true
edition.workspace = true
Expand Down
18 changes: 8 additions & 10 deletions cookbook/support/src/node.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{path::PathBuf, sync::Arc, time::Duration};

use cellule_app::{ApplicationHandle, CellApplication, CompiledApplication};
use cellule_app::{ApplicationBinding, ApplicationHandle, CellApplication, CompiledApplication};
use cellule_host::{CellNode, CellNodeBuilder, CellNodeTaskGroup};
use cellule_ltx::{CellReplica, DiskBudget, Host, Limits};
use cellule_runtime::{
Expand Down Expand Up @@ -297,15 +297,13 @@ impl LocalNode {
&self,
tenant: TenantId,
) -> Result<ApplicationHandle<A>> {
Ok(self.node.application_handle(
CellClient::local_runtime(
self.node.application().registry(),
self.node.runtime(),
self.layout.clone(),
),
tenant,
self.application_id,
)?)
Ok(self.application_binding::<A>()?.scope(tenant))
}

/// Binds a reusable typed application factory to this node's existing resources.
/// Authorize each tenant before creating a scoped capability from the binding.
pub fn application_binding<A: CellApplication>(&self) -> Result<ApplicationBinding<A>> {
Ok(self.node.bind_local_application(self.layout.clone())?)
}

/// Provisions or restores a declared Cell, serializing concurrent local acquisition.
Expand Down
63 changes: 55 additions & 8 deletions crates/cellule-app/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,70 @@
# cellule-app

Declare a stable Cell topology, compile native Rust modules, and expose typed
application handles. The host owns runtime lifecycle and network wiring.
application handles. `cellule-host` manages runtime lifecycle; the embedding
service owns network wiring and authorization.

Each SQL Cell owns its SQLite database, request ledger, and capture/recovery
lineage. Cells can share worker threads and resource budgets within a host,
and run on different nodes under separate fenced ownership. The application
builder declares Cell types, partitions, schemas, and operations; the host
handles placement, activation, routing, and recovery.

```mermaid
flowchart LR
Modules[Native modules] --> Builder[ApplicationBuilder]
Topology[Cell types] --> Builder
Builder --> Descriptor[CompiledApplication]
Descriptor --> Host[CellNode]
Descriptor --> Handle[ApplicationHandle]
```text
Module descriptor + explicit CellBinding
│
▼
ApplicationBuilder::module
│
▼
CompiledApplication
│
▼
ApplicationBinding + existing client
│
authorize tenant → scope(tenant)
│
▼
ApplicationHandle → typed operations
```

Register each module and all its namespace bindings together. The module supplies
role, shards, and schema range; you choose stable Cell names, namespace IDs,
partition modes, and limits. The compiler validates the choices before calling
the module's registration hook, then uses the existing canonical descriptor path.

This helper accepts an already declared single-namespace module:

```rust,no_run
use cellule_app::{ApplicationBuilder, CellBinding};
use cellule_runtime::{CellModule, NamespaceId};

fn register_orders<M: CellModule>(
builder: &mut ApplicationBuilder,
module: M,
namespace: NamespaceId,
) -> cellule_runtime::Result<()> {
builder.module(
module,
[CellBinding::entity(namespace, "orders").with_limits(64 << 20, 16 << 20)],
)
}
```

Use `CellBinding::sharded` for fixed shards, `entity` for hashed entity keys,
or `entity_uuid` for canonical UUID SQL Cells. Their descriptor bytes match the
equivalent explicit `CellType` declarations. Include every namespace in the
module exactly once. Module registration errors abort compilation; typed
registration hooks are not rolled back.

Bind your configured `CellClient`, compiled artifact, and installation ID once
with `ApplicationBinding::<A>::new`. Authorize each tenant before calling
`binding.scope(tenant)`. `cellule-host` provides
`CellNode::bind_local_application` to create this factory from its existing
runtime and a supplied storage layout. The same artifact can feed Axum/OpenAPI;
the [complete service example](../cellule-axum/examples/application-builder-service/README.md)
uses these framework building blocks.

| Guide | Topic |
| --- | --- |
| [API guide](../../docs/api.md) | Compile an application, bind handles, invoke commands, and handle outcomes. |
Expand Down
6 changes: 6 additions & 0 deletions crates/cellule-app/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ modules + Cell types -> compiled descriptor -> catalog + fenced owner
| `cargo run -p cellule-app --example workflow --locked` | Workflow, Activities | Start a durable run, have an `ActivitySupervisor` validate and execute its activity, then read the completed state. |
| `cargo run -p cellule-app --example schedules --locked` | Cron, Effects | Register a fixed-interval schedule, drive one due maintenance tick, deliver its effect through a signed local peer loopback, and count the destination row. |

For a complete HTTP service, run
`cargo run --manifest-path crates/cellule-axum/examples/application-builder-service/Cargo.toml --locked`.
The [service guide](../../cellule-axum/examples/application-builder-service/README.md)
uses native module registration and scoped factories, authorized Axum routes,
OpenAPI, retained command evidence, and ordered host shutdown.

## How each path works

**[basic.rs](../examples/basic.rs)** compiles two modules with distinct
Expand Down
10 changes: 9 additions & 1 deletion crates/cellule-app/docs/invocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ sequenceDiagram

| Operation | Route and evidence |
| --- | --- |
| `ApplicationHandle::new` | Checks application identity and registry digest before calls. |
| `ApplicationBinding::new` | Validates a configured client against the author type and compiled registry once during composition. |
| `ApplicationBinding::scope` | Creates a tenant capability from the bound client after application authorization; starts no runtime and opens no Cell. |
| `ApplicationHandle::new` | Checks author type and registry digest before calls. |
| `cell_client!` | Binds declared namespace, operation IDs, and `CellKey` type. |
| Command | Always uses current owner; durable outcome includes a receipt. |
| Default query | `ReadPolicy::CurrentOwner` preserves owner ordering. |
Expand All @@ -26,3 +28,9 @@ the client does not silently fall back to the owner. The host installs read
replicas and the authenticated peer client. Application code sees only typed
capabilities. Run the [SQL example](../examples/sql.rs) for a command and
visible read-back.

Keep one `ApplicationBinding<A>` in service state and call `scope` after verifying
the caller's tenant permission. Each handle retains the bound installation ID,
compiled artifact, read policy, and Blob artifact store. Scoped invocations still
reject targets from another tenant, installation, or module. The factory itself
grants capabilities and must remain in trusted application code.
26 changes: 19 additions & 7 deletions crates/cellule-app/docs/topology.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
# Topology and descriptors

```mermaid
flowchart TD
Namespace[Stable namespace ID] --> CellType[CellType]
Role[Catalog role] --> CellType
Partition[Fixed shard or entity key] --> CellType
CellType --> Descriptor[Compiled descriptor digest]
Descriptor --> Routing[Typed client routing]
```text
Module descriptor CellBinding
role + shards + schema range stable namespace + name + partition + limits
│ │
└──────────┬─────────────┘
▼
ApplicationBuilder::module
│
▼
canonical CellType descriptor
│
▼
typed client routing
```

| Surface | Contract |
| --- | --- |
| `CellBinding` | Explicit namespace, Cell name, partition mode and optional limits; role, shards and schema range come from the module. |
| `ApplicationBuilder::module` | Registers a module and all namespace bindings together; validates topology before invoking registration hooks. |
| `CellType::new` | Declares module, name, namespace, role, and shard count. |
| Fixed shards | Scope hashes to one declared shard. |
| `with_entity_partitions` | One declared shard; canonical entity key derives a 33-byte partition. |
Expand All @@ -21,6 +29,10 @@ flowchart TD

Changing a stable namespace, role, partition scheme, or descriptor changes
routing and persisted identity. Treat it as a versioned application change.
`CellBinding::sharded`, `entity`, and `entity_uuid` preserve the existing
partition versions and canonical descriptor encoding. The final compiler still
checks the module registry against every topology declaration. Keep the explicit
`register`/`cell_type` path when composing declarations independently.
Fixed shards use partition version 1, hashed entity keys use version 2, and
direct UUID partitions use version 3. These schemes have distinct descriptor
bytes. UUID keys must be 16 bytes with a recognized version (1 through 8)
Expand Down
106 changes: 106 additions & 0 deletions crates/cellule-app/src/binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Validated application composition shared by authorized tenant scopes.

use crate::{
ApplicationHandle, ApplicationId, BlobArtifactStore, CellApplication, CellClient,
CompiledApplication, Error, PhantomData, Result, TenantId,
};
use std::sync::Arc;

/// A validated application/client binding that creates tenant-scoped capabilities.
///
/// Construct once during trusted service startup and share it with authorization
/// code. [`Self::scope`] grants access to the supplied tenant: callers must
/// authenticate and authorize that tenant before calling it. This factory does
/// not open Cells, start a runtime, or perform authorization.
pub struct ApplicationBinding<A> {
client: CellClient,
compiled: Arc<CompiledApplication>,
application: ApplicationId,
marker: PhantomData<fn() -> A>,
}

impl<A> Clone for ApplicationBinding<A> {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
compiled: Arc::clone(&self.compiled),
application: self.application,
marker: PhantomData,
}
}
}

impl<A: CellApplication> ApplicationBinding<A> {
/// Validates the author type and client registry against the compiled artifact.
///
/// The embedding application constructs and configures the client transport.
/// `cellule-host` can bind a local client to its existing runtime and layout.
pub fn new(
client: CellClient,
compiled: Arc<CompiledApplication>,
application: ApplicationId,
) -> Result<Self> {
if compiled.name() != A::NAME {
return Err(Error::Registry(
"application type differs from compiled application",
));
}
if client.registry_digest() != compiled.registry().release_digest() {
return Err(Error::Registry(
"client registry differs from compiled application",
));
}
Ok(Self {
client,
compiled,
application,
marker: PhantomData,
})
}

/// Creates a capability for an application-authorized tenant using the bound client.
///
/// This clones the validated resources without reconstructing a client or
/// rereading registry descriptors. It does not validate a caller's permissions.
#[must_use]
pub fn scope(&self, authorized_tenant: TenantId) -> ApplicationHandle<A> {
ApplicationHandle {
client: self.client.clone(),
compiled: Arc::clone(&self.compiled),
tenant: authorized_tenant,
application: self.application,
marker: PhantomData,
}
}

/// Returns the immutable artifact used by every scoped capability.
#[must_use]
pub fn compiled(&self) -> &CompiledApplication {
&self.compiled
}

/// Returns the stable application installation identity bound to the client.
#[must_use]
pub const fn application_id(&self) -> ApplicationId {
self.application
}

/// Returns a binding with the explicit typed-query policy applied to all scopes.
///
/// Commands and outcome resolution retain owner order. Replica queries
/// require configured readers and never silently fall back to owner reads.
#[must_use]
pub fn with_read_policy(&self, policy: cellule_runtime::client::ReadPolicy) -> Self {
let mut binding = self.clone();
binding.client = binding.client.with_read_policy(policy);
binding
}

/// Returns a binding whose scoped Blob capabilities share the supplied artifact store.
#[must_use]
pub fn with_blob_artifact_store(&self, store: BlobArtifactStore) -> Self {
let mut binding = self.clone();
binding.client = binding.client.with_blob_artifact_store(store);
binding
}
}
Loading
Loading