taba
taba (束, Japanese for “sheaf”) is a next-generation infrastructure primitive. It replaces container + orchestrator (Docker + Kubernetes) with self-describing, capability-aware workload units composed through a distributed solver. The control plane emerges from unit composition — it is not a separate system.
Why
Every step in the infrastructure abstraction trajectory — VMs, containers, Kubernetes — increased control plane complexity monotonically. The diagnosis: the container is too dumb (opaque black box) and the control plane is too smart (unbounded state reconciliation engine). The separation between workload description and orchestration is drawn in the wrong place.
taba draws it differently. Units describe themselves — what they need, what they provide, what they tolerate, who they trust. A deterministic solver composes them. The result is the desired state. There is no separate desired state store.
Core design
Five pillars
- Self-describing typed units — workload, data, policy, governance. Each carries capability declarations, behavioral contracts, and security requirements.
- Emergent control plane — the control plane is the union of deployed units’ operational semantics. One unit = trivial control plane. A thousand = the union of their contracts.
- Security as first class — zero-access default, capability-based, fail-closed on conflicts. Every unit is signed by its author. Taint propagation is structural.
- Data as first-class unit — datasets carry schema, classification, provenance, retention, and consent. Lineage falls out of the composition graph.
- Peer-to-peer — no masters, no leaders, no external metadata store. CRDT graph, erasure-coded, gossip membership.
Load-bearing decisions
| Decision | Rationale |
|---|---|
| No masters | All nodes are peers. Same binary, same protocol, 1 node or 10,000. |
| CRDT graph | No consensus for normal operations. Eventually consistent, partition-tolerant. |
| Fail closed | Security conflicts are never implicitly resolved. |
| Deterministic solver | Same graph + same nodes = same placement on any node. Fixed-point arithmetic (ppm). |
| Signed units | Every unit is signed with context binding (trust domain, cluster, validity window). |
| Erasure coding | Not replication. k-of-n with fleet-adaptive parameters. |
| Gossip (SWIM) | Authenticated messages, 2-witness failure confirmation. |
Quick start
# Install Rust (stable, 1.85+)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Clone and build
git clone https://github.com/witlox/taba.git
cd taba
cargo build --workspace
# Initialize a local node
cargo run --bin taba -- init
# Author a workload unit
cat > hello.taba.toml << 'EOF'
[unit]
name = "hello-web"
image = "nginx:alpine"
EOF
# Apply it to the graph
cargo run --bin taba -- apply hello.taba.toml
# Check status
cargo run --bin taba -- status
# Run the solver
cargo run --bin taba -- compose
# List units
cargo run --bin taba -- unit list
# Audit decision trails
cargo run --bin taba -- audit trails
Architecture
taba-common types, config, protobuf
|
taba-core unit type system, capabilities, contracts
|
taba-security signing, verification, taint, Shamir ceremony
/ \
taba-graph taba-solver CRDT graph + deterministic solver (parallel)
/ \ |
taba-erasure taba-gossip erasure coding + membership (parallel)
\ /
taba-node per-node daemon, WAL, reconciliation
|
taba-cli command-line interface
|
taba-k8s K8s manifest converter
14 crates. Acyclic dependency graph. Single-node works before multi-node (progressive complexity).
Unit model
Everything in taba is a typed, self-describing unit:
# Example: a workload unit declaration
[unit]
name = "api-server"
image = "api:v2.1.0"
[needs]
postgres = { type = "storage", purpose = "primary" }
[provides]
http-api = { type = "network", purpose = "serving" }
[scaling]
min = 2
max = 10
Four unit types:
- Workload — compute process (container, microVM, Wasm, native)
- Data — dataset with classification, provenance, retention, consent
- Policy — resolves a specific capability conflict between units
- Governance — trust domain definitions, role assignments, certifications
Security model
- Zero-access default: units access nothing unless explicitly declared and policy-approved
- Capability-based: typed capabilities with optional purpose qualifiers
- Fail closed: ambiguous security decisions are denied, not guessed
- Taint propagation: PII in = PII out, unless multi-party policy declassifies
- Signed everything: units, gossip messages, ceremony events
- Scoped authority: authors are parameterized by (unit type scope × trust domain scope)
K8s migration
# Convert K8s manifests to taba unit declarations
cargo run --bin taba-k8s -- convert deployment.yaml --output-dir ./units
# Apply the generated units
cargo run --bin taba -- apply units/api-server.taba.toml
Supported K8s resources: Deployment, StatefulSet, DaemonSet, Pod, Service, ConfigMap, Secret, NetworkPolicy, Role, ClusterRole, RoleBinding, ClusterRoleBinding.
Testing
| Tier | What | When | Command |
|---|---|---|---|
| 1 (fast) | Unit tests + BDD @smoke + integration | Between every edit | just or just test |
| 2 (slow) | Tier 1 + slow-marked tests + full BDD | Pre-PR | just test-slow |
| 3 (full) | Tier 2 + slow integration tests | Pre-merge / nightly | just test-full |
960 tests across 13 crates + taba-e2e + taba-acceptance. Property tests (proptest, 10k+ cases) for CRDT merge laws, solver determinism, Shamir secret sharing, and capability matching.
Ecosystem
taba is the fourth project in the witlox infrastructure ecosystem:
| Project | Language | Purpose |
|---|---|---|
| pact | Rust | HPC configuration management |
| lattice | Rust | HPC workload scheduling |
| sovra | Go | Federated key management |
| taba | Rust | Next-gen infrastructure composition |
Integration between projects is opt-in via hpc-core crates. Each project owns its space.
Technology
- Language: Rust (workspace), edition 2024
- Config: TOML (human), protobuf (wire) via prost + tonic
- Async: tokio multi-threaded
- CLI: clap
- Errors: thiserror, typed enums
- Testing: proptest (properties), cucumber-rs (BDD), criterion (benchmarks)
- License: Apache-2.0
License
Apache-2.0. See LICENSE.
Getting Started
Prerequisites
- Rust stable (1.85+). Install via rustup.
- just (optional, command runner):
cargo install just --locked - cargo-nextest (optional, faster tests):
cargo install cargo-nextest --locked - cargo-deny (optional, dependency auditing):
cargo install cargo-deny --locked - mdbook (optional, local docs):
cargo install mdbook --locked
Installation
From source
git clone https://github.com/witlox/taba.git
cd taba
cargo build --workspace --release
# Binaries are in target/release/
# - taba (CLI)
# - taba-k8s (K8s converter)
Verify the build
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
cargo deny check
Your first unit
1. Initialize a local node
taba init
This generates:
- An Ed25519 keypair (private key stored with
0600permissions) - A trust domain
- A root governance unit
- A role assignment (you are the admin)
State is stored in ~/.taba/ by default. Use --state-dir to override.
2. Author a workload unit
Create hello.taba.toml:
[unit]
name = "hello-web"
image = "nginx:alpine"
This is the simplest possible unit — Level 0 in the progressive disclosure schema. Everything else defaults: kind = service, scaling min=1 max=1, health = OS-level process monitoring, placement on failure = from environment.
3. Apply it to the graph
taba apply hello.taba.toml
The unit is:
- Parsed from TOML
- Identity filled in (author, trust domain, cluster ID from your keypair)
- Signed with Ed25519 (INV-S3)
- Structurally validated (
DefaultValidator) - Scope-checked (INV-S8: no duplicate author scopes)
- Signature verified by the graph’s verifier
- Inserted into the composition graph
- Persisted to
~/.taba/graph.json
4. Check status
taba status
Shows graph stats (active units, pending, archived, memory), operational mode, node ID, trust domain, and cluster ID.
5. Run the solver
taba compose
The solver takes a snapshot of the graph, runs deterministic placement (fixed-point Ppm arithmetic, no floating-point), and shows the results: placements, unplaceable units, and detected conflicts.
6. List units
taba unit list
taba unit inspect <unit-id>
7. Archive a unit
taba unit archive <unit-id>
Archived units are removed from the active set but retained for provenance integrity. Governance units cannot be archived (INV-G3).
8. Audit
taba audit trails # decision trails
taba audit provenance <id> # provenance chain for a data unit
Next steps
- Unit Authoring — full TOML schema, all 6 levels
- Composition & Placement — how the solver works
- K8s Migration — convert existing K8s manifests
- Configuration Reference — all config options
Unit Authoring
Units are declared in TOML files (.taba.toml). The schema uses
progressive disclosure: simple cases are 3 lines, complexity is
available but never mandatory. One file = one unit.
Level 0 — Minimal workload (3 lines)
[unit]
name = "hello-web"
image = "nginx:alpine"
Everything defaults:
type = "workload"(inferred fromimage)kind = "service"(default)- Trust domain: current (from
taba init) - Author: current (from keypair)
- Capabilities: none needed, none provided
- Health: OS-level process monitoring (INV-O3)
- Placement on failure: from environment (INV-N5: dev=leave-dead, prod=auto-replace)
- Scaling: min=1, max=1
Level 1 — Capabilities
[unit]
name = "api-server"
image = "api:v2.1.0"
digest = "sha256:abc123..."
[needs]
postgres = { type = "storage", purpose = "primary" }
redis = { type = "storage", purpose = "cache" }
[provides]
http-api = { type = "network", purpose = "serving" }
[scaling]
min = 2
max = 10
[[scaling.triggers]]
metric = "cpu_ppm"
threshold = 700_000 # 70%
direction = "up"
Capability shorthand
# Full form:
postgres = { type = "storage", purpose = "primary" }
# Minimal form (type only):
postgres = { type = "storage" }
Level 2 — Behavioral contracts
[unit]
name = "payment-processor"
image = "payments:v3.0.0"
kind = "service"
[tolerates]
max_latency = "50ms"
consistency = "strong"
[failure]
on_oom = "backoff-inputs"
on_crash = { restart_with_backoff = 3 }
on_shutdown = { drain = "30s" }
[recovery]
strategy = "replay-from-offset"
stream = "payments-events"
[health]
type = "http"
path = "/healthz"
port = 8080
interval = "10s"
timeout = "2s"
Failure semantics
| Field | Values | Default |
|---|---|---|
on_oom | "backoff-inputs", "restart", "fail-permanent" | "restart" |
on_crash | { restart_with_backoff = N }, "unexpected" | { restart_with_backoff = 3 } |
on_shutdown | { drain = "duration" }, "immediate" | { drain = "30s" } |
Health check types
| Type | Fields | Default |
|---|---|---|
"http" | path, port, interval, timeout | — |
"tcp" | port, interval, timeout | — |
"command" | command, interval, timeout | — |
| (omitted) | — | OS-level process monitoring |
Recovery strategies
| Strategy | Fields | Default |
|---|---|---|
"stateless" | — | (default) |
"replay-from-offset" | stream, offset (optional) | — |
"require-quorum" | min_peers | — |
Level 3 — Data unit
[unit]
name = "customer-profiles"
type = "data"
[schema]
format = "json-schema"
definition = "schemas/customer.json"
[classification]
level = "pii"
[retention]
mode = "persistent"
duration = "7y"
legal_basis = "GDPR Art. 6(1)(b)"
mandatory = true
[storage]
encrypted_at_rest = true
jurisdictions = ["EU", "CH"]
[provides]
customer-data = { type = "dataset", purpose = "analytics" }
Classification levels (INV-S4, INV-S7)
Lattice: "public" < "internal" < "confidential" < "pii"
Retention modes
| Mode | Behavior | Default |
|---|---|---|
"persistent" | Governed by duration + legal_basis. Tombstoned on expiry. | (default) |
"ephemeral" | Auto-removed when producing bounded task terminates. | — |
"local-only" | Never enters graph. Requires policy for classification > public. | — |
Hierarchical data
[unit]
name = "eu-customers"
type = "data"
parent = "customer-profiles"
[classification]
level = "pii"
[storage]
jurisdictions = ["EU"]
# Inherits encrypted_at_rest from parent
# Narrows jurisdiction (always allowed per INV-S7)
Level 4 — Policy unit
[unit]
name = "allow-analytics-access"
type = "policy"
[conflict]
units = ["customer-profiles", "analytics-pipeline"]
capability = "customer-data"
[resolution]
action = "allow"
rationale = "Analytics pipeline has DPA and processes within EU jurisdiction"
Resolution actions
| Action | Behavior |
|---|---|
"allow" | Allow the composition unconditionally |
"deny" | Deny the composition |
"conditional" | Allow under conditions: conditions = ["condition1", "condition2"] |
Policy supersession
[unit]
name = "updated-analytics-policy"
type = "policy"
supersedes = "allow-analytics-access"
[conflict]
units = ["customer-profiles", "analytics-pipeline"]
capability = "customer-data"
[resolution]
action = "conditional"
conditions = ["processing within EU only", "audit trail enabled"]
rationale = "Updated per legal review 2026-03"
Level 5 — Bounded task with spawning
[unit]
name = "nightly-etl"
kind = "bounded-task"
image = "etl:v1.2.0"
[deadline]
wall_time = "2h"
[spawn]
max_depth = 2
max_count = 10
Bounded task fields
| Field | Type | Default |
|---|---|---|
kind | "bounded-task" | "service" |
deadline.wall_time | duration string | (required for bounded-task) |
deadline.lc_range | integer (logical clock delta) | (optional) |
spawn.max_depth | integer (1-4) | governed by INV-W3 default (4) |
spawn.max_count | integer | (required if spawning) |
Governance units
[unit]
name = "production-domain"
type = "governance"
governance_type = "trust-domain"
[trust_domain]
description = "Production workloads for ACME Corp"
Promotion gate (INV-E3)
[unit]
name = "prod-gate"
type = "governance"
governance_type = "promotion-gate"
[[transitions]]
from = "dev"
to = "test"
mode = "auto"
[[transitions]]
from = "test"
to = "prod"
mode = "human-approval"
Cross-domain capability advertisement (INV-X5)
[unit]
name = "shared-data-api"
type = "governance"
governance_type = "cross-domain-capability"
[cross_domain]
provides = { type = "dataset", name = "shared-metrics", purpose = "monitoring" }
conditions = "Bilateral policy required. Read-only access."
Artifact type shorthand
| Key | Artifact type | Example |
|---|---|---|
image | OCI container | image = "myapp:v1.0" |
binary | Native binary | binary = "bin/myapp" |
wasm | WebAssembly module | wasm = "module.wasm" |
k8s | Kubernetes manifest | k8s = "pod-spec.yaml" |
microvm | MicroVM kernel | microvm = "vmlinux-5.10" |
MicroVM workloads also require kernel and rootfs paths (INV-N6).
See Runtime Matching for details.
Only one artifact key per unit. digest (SHA256) is always recommended
and required for production environments (INV-A1).
Defaults summary
| Field | Default | Source |
|---|---|---|
type | "workload" | inferred from artifact key |
kind | "service" | — |
trust_domain | current | from taba init |
author | current | from keypair |
scaling.min | 1 | — |
scaling.max | 1 | — |
health | OS-level process monitoring | INV-O3 |
placement_on_failure | env-derived (dev=leave-dead, else=auto-replace) | INV-N5 |
failure.on_oom | "restart" | — |
failure.on_crash | { restart_with_backoff = 3 } | — |
failure.on_shutdown | { drain = "30s" } | — |
recovery.strategy | "stateless" | — |
retention.mode | "persistent" | — |
classification.level | "internal" | — |
File conventions
- Extension:
.taba.toml(e.g.,api-server.taba.toml) - One file per unit
- Multi-unit deployments: directory of
.taba.tomlfiles - Policy files can reference units by name (scoped to trust domain)
- Names must match
[a-z0-9][a-z0-9-]*(lowercase, hyphens, no dots)
Composition & Placement
The solver is the only “active” component in taba. It is a deterministic pure function: given an immutable graph snapshot and a membership snapshot, any node produces identical placement decisions (INV-C3).
How composition works
-
Capability matching — For each unit with
needs, the solver searches the graph for units with matchingprovides. Matching is typed:(cap_type, name, purpose?). Purpose is optional — when declared, it must match exactly. When omitted, any provide of the same type+name is acceptable (INV-K2). -
Conflict detection — The solver scans all capability need/provide pairs. A conflict exists when:
- A need has no matching provide (missing capability)
- A need matches a provide but purpose qualifiers conflict (INV-K2)
- A need matches but security requirements are incompatible (INV-S2) and no policy resolves the incompatibility
-
Cycle detection — Recovery relationships declare dependency ordering on failure. Cycles in these relationships are unresolvable without explicit policy (INV-K5). The solver detects strongly connected components of size > 1.
-
Placement scoring — For each placeable unit, candidate nodes are scored using fixed-point Ppm arithmetic (10^6 scale, u64). No floating-point anywhere (INV-C3, DL-004). Higher scores are better. Ties are broken by lexicographically lowest NodeId (INV-C3).
-
Scaling — The solver evaluates declared triggers (e.g.,
cpu_ppm > 700_000). It does not invent scaling logic — it only evaluates what the unit declared (INV-K4).
Determinism guarantees
- Same input = same output on any node (INV-C3)
- All arithmetic uses
Ppm(u64)— fixed-point at 10^6 scale - Division rounds toward zero (Rust integer division default)
#[deny(clippy::float_arithmetic)]enforced at compile time- Property tests verify determinism with 10,000+ cases
Composition is order-independent
Units can be inserted in any order. The solver produces the same result regardless (INV-C6). This is load-bearing for partition tolerance — different nodes may receive units in different orders via gossip, but all converge to the same placement.
Performance
| Operation | N=10 | N=100 | N=500 | N=1000 (extrapolated) |
|---|---|---|---|---|
| solve | 2.6 µs | 46 µs | 770 µs | ~3 ms |
| detect_conflicts | 490 ns | 23 µs | 590 µs | ~2.4 ms |
| detect_cycles | 1.6 µs | 25 µs | 189 µs | ~380 µs |
| rank_nodes | 158 ns | 3.6 µs | 64 µs | ~128 µs |
The solver’s O(N²) conflict detection is the primary scaling bottleneck. At N=500, solve takes ~770 µs (acceptable for real-time placement). At N=10,000, it would take ~300 ms (unacceptable). Sharding by trust domain is the planned solution for N > 10,000.
Using the solver
# Run the solver on the current graph
taba compose
# Output shows placements, unplaceable units, and conflicts
Solver result structure
Placements:
unit: 550e8400-e29b-41d4-a716-446655440000
node: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
score: 850000 (85.0%)
Unplaceable:
unit: 550e8400-e29b-41d4-a716-446655440001
reason: NoCapableNode { unmet: [storage:postgres] }
Conflicts:
conflict: [unit-a, unit-b] capability: network
status: Unresolved
has_policy: false
Runtime Matching
taba supports four workload runtime types. The solver matches each
workload’s ArtifactType to the node’s RuntimeCapability (hard
constraint), then ranks by resource availability (soft constraint).
Supported runtimes
| Runtime | ArtifactType | TOML key | Example |
|---|---|---|---|
| Container | Oci | image | image = "myapp:v1.0" |
| Native binary | Native | binary | binary = "/bin/daemon" |
| WebAssembly | Wasm | wasm | wasm = "module.wasm" |
| MicroVM | MicroVm | microvm | microvm = "vmlinux-5.10" |
| K8s manifest | K8sManifest | k8s | k8s = "pod-spec.yaml" |
Container (OCI)
Docker or Podman with root daemon. The node must advertise
RuntimeCapability::Oci or RuntimeCapability::OciRootless.
[unit]
name = "web-api"
image = "registry.example.com/web-api:v1.0"
digest = "sha256:abc123..."
Native binary
Standalone executables (MSI, RPM, DEB, scripts). The node must
advertise RuntimeCapability::Native. NativeRuntime spawns the
binary via std::process::Command and tracks the PID.
[unit]
name = "batch-job"
binary = "/usr/local/bin/batch-processor"
WebAssembly
Wasm modules (wasmtime/wasmer compatible). The node must advertise
RuntimeCapability::Wasm. WasmRuntime tracks lifecycle state;
full wasmtime execution is deferred.
[unit]
name = "edge-function"
wasm = "/opt/edge-fn.wasm"
MicroVM
Firecracker, cloud-hypervisor, or QEMU. The node must advertise
RuntimeCapability::MicroVm (requires a VM monitor binary).
MicroVmRuntime auto-detects the monitor and generates the
appropriate VM configuration (INV-N6).
[unit]
name = "secure-worker"
microvm = "vmlinux-5.10"
kernel = "/opt/vmlinux"
rootfs = "/opt/rootfs.ext4"
digest = "sha256:abc123..."
How the solver matches
-
Capability filter (hard constraint):
DefaultCapabilityFilterchecks if the node’sRuntimeCapabilitylist matches the workload’sArtifactType. Nodes without a matching capability are excluded. -
Placement scorer (soft constraint):
DefaultPlacementScorerranks eligible nodes by resource availability, environment match, author affinity, and health status. Exact runtime matches get a bonus. -
RuntimeSelector: At reconcile time,
RuntimeSelectordispatches to the correctRuntimeExecutor(DockerRuntime, NativeRuntime, WasmRuntime, or MicroVmRuntime) based on the unit’sArtifactType.
Node capability advertisement
Nodes advertise their capabilities via the NodeCapabilitySet:
| Capability | Description | Detection |
|---|---|---|
Oci | Docker/Podman with root daemon | Docker::connect_with_defaults() |
OciRootless | Rootless Docker/Podman | Socket probe |
K8s | Kubernetes API access | kubectl available |
Wasm | WebAssembly runtime | Always available |
Native | Native binary execution | Always available |
MicroVm | VM execution (Firecracker/QEMU) | which firecracker, which cloud-hypervisor, which qemu-system-* |
Status & Audit
Status
taba status
Shows:
- Graph stats: active units, pending units, archived units, memory usage, memory limit
- Operational mode: Normal, Degraded (with reason), or Recovery
- Node info: node ID, trust domain, cluster ID
Output formats
taba status --format table # Human-readable (default)
taba status --format json # JSON for scripting
Unit inspection
taba unit list # All active units
taba unit inspect <id> # Details of a specific unit
taba unit validate <file> # Validate a TOML file without inserting
taba unit archive <id> # Archive a unit (soft-delete)
Audit
Decision trails
Every solver run produces a decision trail (INV-O1): the inputs (graph snapshot, membership), outputs (placements, conflicts), and solver version. Trails are queryable.
taba audit trails # List all decision trails
taba audit trails --format json
Provenance
Data units carry provenance metadata (INV-D1): which workload produced them, from what input data. The provenance chain is structural — it emerges from the composition graph, not a separate logging system.
taba audit provenance <data-unit-id>
Returns the full provenance path: all producing workloads and their input data units, recursively, back to source data units with no provenance (root data).
Taint propagation
Taint is computed at query time (INV-S4), not cached at merge time. This makes taint eventually consistent across nodes.
- Single input: output inherits the input’s classification
- Multiple inputs: output inherits the union (most restrictive) of all input classifications
- Declassification: requires multi-party signing (INV-S9) — one policy-scoped signer + one data-steward-scoped signer
Classification lattice: Public < Internal < Confidential < PII
K8s Migration
taba includes a K8s manifest converter (taba-k8s) that reads
existing Kubernetes YAML and generates taba unit declarations.
Quick start
# Convert a single deployment
taba-k8s convert deployment.yaml --output-dir ./units
# Apply the generated units
taba apply units/web-server.taba.toml
Supported resources
| K8s Resource | taba Unit | Mapping |
|---|---|---|
| Deployment | WorkloadUnit (service) | Container image → artifact, replicas → scaling |
| StatefulSet | WorkloadUnit (service) | + state recovery (require-quorum), PVC references |
| DaemonSet | WorkloadUnit (service) | + one-per-node constraint, node selector |
| Pod | WorkloadUnit (service) | Direct pod (no controller, no auto-scaling) |
| Service | WorkloadUnit (provides) | Network capability from ports |
| ConfigMap | DataUnit | Internal classification, text/plain schema |
| Secret | DataUnit | Confidential classification, encrypted_at_rest |
| NetworkPolicy | PolicyUnit | Pod selector → conflict units, ingress/egress → resolution |
| Role | GovernanceUnit (RoleAssignment) | Rules → unit_type_scope, namespace → trust_domain_scope |
| ClusterRole | GovernanceUnit (RoleAssignment) | Cluster-wide scope |
| RoleBinding | GovernanceUnit (RoleAssignment) | Binds Role to subjects |
| ClusterRoleBinding | GovernanceUnit (RoleAssignment) | Binds ClusterRole to subjects, cluster-wide |
Unmappable resources
The converter surfaces resources it cannot map, with a reason and suggestion:
| Resource | Reason | Suggestion |
|---|---|---|
| Ingress | taba has no ingress concept | Map the service backend directly |
| PersistentVolume | No direct taba equivalent | Use a data unit with storage_requirements |
| PersistentVolumeClaim | No direct taba equivalent | Use a data unit with storage_requirements |
| CRDs | Unbounded, no generic mapping | Write a custom taba unit declaration |
| Job | Bounded task (manual) | Set kind = “bounded-task”, add [deadline] |
| CronJob | Bounded task (manual) | Set kind = “bounded-task”, add [deadline] |
| HPA | Scaling triggers (manual) | Add triggers to [scaling] section |
| Namespace | No taba equivalent needed | — |
| ServiceAccount | No taba equivalent needed | — |
Multi-document YAML
The converter supports --- separated multi-document YAML:
taba-k8s convert multi-resource.yaml --output-dir ./units
Each resource is converted independently. The conversion report shows what was generated, skipped, and unmappable.
Conversion report
taba-k8s convert deployment.yaml
=== K8s → taba Conversion Report ===
Generated: 1 units
✓ web-server
Unmappable: 1 resources
✗ Ingress/web-ingress: taba has no ingress concept.
→ Map the service backend directly.
Total processed: 2
Provenance placeholders
taba-k8s convert deployment.yaml --provenance
Adds SLSA provenance placeholders to generated units, for filling in build system details (builder, source repo, digest, SLSA level).
Deployment
Single-node (dev/test)
# Initialize
taba init
# Apply units
taba apply workload.taba.toml
# Compose
taba compose
# Status
taba status
State is persisted to ~/.taba/ (or --state-dir):
keypair— Ed25519 private key (0600 permissions)config.json— trust domain, cluster ID, node IDgraph.json— active units (JSON array)
Restart
On restart, taba loads the keypair, config, and graph from the
state directory. The graph is reconstructed in memory from the
persisted JSON.
Multi-node (M4+)
Multi-node operation requires taba-gossip (SWIM membership) and taba-erasure (Reed-Solomon coding). These are implemented as library crates but not yet wired into a multi-node daemon.
The planned multi-node flow:
- Each node runs
taba initindependently - Nodes discover each other via seed nodes (gossip)
- Units are replicated via CRDT merge (commutative, associative, idempotent)
- Graph shards are erasure-coded across N nodes (k-of-n)
- Solver runs locally on each node (same input = same output)
Docker
The taba CLI and node daemon can run in Docker:
FROM rust:1.85-slim AS builder
WORKDIR /taba
COPY . .
RUN cargo build --workspace --release
FROM debian:bookworm-slim
COPY --from=builder /taba/target/release/taba /usr/local/bin/
COPY --from=builder /taba/target/release/taba-k8s /usr/local/bin/
ENTRYPOINT ["taba"]
Configuration
See Configuration Reference for all options.
Configuration Reference
Cluster configuration (ClusterConfig)
Stored in TOML, loaded on node startup, propagated via gossip.
| Field | Type | Default | Description |
|---|---|---|---|
cluster_id | UUID | (generated) | Unique identifier for this cluster |
resilience_pct | u8 | 33 | Erasure coding resilience percentage. k = ceil(N * (1 - R/100)) |
graph_memory_limit_bytes | u64 | 1,073,741,824 | Maximum memory for active graph per node (INV-R6) |
shamir_total_shares | u8 | 5 | Total shares in Shamir ceremony |
shamir_threshold | u8 | 3 | Minimum shares for reconstruction |
max_data_hierarchy_depth | u8 | 16 | Maximum depth for hierarchical data units |
gossip_interval | Duration | 500ms | SWIM probe interval |
gossip_suspicion_timeout | Duration | 5s | Base suspicion timeout |
witness_count | u8 | 2 | Independent witnesses for failure (INV-R3) |
indirect_probe_count | u8 | 3 | Peers asked for indirect probes |
retransmit_multiplier | u8 | 4 | Piggyback rounds = mult × log₂(N) |
max_piggyback_entries | u8 | 8 | Max membership changes per message |
suspicion_multiplier | u8 | 4 | Effective timeout = mult × ceil(log₂(N)) × interval |
max_spawn_depth | u8 | 4 | Maximum spawn depth for bounded tasks (INV-W3) |
revocation_grace_window | Option | None | Grace window in logical clock delta (INV-S3) |
fleet_command_rate_limit_lc | u64 | 1000 | Min LC delta between fleet commands of same type |
Node configuration (NodeConfig)
| Field | Type | Default | Description |
|---|---|---|---|
wal_dir | String | “./wal” | Directory for WAL segment files |
listen_addr | String | “0.0.0.0:7946” | Bind address for peer communication |
seed_nodes | Vec | [] | Seed nodes for gossip bootstrap |
require_tpm | bool | false | Whether TPM attestation is required (A5) |
environment | Option | None | Environment tag (e.g., “env:dev”) |
custom_tags | Vec<(String, String)> | [] | Additional custom tags |
archive_backend | Option | None | Archive storage configuration |
Archive backend
[archive_backend]
# Local filesystem
backend = "local"
path = "/var/lib/taba/archive"
# Or S3-compatible
backend = "s3"
endpoint = "https://s3.amazonaws.com"
bucket = "taba-archive"
region = "us-east-1"
Gossip auto-tuning
Effective suspicion timeout scales with cluster size:
effective = max(base_timeout, suspicion_mult × ceil(log2(N)) × interval)
At 10,000 nodes: ~1 KB/s per node, ~10 MB/s cluster-wide. Acceptable.
Key revocation messages use 2 × retransmit_multiplier × log₂(N)
rounds (double normal) for rapid convergence.
Operational Modes
taba nodes operate in one of three modes:
Normal
All operations permitted: authoring, composition, placement, archive, compaction. This is the default state.
Degraded
Entered when a trigger condition is met:
- Erasure threshold exceeded (INV-R4): too many node failures
- Memory limit exceeded (INV-R6): graph exceeds configured limit
- WAL failure (FM-07): disk full, corruption, I/O error
- Operator-triggered: manual
taba node degraded
In Degraded mode:
- ❌ Authoring frozen (no new units)
- ❌ Composition frozen (no new placements)
- ❌ Placement frozen
- ✅ Drain and evacuation only
- ✅ Gossip continues (membership updates, health)
Transitions to Normal: operator override only (not automatic).
Recovery
Entered from Degraded when the trigger condition is resolved:
- Erasure re-coding completes
- Compaction frees enough memory
- WAL is repaired or rotated
- Operator clears the degraded state
In Recovery mode:
- ✅ Placement throttled (re-building capacity)
- ✅ Re-coding in progress
- ❌ Full-speed operations not yet resumed
Transitions to Normal: automatic when recovery completes.
State machine
Normal → Degraded (any trigger)
Degraded → Recovery (trigger resolved)
Recovery → Normal (recovery complete)
Degraded → Normal (operator override only)
Invalid transitions return NodeError::InvalidModeTransition.
Monitoring
taba status # Shows current operational mode
The health reporter (HealthReporter) tracks:
- Units running vs failed
- WAL size in bytes
- Graph memory vs limit
- Memory pressure percentage (0-100)
Compaction triggers at 80% of memory limit (INV-R6). Degraded mode triggers at 100%.
WAL & Persistence
The Write-Ahead Log (WAL) is the foundation of taba’s durability guarantee (INV-C4): every mutation is appended to the WAL before its effects become visible to queries.
The graph crate uses an in-memory WAL. The taba-node crate
provides a disk-backed DiskWalManager with CRC32C framing,
segment rotation, and crash-safe compaction (write-then-delete).
The CLI persists state to graph.json on every mutation.
Frame format (DL-014)
┌──────────┬──────────┬───────────────────────┬──────────┐
│ len: u32 │ crc: u32 │ WalEntry (protobuf) │ pad 0-7 │
└──────────┴──────────┴───────────────────────┴──────────┘
len: payload length in bytes (little-endian u32)crc: CRC32C of the payload (corruption detection, FM-07)- Payload: prost-encoded
WalEntry - Padding: zero bytes to 8-byte alignment
Segment naming
wal-{sequence_start:016}.log
Lexicographic sort = temporal order. Default segment size: 64 MB. New segment on threshold or explicit rotation.
Entry types (DL-008)
| Type | When | Discardable when |
|---|---|---|
Merged | Unit verified and merged into graph | Erasure-coded to cluster (durable beyond this node) |
Pending | Unit verified but references not satisfied | Promoted (refs arrived) or expired (1-hour timeout) |
Promoted | Previously pending unit activated | Immediately after corresponding Merged entry |
Compaction
Compaction = create new segment with non-discardable entries, fsync, then delete old segments.
Crash safety: new segment is written and fsynced before old segments are deleted. If the process crashes during compaction, old segments are still intact and can be replayed on recovery.
Compaction eligibility (deterministic, all nodes agree — INV-G1):
- Ephemeral data (highest priority)
- Decision trails
- Terminated bounded tasks
- Superseded policies
- Terminated services
- Expired persistent data
Governance units are exempt from compaction (INV-G3). They are actively replicated (full copies on N nodes), not just erasure-coded.
Auto-compaction (INV-R6)
| Trigger | Threshold | Action |
|---|---|---|
| Memory pressure | 80% of limit | Auto-compaction begins |
| Memory limit | 100% of limit | Node enters Degraded mode |
| Operator | Manual | Compaction on demand |
WAL interface
#![allow(unused)]
fn main() {
pub trait WalManager {
async fn append(&self, entry: WalEntry) -> Result<WalPosition, NodeError>;
async fn replay(&self, from: WalPosition, callback: &mut dyn FnMut(WalEntry) -> Result<(), NodeError>) -> Result<WalPosition, NodeError>;
async fn compact(&self, before: WalPosition) -> Result<u64, NodeError>;
fn size_bytes(&self) -> u64;
fn latest_position(&self) -> WalPosition;
}
}
The DiskWalManager is the production implementation. The
InMemoryWal (in taba-test-harness) is used for tests.
Decision trails (INV-O1)
Decision trails use the same frame format but a separate file
sequence (trail-{sequence_start:016}.log), managed by taba-observe
via taba-node. This keeps the graph WAL focused on graph mutations.
Health & Reconciliation
Health Reporter
The HealthReporter trait tracks per-node health:
| Metric | Description |
|---|---|
mode | Current OperationalMode (Normal, Degraded, Recovery) |
units_running | Count of running workload units |
units_failed | Count of failed workload units |
wal_size_bytes | Current WAL size in bytes |
graph_memory_bytes | Current graph memory usage |
graph_memory_limit_bytes | Configured memory limit |
memory_pressure_pct | Memory usage as percentage of limit (0-100) |
Triggers
| Trigger | Threshold | Action |
|---|---|---|
| Memory pressure | ≥ 80% | Auto-compaction begins (INV-R6) |
| Memory limit | ≥ 100% | Degraded mode (reason: MemoryLimitExceeded) |
| WAL failure | I/O error | Degraded mode (reason: WalFailure) |
Reconciliation
The Reconciler trait compares desired state (from solver
placements) with actual state (what’s running on this node).
Reconciliation actions
| Desired | Actual | Action |
|---|---|---|
| Running | Stopped | Start the unit |
| Running | Failed | Restart (respecting failure semantics) |
| Running | Unknown | Start the unit |
| Stopped | Running | Drain then stop |
| Draining | (any) | Continue draining |
| — | — | No drift |
Reconciliation is NOT permitted in Degraded mode
Returns NodeError::DegradedModeRestriction. Only drain and
evacuation are permitted in Degraded mode.
Evacuation
#![allow(unused)]
fn main() {
pub trait Reconciler {
async fn reconcile(&self, placements: &[LocalPlacement]) -> Result<Vec<Drift>, NodeError>;
async fn detect_drift(&self, placements: &[LocalPlacement]) -> Vec<Drift>;
async fn drain(&self, unit: &UnitId) -> Result<(), NodeError>;
async fn evacuate(&self) -> Result<(), NodeError>;
}
}
evacuate() drains all running units. Used before
gossip::MembershipProtocol::leave to give the system time
to re-place units elsewhere.
Runtime executors
taba supports five runtime backends, dispatched by
RuntimeSelector based on the unit’s ArtifactType:
| Executor | ArtifactType | Use case | Requires |
|---|---|---|---|
SimulatedRuntime | — | Tests, dev | Nothing (in-memory state machine) |
DockerRuntime | Oci | Containers | Docker daemon (via bollard crate) |
NativeRuntime | Native | Standalone binaries | Nothing (std::process) |
WasmRuntime | Wasm | WebAssembly modules | Nothing (state tracking) |
MicroVmRuntime | MicroVm | MicroVMs | Firecracker, cloud-hypervisor, or QEMU |
The SimulatedRuntime transitions units through
Pending → Starting → Running → Draining → Stopped (or Failed)
in memory, with no real process spawning. Fast tests, no Docker
dependency.
The DockerRuntime pulls images, creates containers, starts/stops
them, and inspects container state. Tests using Docker are marked
#[ignore = "slow: requires Docker"].
The NativeRuntime spawns binaries via std::process::Command and
monitors them with kill(pid, 0). No external dependencies.
The WasmRuntime tracks lifecycle state in memory. Full wasmtime
execution is deferred — sufficient for testing placement and
lifecycle without requiring a real WASM module.
The MicroVmRuntime auto-detects the VM monitor (Firecracker,
cloud-hypervisor, or QEMU) at runtime. It generates the appropriate
VM configuration (JSON for Firecracker, CLI args for cloud-hypervisor
or QEMU) from the unit’s kernel_ref and rootfs_ref (INV-N6).
The RuntimeSelector is created once at node startup. It detects
all available runtimes and dispatches start(), stop(),
check_state(), and drain() to the correct executor based on
the unit’s ArtifactType.
Key Management
taba uses Ed25519 for all cryptographic operations. Key material
is zeroized on drop via the zeroize crate.
Key lifecycle
Generation
taba init # Generates Ed25519 keypair
The private key is stored as hex in ~/.taba/keypair with
0600 (owner-only) permissions on Unix. On non-Unix systems,
std::fs::write is used as a fallback (no permission control).
Key ID
KeyId = SHA-256 of the public key, truncated to 128 bits,
encoded as a UUID. This provides a stable, deterministic
identifier without exposing the full public key.
Revocation
Keys are revoked via a governance unit (KeyRevocationDef).
Revocation follows a causal model (INV-S3):
- Units signed before the revocation timestamp remain valid
- Units signed after are rejected
- No retroactive rejection — the graph doesn’t undo history
The revocation is propagated via priority gossip (double the normal retransmit rounds) for rapid convergence.
Revocation reasons
| Reason | Description |
|---|---|
Compromised | Key material was compromised |
Departed | Author has left the organization |
Rotated | Key rotation — replaced by a new public key |
Administrative | Administrative revocation with details |
Shamir secret sharing
The root key (root of all authority) is split into n shares
using Shamir secret sharing over GF(2^8):
- Tier 0 (solo): Single key, no Shamir.
taba initin one command. - Tier 1 (basic):
start → add shares → complete with witness - Tier 2 (password-protected): Each share encrypted with Argon2id
- Tier 3 (offline two-factor): Seed code + password
The ceremony produces the root key whose public half signs the first governance unit, seeding the composition graph. The reconstructed key is immediately zeroized — only the public key persists.
Node enrollment
New nodes join via a multi-party enrollment ceremony:
- An authorized author initiates enrollment for a new node
- A new Ed25519 key pair is generated, private key split into
nshares - Existing nodes contribute shares to the new node
- Once
kshares are collected, the new node reconstructs its key - The key is zeroized; only the public key persists
- A membership governance unit is created
Attestation can be required before enrollment:
- Software attestation (dev/test): SHA-256 of binary + OS info
- TPM attestation (production, feature-gated): TPM 2.0 quote
Shamir Ceremony
The Shamir key ceremony is the pre-graph bootstrap: it produces the root key whose public half signs the first governance unit, seeding the composition graph.
Tiers
| Tier | Name | Flow | Use case |
|---|---|---|---|
| 0 | Solo | taba init (one command) | Dev, single-operator |
| 1 | Basic | start → add shares → complete with witness | Small teams |
| 2 | Password-protected | Each share encrypted with Argon2id | Production |
| 3 | Offline two-factor | Seed code + password | High-security production |
Tier 1 ceremony protocol
1. Start
#![allow(unused)]
fn main() {
ceremony.start(total_shares: 5, threshold: 3)
}
- Validates: threshold ≥ 2, threshold ≤ total_shares, total ≤ 255
- Generates a new Ed25519
KeyPair - Splits the private key using
shamir::split_secretover GF(2^8) - Returns a
CeremonyIdand the shares (for distribution)
2. Add shares
#![allow(unused)]
fn main() {
ceremony.add_share(ceremony_id, share)
}
- Rejects duplicate share indices
- Rejects shares from unknown ceremonies
- Returns updated
CeremonyState::CollectingShares { shares_received, threshold }
3. Complete
#![allow(unused)]
fn main() {
ceremony.complete(ceremony_id, witness_node: &KeyId)
}
- Requires ≥ threshold shares
- Reconstructs the private key via
shamir::reconstruct_secret(Lagrange interpolation at x = 0) - Derives
VerifyingKeyfrom reconstructed key - Zeroizes the reconstructed private key immediately
- Returns the
VerifyingKey(public key only)
4. Cancel (if needed)
#![allow(unused)]
fn main() {
ceremony.cancel(ceremony_id)
}
- Zeroizes all stored share material
- Transitions to
Failedstate
GF(2^8) arithmetic
All Shamir operations use GF(2^8) with the irreducible polynomial
x^8 + x^4 + x^3 + x + 1 (0x11B), the same field used by AES
and Reed-Solomon.
- Precomputed log/exp tables with generator g = 3
- Multiplication via log/exp:
gf_mul(a, b) = EXP[LOG[a] + LOG[b] % 255] - Division via log/exp:
gf_div(a, b) = EXP[(LOG[a] - LOG[b] + 255) % 255] - Each byte of the secret is split independently
Security properties
- Information-theoretic: fewer than
kshares reveal nothing - Zero-knowledge: shares are never aggregated in memory — each participant holds their share, the ceremony reconstructs only at completion, then immediately zeroizes
- Byzantine-resistant: duplicate shares are rejected; threshold must be ≥ 2
Ceremony state
Created → CollectingShares → Complete
↓
Failed (cancel or error)
Ceremony events are recorded as governance units in the graph.
Node Enrollment
New nodes join the cluster via a multi-party enrollment ceremony. Unlike the root key ceremony (which establishes the root of trust), enrollment gives a new node its own Ed25519 key.
Protocol
1. Initiate
An authorized author (or the root key) initiates enrollment:
#![allow(unused)]
fn main() {
enrollment.initiate(
node_id: &NodeId,
trust_domain: &TrustDomainId,
authorized_by: &AuthorId,
total_shares: u8,
threshold: u8,
attestation: Option<&AttestationResult>,
)
}
- If attestation is provided, it is verified before enrollment proceeds
- Generates a new Ed25519
KeyPair - Splits the private key via
shamir::split_secret - Returns
EnrollmentIdand shares (for distribution to existing nodes)
2. Contribute
Existing nodes contribute shares:
#![allow(unused)]
fn main() {
enrollment.contribute(enrollment_id, share)
}
- Rejects duplicate share indices
- Rejects shares from unknown or completed enrollments
- Returns updated
EnrollmentState::Collecting { shares_received, threshold }
3. Complete
#![allow(unused)]
fn main() {
enrollment.complete(enrollment_id)
}
- Requires ≥ threshold shares
- Reconstructs private key via
shamir::reconstruct_secret - Derives
VerifyingKey(public key) - Zeroizes the reconstructed private key
- Generates a membership governance unit ID
- Returns
EnrollmentResult { node_id, public_key, key_id, trust_domain, membership_unit_id, attestation }
4. Cancel (if needed)
#![allow(unused)]
fn main() {
enrollment.cancel(enrollment_id)
}
Zeroizes all stored share material. Transitions to Failed state.
Trust model
- The root key (from the Shamir ceremony) authorizes enrollment
- Threshold
kout ofnexisting nodes must contribute shares - The new node never receives the root key — only its own key
- The enrollment event is recorded as a governance unit in the graph
Attestation
Attestation can be required before enrollment proceeds:
| Provider | Security | Use case |
|---|---|---|
SoftwareAttestation | Weak (SHA-256 of binary + OS info) | Dev, testing |
TpmAttestation | Strong (TPM 2.0 quote, AIK-signed) | Production (feature-gated) |
Software attestation generates a SHA-256 hash of the running binary and OS information, bound to a nonce. The verifier checks the quote matches and the signature is valid. This is intentionally weak — it only proves the binary hasn’t been modified at rest, not that the platform is trustworthy.
TPM attestation uses the TPM’s Attestation Identity Key (AIK) to sign a quote of the platform’s boot state. This is cryptographically verifiable and resistant to tampering.
SLSA Provenance
taba supports SLSA (Supply-chain Levels for Software Artifacts) build provenance verification. Trust domains can require a minimum SLSA level for workload units.
SLSA levels
| Level | Description | taba enforcement |
|---|---|---|
| 0 | No provenance | No requirement (default) |
| 1 | Build process documented | Provenance exists |
| 2 | Hosted build service | Builder identified, source repo + digest |
| 3 | Hardened build platform | Builder signature verified |
Provenance structure
#![allow(unused)]
fn main() {
pub struct SlsaProvenance {
pub builder: String, // e.g., "github-actions/v3"
pub source_repo: String, // e.g., "github.com/acme/service"
pub source_digest: String, // e.g., "sha256:abc123..."
pub slsa_level: u8, // 1, 2, or 3
pub build_timestamp: u64, // Wall time millis
pub builder_signature: Vec<u8>, // Ed25519 signature over payload
pub build_parameters: Vec<(String, String)>,
}
}
Verification
The ProvenanceVerifier trait checks:
- Structural validation: all required fields present
- SLSA level check:
provenance.slsa_level >= min_level - Source digest comparison: if expected digest is provided, it must match exactly
- Builder signature verification: if the builder is in the trusted list, its Ed25519 signature is verified against the builder’s public key
Default behavior
- No trusted builders: all builders are trusted without signature verification (progressive disclosure — SLSA level 1 doesn’t require signatures)
- With trusted builders: only builders in the trusted list are accepted; their signatures are verified
Signing
#![allow(unused)]
fn main() {
pub fn sign_provenance(
provenance: &mut SlsaProvenance,
signing_key: &SigningKey,
) -> Result<(), SecurityError>
}
Signs over the JSON serialization of the provenance without the
builder_signature field. Any modification after signing
(including a single byte change) causes verification to fail.
Integration
The solver calls ProvenanceVerifier::verify before placing a
workload unit. If the trust domain requires a minimum SLSA level
and the unit’s provenance is below that level, placement is
blocked (fail-closed per INV-S2).
K8s migration
The taba-k8s converter can generate SLSA provenance placeholders:
taba-k8s convert deployment.yaml --provenance
The generated units contain commented-out [provenance] sections
for filling in build system details.
Attestation
Attestation proves a node’s hardware/software integrity before it joins the cluster. It is optional (A5): dev/small deployments work without hardware attestation.
Providers
Software attestation (dev/test)
SoftwareAttestation generates a SHA-256 hash of the running
binary and OS information, bound to a nonce:
quote = SHA-256(binary_hash || os || arch || nonce)
signature = quote (self-signed — intentionally weak)
Weakness: only proves the binary hasn’t been modified at rest. Does not prove the platform is trustworthy. A compromised system can forge a quote.
Use case: dev, testing, CI. Clearly marked as dev-only.
TPM attestation (production, feature-gated)
TpmAttestation uses TPM 2.0 to produce a cryptographically
signed quote of the platform’s boot state:
- Uses the TPM’s Attestation Identity Key (AIK)
- Quote includes the nonce (prevents replay)
- Signature is verifiable against the AIK public key
- Requires a TPM 2.0 chip and the
tssfeature
Use case: production, high-security environments.
Verification
#![allow(unused)]
fn main() {
pub fn verify_software_attestation(
result: &AttestationResult,
expected_nonce: &[u8],
) -> Result<(), SecurityError>
}
Checks:
- The quote matches the expected value (binary_hash + os + arch + nonce)
- The signature matches the quote (self-signed for software)
- The nonce matches (prevents replay)
Tampering with any field after attestation causes verification to fail.
Attestation result
#![allow(unused)]
fn main() {
pub struct AttestationResult {
pub node_id: NodeId,
pub binary_hash: [u8; 32],
pub os: String,
pub arch: String,
pub quote: Vec<u8>,
pub signature: Vec<u8>,
pub provider: AttestationProvider,
}
}
The provider field indicates whether software or TPM attestation
was used, allowing the enrollment ceremony to reject weak
attestation in production environments.
System Overview
taba is a next-generation infrastructure primitive that replaces the container + orchestrator model (Docker + Kubernetes) with self-describing, capability-aware workload units composed through a distributed solver.
The problem
Every step in the infrastructure abstraction trajectory — VMs, containers, Kubernetes — increased control plane complexity monotonically. The diagnosis:
- The container is too dumb: an opaque black box that carries no information about what it needs, what it provides, or what it tolerates.
- The control plane is too smart: an unbounded state reconciliation engine that must infer everything the container didn’t say.
The separation between workload description and orchestration is drawn in the wrong place.
The taba approach
taba draws it differently. Units describe themselves — what they need, what they provide, what they tolerate, who they trust. A deterministic solver composes them. The result is the desired state. There is no separate desired state store.
Five pillars
- Self-describing typed units — workload, data, policy, governance. Each carries capability declarations, behavioral contracts, and security requirements.
- Emergent control plane — the control plane is the union of deployed units’ operational semantics. One unit = trivial control plane. A thousand = the union of their contracts.
- Security as first class — zero-access default, capability-based, fail-closed on conflicts. Units are Ed25519-signed by their author. Taint propagation is structural.
- Data as first-class unit — datasets carry schema, classification, provenance, retention, and consent. Lineage falls out of the composition graph.
- Peer-to-peer — no masters, no leaders, no external metadata store. CRDT graph, erasure-coded, gossip membership.
Architecture diagram
taba-common types, config, protobuf
|
taba-core unit type system, capabilities, contracts
|
taba-security signing, verification, taint, Shamir ceremony
/ \
taba-graph taba-solver CRDT graph + deterministic solver (parallel)
/ \ |
taba-erasure taba-gossip erasure coding + membership (parallel)
\ /
taba-node per-node daemon, WAL, reconciliation
|
taba-cli command-line interface
|
taba-k8s K8s manifest converter
14 crates. Acyclic dependency graph. Single-node works before multi-node (progressive complexity).
Crates
| Crate | Responsibility | Tests |
|---|---|---|
| taba-common | Identity newtypes, Ppm arithmetic, clocks, config | 31 |
| taba-core | Unit model, capabilities, validation, contracts | 86 |
| taba-security | Ed25519, scope, taint, delegation, ceremony, SLSA | 106 |
| taba-graph | δ-state CRDT composition graph, merge, policy chains | 162 |
| taba-solver | Deterministic placement, conflict detection, cycles | 143 |
| taba-observe | Decision trails, structured events, health, Prometheus | 45 |
| taba-node | WAL, reconciler, runtime (Docker, Native, Wasm, MicroVm), health, mode, discovery | 97 |
| taba-gossip | SWIM membership, signed messages, failure detection | 41 |
| taba-erasure | Reed-Solomon GF(2^8), shard distribution, reconstruction | 78 |
| taba-cli | clap CLI (init, apply, unit, status, compose, audit, reconcile, daemon) | 55 |
| taba-k8s | K8s manifest converter | 31 |
| taba-acceptance | BDD acceptance tests (cucumber-rs, 268 scenarios) | — |
| taba-test-harness | Builders, InMemoryUnitStore, proptest strategies | 33 |
| taba-integration | End-to-end integration tests | 15 |
Total: 960 unit tests + 47 e2e tests + 268 BDD scenarios.
Key invariants
| ID | Description |
|---|---|
| INV-C1 | Graph is the single source of desired state |
| INV-C2 | CRDT merge: commutative, associative, idempotent |
| INV-C3 | Solver determinism: fixed-point, no float |
| INV-C4 | WAL-before-effect |
| INV-C6 | Composition is order-independent |
| INV-C7 | One non-revoked policy per conflict tuple |
| INV-S1 | Zero-default capabilities |
| INV-S2 | Security conflicts fail closed |
| INV-S3 | Signed units, causal revocation |
| INV-S4 | Taint propagation at query time |
| INV-S5 | Author scope enforcement |
| INV-S7 | Data hierarchy: narrow freely, widen with policy |
| INV-S8 | Unique author scopes (state-producing) |
| INV-S9 | Multi-party declassification |
| INV-S10 | Multi-party trust domain creation |
| INV-W3 | Spawn depth ≤ 4 |
| INV-R3 | 2-witness failure confirmation |
| INV-R6 | Graph memory limit, auto-compaction at 80% |
| INV-G3 | Governance units exempt from compaction |
| INV-O1 | Every solver run produces a decision trail |
See specs/invariants.md for the complete list (66 invariants).
Module Map
Canonical reference for what each crate owns, exports, and explicitly does not own. Every boundary is load-bearing: crossing a boundary without going through the documented API surface is a defect.
taba-common
Responsibilities
Foundation types and infrastructure shared by every other crate. Owns identity types, configuration parsing, protobuf codegen, and tracing initialization. No domain logic.
Public API surface
Types: NodeId, UnitId, AuthorId, TrustDomainId, ClusterId,
PolicyId, DelegationTokenId, Timestamp, ValidityWindow,
Ppm (fixed-point ppm wrapper, u64)
Logical clock: LogicalClock (u64, monotonically increasing),
DualClockEvent (logical_clock, wall_time, timezone),
clock_advance(local, remote) -> LogicalClock (max(local, remote) + 1)
Clock capability: ClockQuality (Ntp | Ptp | Gps | Unsync),
Timezone (IANA string)
Config: TabaConfig (TOML-backed), NodeConfig, ErasureConfig,
GossipConfig, SolverConfig, ObserveConfig, ArtifactConfig
Proto: generated prost structs for all wire types, tonic service stubs
Tracing: init_tracing(), span helpers
Error foundation: CommonError enum (serialization, config, I/O)
Internal modules
types– identity newtypes,Ppmarithmeticconfig– TOML deserialization, validation, defaultsproto– prost/tonic build outputtracing– subscriber setup, span macros
Does NOT own
- Domain semantics of any type (units, capabilities, policies)
- Crypto (no signing, no key material)
- Any network I/O
Traces to
- A2 (ppm type lives here, arithmetic helpers)
- A7 (config is TOML, human-readable)
- INV-T1 (logical clock monotonic, sync on communication)
- INV-T2 (dual clock model: logical for ordering, wall for retention)
taba-core
Responsibilities
The unit type system. Owns the definition of what a unit is, what capabilities are, how contracts are structured, and validation of well-formedness. This is the domain model in code. Provides the capability matching algorithm.
Public API surface
Unit types: Unit, WorkloadUnit, DataUnit, PolicyUnit,
GovernanceUnit, GovernanceSubtype (TrustDomain | RoleAssignment |
Certification | OperationalCommand | PromotionGate | CrossDomainCapability)
Unit lifecycle: UnitState (Declared, Composed, Placed, Running,
Draining, Terminated)
Workload subtypes: WorkloadKind (Service | BoundedTask),
BoundedTaskTermination (Completed | Failed | DeadlineExceeded),
ValidityWindow (optional: lc_range, wall_time_deadline)
Artifact model: Artifact (type_, ref_, digest, requires),
ArtifactType (Oci | Native | Wasm | K8sManifest)
Delegation tokens: DelegationToken (service_id, node_id, trust_domain,
valid_lc_range, max_spawns, author_sig), DelegationTokenId
Node capabilities: NodeCapabilitySet (hardware, os, privilege, runtimes,
network, storage, environment, author_affinity, custom_tags),
RuntimeCapability (Oci | OciRootless | K8s | Wasm | Native | MicroVm),
EnvironmentTag (Dev | Test | Prod | Custom(String)),
PrivilegeLevel (Root | User)
Promotion: PromotionPolicy (unit_ref, version, environment, rationale),
PromotionGate (transitions: VecPromotionTransition (from_env, to_env, mode: Auto | HumanApproval)
Tombstone: Tombstone (unit_id, author_id, unit_type, created_at_lc,
terminated_at_lc, termination_reason, references, original_digest)
Health checks: HealthCheck (type_: OsProcess | Http | Tcp | Command,
endpoint, interval, timeout)
Capabilities: Capability (type, name, purpose?), CapabilityList
(sorted, deterministic), NeedsDeclare, ProvidesDeclare
Contracts: Tolerates, Trusts, ScalingParams, FailureSemantics,
RecoveryRelationship, StateRecovery, PlacementOnFailure (Replace |
LeaveDead, default from environment)
Data model: DataClassification (Public < Internal < Confidential < PII),
Provenance, Retention, ConsentScope, DataHierarchy
Policy model: ConflictTuple (set of UnitIds + capability name),
Resolution (Allow | Deny | Conditional), SupersessionChain
Validation: validate_unit() -> Result<ValidatedUnit, ValidationError>
Matching: match_capabilities(needs, provides) -> MatchResult,
CapabilityMatcher trait
Shared abstract types (defined here for DAG cleanliness, implemented elsewhere):
GraphSnapshottrait: immutable point-in-time graph view. Implemented by taba-graph. Consumed by taba-solver. Wrapped inArcfor safe concurrent access — snapshot is frozen at creation, never mutated.MembershipSnapshot: current cluster membership view. Implemented by taba-gossip. Consumed by taba-solver.Waltrait: write-ahead log abstract interface. Implemented by taba-node (disk-backed) and taba-test-harness (in-memory). Consumed by taba-graph (WAL-before-effect per INV-C4).
Errors: CoreError (InvalidUnit | MalformedCapability |
MatchAmbiguous | HierarchyDepthExceeded)
Internal modules
unit– type definitions, state machine, workload subtypes (Service/BoundedTask)capability– capability tuples, sorted lists, matchingcontract– tolerance, trust, scaling, failure, recovery, placement-on-failuredata– classification lattice, provenance, retention (persistent/ephemeral/ local-only), hierarchy (max depth 16)policy– conflict tuples, resolution, supersession, promotion policiesgovernance– trust domain, role assignment, certification, operational command, promotion gate, cross-domain capability advertisement subtypesvalidation– well-formedness checks, cross-field consistency, spawn depthartifact– artifact type, ref, digest, requiresdelegation– delegation token type, scope, lifetimetombstone– tombstone type, reference preservationnode_capability– capability set, runtime capabilities, environment tags, privilege level, custom tagshealth– health check declaration types
Does NOT own
- Signing or verification (that is taba-security)
- Graph storage or merge (that is taba-graph)
- Placement or composition resolution (that is taba-solver)
- Serialization format decisions beyond derive macros (wire format is proto in taba-common)
- Delegation token validation (that is taba-security)
Traces to
- Domain model (all entity definitions)
- INV-K1, INV-K2 (capability matching)
- INV-K3, INV-K4 (tolerance/scaling declarations)
- INV-S7 (classification lattice ordering, hierarchy narrowing/widening)
- INV-D3 (hierarchy depth limit)
- INV-D4, INV-D5 (ephemeral/local-only data lifecycle)
- INV-E1, INV-E2, INV-E3 (environment progression)
- INV-N2, INV-N4 (capability matching, custom tags)
- INV-W1, INV-W2, INV-W3 (service vs bounded task lifecycle, spawn depth)
- INV-W4 (delegation token structure)
- DL-010 (purpose as optional qualifier)
taba-security
Responsibilities
All cryptographic operations and security policy enforcement. Owns Ed25519 signing/verification, capability enforcement (declared vs allowed), taint computation via provenance traversal, Shamir root key ceremony (Tier 0 solo through Tier 3), delegation token validation, and key revocation lifecycle (causal model).
Cross-cutting: every other crate calls into taba-security for verification, signing, or enforcement.
Public API surface
Signing: Signer trait, sign_unit(key, unit, context) -> SignedUnit,
SigningContext (trust_domain_id, cluster_id, logical_clock, validity_window?)
Verification: Verifier trait,
verify_unit(signed_unit, context) -> Result<(), VerificationError>,
verify_signature(sig, pubkey, payload) -> Result<(), CryptoError>
Delegation: DelegationValidator trait,
validate_delegation(token, spawned_unit) -> Result<(), DelegationError>,
create_delegation_token(author_key, service_id, node_id, trust_domain, lc_range, max_spawns) -> DelegationToken,
revoke_delegation(token_id) -> Result<(), DelegationError>
Scope enforcement: ScopeChecker trait,
check_author_scope(author, unit_type, trust_domain) -> Result<(), ScopeError>,
validate_scope_uniqueness(assignments, unit_type) -> Result<(), ScopeConflict>
(INV-S8: unique for workload/data, INV-S8a: overlapping OK for policy/governance)
Capability enforcement: CapabilityEnforcer trait,
enforce(unit, granted_capabilities) -> Result<(), EnforcementError>
Taint: TaintComputer trait,
compute_taint(unit_id, graph) -> DataClassification (query-time traversal),
check_declassification(policy) -> Result<(), DeclassError> (requires
multi-party per INV-S9)
Ceremony: CeremonyManager trait, CeremonyState (Started |
CollectingShares | Complete), CeremonyTier (Tier0 | Tier1 | Tier2 | Tier3),
solo_init() -> Result<(KeyPair, TrustDomain, GovernanceUnit), CeremonyError>
(Tier 0: single command, no Shamir)
Revocation (causal model per INV-S3):
is_key_revoked(author_id, local_graph) -> bool (checks if revocation
governance unit is merged into local graph — not clock comparison),
RevocationGovernanceUnit,
check_with_grace(author_id, creation_lc, revocation_lc, grace_window) -> bool
(optional fallback)
Key management: KeyPair (Ed25519), PublicKey, SecretKey (zeroize on drop)
Errors: SecurityError (InvalidSignature | ScopeViolation | KeyRevoked |
ContextMismatch | TaintViolation | CeremonyFailed | DeclassificationDenied |
DelegationExpired | DelegationScopeViolation | DelegationSpawnLimitExceeded |
DelegationGovernanceBlocked | DelegationTokenForged)
Internal modules
crypto– Ed25519 operations, key generation, zeroizesigning– unit signing with bound contextverification– signature verification, causal revocation check (key revoked in local graph? per INV-S3)scope– author scope checking, type-dependent uniqueness (INV-S8/S8a)delegation– delegation token creation, validation, revocation, governance authority block (INV-W4a)enforcement– runtime capability enforcementtaint– provenance graph traversal, classification union for multi-input, declassification multi-party checkceremony– Shamir split/reconstruct, ceremony state machine, witness tracking, tier-specific logicrevocation– revocation event creation, propagation via priority gossip (interface only; gossip transport is taba-gossip)
Does NOT own
- The graph data structure (traverses it for taint, does not store it)
- Gossip transport (publishes revocation events; transport is taba-gossip)
- Policy semantics (checks signatures on policies, does not interpret them)
- TPM attestation (Phase 5, feature-gated, not in initial scope)
Traces to
- INV-S1 (zero-default capability enforcement)
- INV-S2 (fail-closed on conflicts – enforcement side)
- INV-S3 (signature binding, causal revocation model, grace window)
- INV-S4 (taint: query-time traversal, union of inputs)
- INV-S5 (author scope enforcement)
- INV-S8, INV-S8a (scope uniqueness: strict for workload/data, relaxed for policy/governance)
- INV-S9 (multi-party declassification)
- INV-S10 (multi-party trust domain creation signing)
- INV-W4, INV-W4a (delegation tokens: validation, governance authority block)
- DL-005 (ceremony tiers: Tier 0 solo through Tier 3)
- DL-007 (query-time taint)
- FM-04 (compromised node), FM-05 (compromised author)
- FM-18 (role succession, break-glass via root key)
taba-graph
Responsibilities
The CRDT composition graph. Owns the distributed data structure that IS the desired state. Manages merge semantics (commutative, associative, idempotent), signature verification as a synchronous gate before merge, causal buffering (pending queue for units with unsatisfied references), provenance chain tracking, hierarchical data unit containment, and WAL integration for local persistence.
Public API surface
Graph: CompositionGraph trait, GraphSnapshot (immutable, for solver)
Operations: insert_unit(signed_unit) -> Result<InsertResult, GraphError>,
query_units(filter) -> Vec<Unit>, query_provenance(unit_id) -> ProvenanceChain,
supersede_policy(old_id, new_policy) -> Result<(), GraphError>,
archive_subgraph(filter) -> Result<ArchivedSubgraph, GraphError>
Insert result: InsertResult (Merged | Pending { missing_refs })
Merge: merge(local, remote) -> CompositionGraph (CRDT merge),
MergeOutcome (units added, conflicts surfaced)
Causal buffering: PendingQueue, promote(unit_id) -> Result<(), GraphError>
WAL: WalEntry (Merged(unit) | Pending(unit, missing_refs) |
Promoted(unit_id)). Uses Wal trait (defined in taba-core, implemented
by taba-node). Graph calls Wal::append() before applying mutations (INV-C4).
Provenance: ProvenanceChain, ProvenanceLink (producer, inputs, timestamp, policies)
Compaction: CompactionPolicy, compact(policy) -> CompactionResult
Errors: GraphError (SignatureInvalid | ScopeViolation | KeyRevoked |
ContextMismatch | WalFailed | CompactionFailed | HierarchyViolation)
Internal modules
crdt– graph CRDT implementation, merge algorithm, idempotency checksmerge– merge entry point, signature verification gate (calls taba-security), causal reference resolutionpending– pending queue, reference tracking, promotion logicprovenance– chain construction, queryhierarchy– data unit parent-child relationships, constraint inheritancewal– WAL entry types, write-ahead semantics, replay on startupcompaction– retention-driven cleanup, auto-compaction at 80% memory, archival of cold subgraphssnapshot– immutable graph snapshot for solver consumptionquery– unit filtering, policy validity check at query time (INV-C5)
Bootstrap special case
The initial governance unit from the Shamir ceremony (CeremonyCompleted) is
inserted without normal signature verification — it is trusted by the ceremony
protocol itself. This is the only unit that bypasses the verification gate.
The ceremony produces a signed root governance unit; the graph accepts it via
a dedicated insert_bootstrap(unit) method that requires the ceremony’s
public key as a parameter (not the normal author key flow).
Governance unit replication
Governance units are actively replicated (full copies on N nodes), not just erasure-coded. taba-graph decides which units are governance (by type) and signals this to taba-erasure via the shard distribution interface.
Does NOT own
- Signature computation (delegates to taba-security; owns the gate that calls it)
- Composition resolution or placement (that is taba-solver)
- Erasure coding of shards (that is taba-erasure)
- Gossip transport of graph deltas (that is taba-gossip)
- Solver determinism (graph provides snapshots; solver owns determinism)
Traces to
- INV-C1 (graph is single source of desired state)
- INV-C2 (merge: commutative, associative, idempotent)
- INV-C4 (WAL-before-effect, causal buffering, entry types)
- INV-C5 (orphaned policy detection at query time)
- INV-D1 (provenance chain unbroken, causal buffering for pending refs)
- INV-D2 (retention enforcement via compaction)
- INV-D3 (hierarchy depth enforcement)
- INV-R6 (memory limit, auto-compaction, governance replication)
- INV-S3 (synchronous signature verification gate before merge)
- DL-008 (WAL entry types: Merged, Pending, Promoted)
- DL-011 (memory-bounded Phase 1-2, sharding Phase 3+)
taba-solver
Responsibilities
Composition resolution, conflict detection, deterministic placement, policy application, scaling computation, and cycle detection. The solver is a pure deterministic function: given an immutable graph snapshot and node membership, it produces identical results on any node.
Public API surface
Solver: Solver trait,
resolve(snapshot, membership) -> SolverResult
Composition: compose(snapshot) -> CompositionResult,
CompositionResult (compositions, unresolved conflicts)
Placement: place(compositions, membership, capabilities, resources) -> PlacementPlan,
PlacementPlan, PlacementDecision (unit_id, node_id, rationale)
Capability filter (hard constraints per INV-N2):
filter_by_capabilities(unit, nodes) -> Vec<NodeId> — binary match on
runtime, arch, OS, privilege, custom tags. Artifact type → runtime capability.
Resource ranking (soft constraints per INV-N3):
rank_by_resources(nodes, resources) -> Vec<(NodeId, Ppm)> — best-fit
scoring using versioned resource snapshots (determinism per F-A306).
Environment filter: filter_by_environment(unit, promotions, nodes) -> Vec<NodeId>
— checks PromotionPolicy authorization for each environment tag.
env:dev uses author affinity instead of promotion (INV-E1).
Promotion evaluation: evaluate_promotion(unit, policies, gates) -> PromotionResult
— checks PromotionGate governance, handles auto vs human-approval.
Collision detection: same-decision → dedup by lowest PolicyId; different
decisions → fail closed (INV-S8a).
Conflict: ConflictReport, Conflict (security | ambiguity | cycle),
detect_conflicts(snapshot) -> Vec<Conflict>
Policy application: apply_policies(conflicts, policies) -> ResolutionResult,
SupersessionResolver (walks chain, uses latest non-revoked per INV-C7)
Scaling: compute_scaling(unit, metrics) -> ScalingDecision
Cycle detection: detect_cycles(units) -> Vec<CyclicDependency>
Tiebreaker: partition_tiebreak(node_a, node_b) -> NodeId (lexicographic lowest)
Errors: SolverError (UnresolvableConflict | CyclicDependency |
InsufficientResources | PrecisionOverflow)
Internal modules
composition– capability needs/provides matching, composition construction (order-independent per INV-C6)conflict– security conflict detection, ambiguity detection, promotion policy collision detection (same-decision dedup, different-decision fail closed)placement– deterministic scoring (fixed-point ppm per DL-004), resource fitting, tolerance matchingcapability_filter– hard constraint filtering: runtime matching (artifact.type → node runtime capability), environment tags, privilege requirements, custom tag matchingresource_rank– soft constraint ranking: memory, CPU, disk, GPU availability. Uses versioned resource snapshots for determinism.promotion– promotion policy evaluation, PromotionGate checking, auto vs human-approval, environment transition validationpolicy– policy lookup, supersession chain resolution, validity checkscaling– declared-parameter-driven scaling (INV-K4)cycle– recovery dependency cycle detection, fail-closed (INV-K5), spawn depth validation (INV-W3)tiebreak– deterministic tiebreaker (lexicographic lowest NodeId per INV-C3)arithmetic– ppm fixed-point operations (u64/i64, division rounds toward zero)
Does NOT own
- Graph state (reads immutable
GraphSnapshotdefined in taba-core, implemented by taba-graph, wrapped in Arc for concurrent safety) - Membership (reads
MembershipSnapshotdefined in taba-core, implemented by taba-gossip) - Placement execution (taba-node reconciles actual state)
- Capability matching algorithm (defined in taba-core; solver uses it)
- Taint computation (delegates to taba-security)
Traces to
- INV-C3 (deterministic solver, fixed-point, tiebreaker)
- INV-C6 (composition order-independent)
- INV-C7 (policy uniqueness, supersession chain)
- INV-K1 (valid composition = all needs satisfied, no conflicts)
- INV-K2 (typed capability matching with purpose)
- INV-K3 (placement respects tolerance declarations)
- INV-K4 (scaling from unit-declared parameters only)
- INV-K5 (cyclic recovery dependencies fail closed)
- INV-S2 (fail closed on security conflicts)
- DL-004 (fixed-point ppm arithmetic)
- DL-006 (policy supersession)
- FM-06 (solver bug), FM-11 (determinism regression), FM-12 (version skew)
taba-erasure
Responsibilities
Erasure coding for graph resilience. Owns Reed-Solomon encoding/decoding, shard distribution across nodes, reconstruction on node failure with backpressure (priority queue, circuit breaker), and re-coding when fleet size changes.
Public API surface
Coding: ErasureCoder trait,
encode(data, params) -> Vec<Shard>,
decode(shards, params) -> Result<Vec<u8>, ErasureError>
Parameters: ErasureParams (k, n, resilience_pct),
compute_params(fleet_size, resilience_pct) -> ErasureParams
Distribution: ShardDistributor trait,
distribute(shards, membership) -> ShardAssignment,
reassign(assignment, membership_change) -> ShardAssignment
Reconstruction: Reconstructor trait,
reconstruct(shard_ids, available_shards) -> Result<Vec<u8>, ReconstructionError>,
ReconstructionQueue (priority: governance > policy > data constraints > workload),
BackpressureConfig (rate limit, queue depth threshold, circuit breaker)
Re-coding: recode(current_shards, new_params) -> Vec<Shard>
Shard: Shard (shard_id, data, parity_index), ShardAssignment
Errors: ErasureError (InsufficientShards | ReconstructionFailed |
BackpressureTripped | CircuitBreakerOpen)
Internal modules
reed_solomon– RS encoding/decoding implementationparams– parameter computation (k = ceil(N * (1 - R/100)))distribution– shard-to-node mapping, rebalancingreconstruction– decode + signature re-verification (calls taba-security), priority queue, backpressure, circuit breakerrecoding– fleet size change handling, rolling re-encode
Does NOT own
- Graph structure (encodes opaque data blobs; taba-graph decides what to shard)
- Gossip transport (receives membership changes; does not participate in gossip)
- Shard persistence (taba-node stores shards on disk)
- Signature verification (delegates to taba-security after reconstruction)
Traces to
- INV-R1 (reconstruction backpressure, priority, circuit breaker, post-reconstruction signature re-verification)
- INV-R4 (reconstructable if failures <= floor(N-k), degraded mode otherwise)
- A3 (erasure overhead acceptable for graph shard sizes)
- FM-02 (multiple node failures), FM-13 (reconstruction storm)
taba-gossip
Responsibilities
SWIM-based membership protocol. Owns failure detection with 2-witness confirmation, node join/leave handling, membership view dissemination, and signed gossip message transport. Also carries priority messages (key revocation events, solver version announcements).
Public API surface
Membership: MembershipService trait,
members() -> MembershipView,
join(node_id, pubkey) -> Result<(), GossipError>,
leave(node_id) -> Result<(), GossipError>
View: MembershipView, MemberState (Joining | Attesting | Active |
Suspected | Draining | Left | Failed), NodeHealth (healthy | suspected | unknown)
Failure detection: FailureDetector trait,
probe(node_id) -> ProbeResult,
confirm_failure(node_id, witnesses) -> Result<FailureConfirmation, GossipError>
Messages: GossipMessage (signed), MessageType (Ping | PingReq |
Ack | MembershipDelta | PriorityEvent)
Priority events: PriorityEvent (KeyRevocation | SolverVersionAnnounce |
DegradedModeAlert | OperationalCommand)
Capability advertisement: CapabilityAdvertisement (node_id,
capability_set, resource_snapshot, logical_clock). Nodes advertise
capabilities (rarely, on change/refresh) and resources (periodically).
Cross-domain: CrossDomainGossip — bridge nodes relay capability
advertisements and forwarding query responses between trust domains.
ForwardingQuery, ForwardingResult (signed by bridge).
Fleet operations: FleetCommand (RefreshCapabilities | other).
Rate-limited: one command per type per gossip convergence window.
Deduplication by command type + logical clock.
Errors: GossipError (SignatureInvalid | InsufficientWitnesses |
NodeNotFound | TransportFailed | FleetCommandRateLimited)
Internal modules
swim– SWIM protocol state machine, probe scheduling, indirect probesmembership– membership view, state transitions, convergence trackingfailure– suspicion tracking, witness collection, 2-witness confirmation threshold, suspected-node retention (INV-R5)messages– message types, signing (calls taba-security), verificationtransport– UDP/TCP gossip transport, message serializationpriority– priority event dissemination (key revocation, version announcements)capability– capability/resource advertisement, periodic resource snapshotscross_domain– bridge node detection, cross-domain capability advertisement relay, forwarding query routing, cache management (fail-open default)fleet– operational command propagation, rate limiting, dedup (INV-A314)
Does NOT own
- Graph state or shards (carries deltas; does not interpret them)
- Placement decisions (announces membership; solver uses it)
- Key material (signs messages via taba-security)
- Shard redistribution logic (that is taba-erasure)
Traces to
- INV-R3 (gossip convergence, signed messages, 2-witness confirmation)
- INV-R5 (suspected nodes stay in pool with health=‘unknown’)
- INV-N1 (capability advertisement via gossip)
- INV-X4, INV-X5 (bridge node discovery, cross-domain capability advertisement)
- DL-009 (signed gossip, witness confirmation)
- A4 (SWIM scales to target cluster sizes)
- A17 (bridge nodes sufficient for cross-domain discovery)
- FM-04 (gossip poisoning resistance), FM-09 (false positive handling)
- FM-12 (solver version announcements via priority gossip)
- FM-25 (bridge failure isolates domains)
- F-A314 (fleet refresh rate limiting)
taba-node
Responsibilities
Per-node daemon. Owns local reconciliation (desired vs actual state), WAL management and persistence, shard storage, health reporting, and operational mode management (Normal, Degraded, Recovery). This is the main runtime binary that ties all other crates together.
Public API surface
Daemon: NodeDaemon (entry point),
start(config) -> Result<(), NodeError>,
shutdown() -> Result<(), NodeError>
Reconciliation: Reconciler trait,
reconcile(desired, actual) -> Vec<ReconciliationAction>,
ReconciliationAction (Start | Stop | Drain | Migrate)
WAL management: WalManager (wraps taba-graph Wal),
replay() -> Result<GraphState, WalError>,
checkpoint() -> Result<(), WalError>
Shard storage: ShardStore trait,
store_shard(shard) -> Result<(), StorageError>,
retrieve_shard(shard_id) -> Result<Shard, StorageError>
Health: HealthReporter trait, report() -> NodeHealthStatus,
OperationalMode (Normal | Degraded | Recovery)
Auto-discovery: CapabilityDiscoverer trait,
discover() -> NodeCapabilitySet (probes Docker, Podman, K8s, Wasm, TPM,
GPU, etc.), refresh() -> NodeCapabilitySet (re-probe)
Artifact management: ArtifactFetcher trait,
fetch(artifact_ref, digest) -> Result<ArtifactPath, ArtifactError>,
fetch order: peer cache → external source (INV-A2).
PeerCache (content-addressed by SHA256, P2P distribution).
push(artifact_path) -> Result<(), ArtifactError> (for air-gapped/dev).
Digest verification mandatory after fetch (INV-A1).
Task spawning: TaskSpawner trait,
spawn(delegation_token, task_unit) -> Result<SpawnResult, SpawnError>,
validates delegation token (calls taba-security), signs task via token,
inserts into graph. Tracks spawn count against token limit.
Health check orchestration: HealthCheckOrchestrator trait,
register_check(unit_id, health_check) -> (),
run_checks() -> Vec<HealthCheckResult>.
Progressive: OS-level process monitoring (default) → HTTP/TCP probe →
custom command (declared by workload unit per INV-O3).
Mode transitions: ModeManager,
enter_degraded(reason) -> (),
enter_recovery() -> (),
enter_normal() -> ()
Errors: NodeError (WalCorrupt | DiskFull | ReconciliationFailed |
ShardStorageFailed | ModeTransitionInvalid | ArtifactFetchFailed |
ArtifactDigestMismatch | DelegationInvalid | SpawnFailed |
CapabilityDiscoveryFailed)
Internal modules
daemon– startup, shutdown, tokio runtime, signal handlinginit–taba initflow: key generation, Tier 0 bootstrap, node setup (userspace or system)reconciliation– desired-vs-actual comparison, action planning, executionwal_manager– WAL lifecycle, replay on startup, periodic checkpointshard_store– on-disk shard persistence, retrievalhealth– health status computation, peer health observationhealth_check– health check orchestration (OS-level, HTTP, TCP, command)mode– operational mode state machine, transition rules, degraded-mode freeze (no new placements), recovery throttlingruntime– workload execution viaRuntimeSelectorwhich dispatches toDockerRuntime(Oci),NativeRuntime(Native, std::process),WasmRuntime(Wasm, state tracking), orMicroVmRuntime(MicroVm, Firecracker/cloud-hypervisor/QEMU subprocess).RuntimeSelector::new()auto-detects available runtimes and advertisesRuntimeCapabilityto the solver.discovery– auto-discovery probes (Docker, Podman, K8s, Wasm, TPM, GPU), capability set construction, refresh on commandartifact– artifact fetching (peer cache → external), digest verification, P2P peer cache, push mode for air-gappedspawner– delegation token management, task spawning, spawn count tracking
Does NOT own
- Graph CRDT logic (uses taba-graph)
- Solver logic (invokes taba-solver with snapshot + membership)
- Gossip protocol (participates via taba-gossip)
- Erasure coding (delegates to taba-erasure)
- Signing/verification (delegates to taba-security)
- Delegation token validation (delegates to taba-security; owns lifecycle)
- Decision trail recording (delegates to taba-observe)
Traces to
- INV-C4 (WAL-before-effect, local persistence)
- INV-R6 (memory limit enforcement, auto-compaction trigger at 80%)
- INV-N1 (auto-discovery, cached capabilities, refresh)
- INV-A1, INV-A2 (artifact digest verification, fetch order)
- INV-W4 (delegation token lifecycle for spawning)
- INV-O3 (progressive health checks)
- INV-N5 (placement-on-failure defaults by environment)
- Domain model: Operational Modes, Node states, Environment Progression
- FM-01 (single node failure), FM-07 (WAL corruption), FM-08 (graph growth)
- FM-15 (artifact unavailable), FM-17 (stale capability discovery)
- FM-22 (bounded task deadline exceeded)
taba-observe
Responsibilities
Cross-cutting observability. Owns decision trail recording, solver replay, structured event emission, health check result aggregation, and integration export (OpenTelemetry, Prometheus, alerting webhooks). Provides queryable audit infrastructure for the “why did this happen?” question.
Public API surface
Decision trails: DecisionTrailRecorder trait,
record_solver_run(inputs, outputs) -> DecisionTrailId,
query_trail(unit_id, time_range?) -> Vec<DecisionTrail>,
replay_solver(trail_id) -> SolverResult (deterministic replay per INV-C3)
Decision trail type: DecisionTrail (trail_id, graph_snapshot_id,
node_membership_snapshot, solver_version, placements, conflicts,
logical_clock, wall_time)
Structured events: EventEmitter trait,
emit(event) -> (). All significant system actions emit structured events
(JSON-formatted, timestamped, typed).
Health aggregation: HealthAggregator trait,
report_health(unit_id, result) -> (),
query_health(unit_id) -> HealthStatus
Integration export:
PrometheusExportertrait — per-node metrics endpointOpenTelemetryExportertrait — trace/metric/log exportAlertWebhooktrait — webhook on degraded mode, policy conflict, promotion failure. Best-effort (failure doesn’t block system operations).
Retention: decision trail retention defaults to since-last-compaction.
Unit-level decision_retention field or governance-level trust-domain
retention policy can extend (INV-O2).
Errors: ObserveError (TrailNotFound | ReplayFailed | ExportFailed |
RetentionPolicyViolation)
Internal modules
trail– decision trail recording, storage, queryreplay– solver replay from historical trail (deterministic)events– structured event types, emission, bufferinghealth– health check result aggregation, status computationprometheus– Prometheus exposition format endpointotel– OpenTelemetry SDK integrationalert– webhook dispatch, retry, best-effort delivery
Does NOT own
- Solver logic (replays solver via taba-solver; does not own scoring)
- Health check execution (taba-node orchestrates; taba-observe aggregates results)
- Graph storage (trails stored in graph via taba-graph)
- Workload-level metrics (workloads export via standard protocols, not taba)
Traces to
- INV-O1 (every solver run produces decision trail)
- INV-O2 (retention defaults, unit/governance override)
- INV-O3 (progressive health checks — aggregation side)
- Domain model: Observability (structural + integration)
- Domain model: Decision Trail, Solver Replay
taba-cli
Responsibilities
Command-line interface for human operators. Owns unit authoring commands, composition management, status inspection, policy management, trust domain management, audit/lineage queries, and ceremony initiation.
Public API surface
Commands (clap subcommands):
init– Tier 0 solo bootstrap (key + trust domain + governance in one command)apply– sign and insert a unit TOML into the graph (dev workflow)unit– create, inspect, validate, list, archivecompose– trigger composition, inspect result, list conflictsstatus– node health, graph stats, placement map, operational modepolicy– create, supersede, list, inspect resolution chainpromote– create promotion policy for a unit version + environmenttrust-domain– create (ceremony), inspect, list membersaudit– lineage query, provenance chain, taint report, decision trailceremony– initiate Shamir ceremony, add share, completenode– join, leave, drain, healthrefresh– re-probe capabilities (single node)fleet– fleet-wide commands (refresh-capabilities, etc.)observe– query decision trails, solver replay, health statuspush– push artifact to peer cache (air-gapped/dev)
Output: structured (JSON, TOML) and human-readable table formats
Errors: CliError (ConnectionFailed | InvalidInput | Unauthorized |
ServerError)
Internal modules
commands– clap command definitions and dispatchclient– gRPC client to taba-node daemon (tonic)format– output formatting (table, JSON, TOML)auth– local author key management (keyring integration)
Does NOT own
- Any domain logic (pure client; all logic lives in other crates)
- Node runtime (connects to taba-node daemon)
- Key storage beyond local author key (cluster keys managed by taba-security)
Traces to
- A7 (unit declarations are human-authorable in TOML)
- All features/*.feature (CLI is the user-facing entry point for every scenario)
taba-test-harness
Responsibilities
Shared test utilities for the workspace. Owns test data builders, in-memory fakes for graph and solver, proptest strategies for core types, and BDD step definition helpers. No production code depends on this crate.
Public API surface
Builders: UnitBuilder, WorkloadBuilder, DataBuilder, PolicyBuilder,
GovernanceBuilder, NodeBuilder, AuthorBuilder, CapabilityBuilder,
BoundedTaskBuilder, DelegationTokenBuilder, ArtifactBuilder,
PromotionPolicyBuilder, TombstoneBuilder, CapabilitySetBuilder,
HealthCheckBuilder, DecisionTrailBuilder
Fakes: InMemoryGraph (implements CompositionGraph),
InMemorySolver (implements Solver),
InMemoryWal (implements Wal),
FakeMembership (implements MembershipService),
FakeSigner / FakeVerifier,
FakeArtifactFetcher, FakeCapabilityDiscoverer,
FakeDecisionTrailRecorder, FakePeerCache
Proptest strategies: arb_unit(), arb_capability(), arb_node_id(),
arb_signed_unit(), arb_graph_snapshot(), arb_placement(),
arb_delegation_token(), arb_bounded_task(), arb_artifact(),
arb_capability_set(), arb_tombstone(), arb_logical_clock()
BDD helpers: step definition utilities for cucumber-rs,
TestCluster (multi-node in-memory setup)
Assertions: assert_crdt_laws(merge_fn) (commutativity, associativity,
idempotency), assert_solver_determinism(solver, snapshot, membership)
Internal modules
builders– fluent builder pattern for all domain typesfakes– in-memory trait implementationsstrategies– proptest Arbitrary implementationsbdd– cucumber-rs step helpers, test cluster setupassertions– property-based law checkers
Does NOT own
- Production code paths (dev-dependency only)
- Test execution (tests live in each crate; harness provides utilities)
Traces to
- INV-C2 (CRDT law assertion helpers)
- INV-C3, DL-004 (solver determinism assertion)
- FM-06, FM-11 (property-based solver testing)
- Guidelines: TESTING_STRATEGY.md
Dependency Graph
Crate-level DAG for the taba workspace. Every edge is justified. The graph is verified acyclic by construction: each crate depends only on crates appearing above it in the topological order.
Crate DAG
taba-common
/ | \
/ | \
v v v
taba-core (proto) (config)
|
v
taba-security
/ \
v v
taba-graph taba-solver ----+
/ \ | |
v v v v
taba-erasure taba-gossip taba-node
\ / \ / |
\ / \ / |
v v v v |
taba-node <-------+---+ |
| |
v |
taba-cli |
|
|
taba-test-harness (dev-only, depends on all crates)
Simplified linear view (topological order)
Level 0: taba-common
Level 1: taba-core
Level 2: taba-security
Level 3: taba-graph, taba-solver, taba-observe (parallel)
Level 4: taba-erasure, taba-gossip (parallel)
Level 5: taba-node
Level 6: taba-cli
Dev-only: taba-test-harness (all levels)
Note: taba-observe is at Level 3 because it depends on taba-common, taba-core, and taba-security (for trail types and solver replay). It does NOT depend on taba-graph directly — decision trails are persisted via taba-node which provides the graph integration.
Precise dependency edges
taba-common -> (none)
taba-core -> taba-common
taba-security -> taba-common, taba-core
taba-graph -> taba-common, taba-core, taba-security
taba-solver -> taba-common, taba-core, taba-security
taba-observe -> taba-common, taba-core, taba-security
taba-erasure -> taba-common, taba-security
taba-gossip -> taba-common, taba-security
taba-node -> taba-common, taba-core, taba-security, taba-graph,
taba-solver, taba-observe, taba-erasure, taba-gossip
taba-cli -> taba-common, taba-core, taba-observe, taba-node
taba-test-harness -> taba-common, taba-core, taba-security, taba-graph,
taba-solver, taba-observe (dev-dependency only)
Acyclicity verification
The topological order (Level 0-6) proves acyclicity: every dependency edge points from a higher level to a lower level. No edge points upward or laterally within the same level where a mutual dependency would exist.
Specifically:
- Level 3 crates (taba-graph, taba-solver) both depend on Level 2 and below, but NOT on each other. The solver reads graph snapshots via a trait defined in taba-graph, but this is a type dependency (GraphSnapshot), not a crate cycle. Both crates depend on taba-security independently.
- Level 4 crates (taba-erasure, taba-gossip) depend on Level 0 and Level 2, but NOT on each other or on Level 3.
- taba-node (Level 5) is the integration point that depends on Levels 0-4.
- taba-test-harness has no reverse dependencies (dev-only).
Build order (parallel opportunities)
Step 1: taba-common (solo)
Step 2: taba-core (solo, needs common)
Step 3: taba-security, taba-test-harness* (parallel; security needs core;
harness needs core)
Step 4: taba-graph, taba-solver, (parallel; all need security)
taba-observe, taba-erasure, taba-gossip
Step 5: taba-node (needs everything from Step 4)
Step 6: taba-cli (needs node, observe)
*taba-test-harness can build incrementally starting at Step 3, adding features as each crate it depends on becomes available.
Maximum parallelism at Step 4: four crates can compile simultaneously.
Dependency justification
taba-core -> taba-common
Core uses identity types (UnitId, AuthorId, NodeId, TrustDomainId), Ppm arithmetic, Timestamp, and protobuf-generated types defined in common.
taba-security -> taba-common
Security uses identity types for key management and signing context (trust_domain_id, cluster_id in SigningContext).
taba-security -> taba-core
Security must understand unit structure to sign units and enforce capabilities. Needs Unit, Capability, DataClassification for taint computation, GovernanceUnit for ceremony handling.
taba-graph -> taba-common
Graph uses identity types, timestamps, config, and Wal-related types.
taba-graph -> taba-core
Graph stores units – needs Unit, PolicyUnit (for supersession), DataUnit (for hierarchy), ProvenanceLink. Validates well-formedness on insert.
taba-graph -> taba-security
Graph calls Verifier trait synchronously before merge (INV-S3). Checks signature validity, author scope, and key revocation status. This is the signature verification gate.
taba-solver -> taba-common
Solver uses NodeId for placement, Ppm for fixed-point arithmetic, config for solver parameters.
taba-solver -> taba-core
Solver uses Capability, CapabilityMatcher, Unit types, ConflictTuple, PolicyUnit, ScalingParams, RecoveryRelationship, Tolerates for composition resolution and placement.
taba-solver -> taba-security
Solver calls TaintComputer to compute inherited classifications at query time (INV-S4). Calls ScopeChecker when evaluating policy validity.
taba-observe -> taba-common
Observe uses identity types (UnitId, NodeId), LogicalClock, DualClockEvent, Ppm for metric values, config for observe parameters.
taba-observe -> taba-core
Observe needs unit types (for decision trail recording), Artifact, Capability, PromotionPolicy, HealthCheck, Tombstone types for structured events.
taba-observe -> taba-security
Observe calls solver replay which needs SecurityError types. Also needs AuthorId for audit trails. Does NOT perform signing/verification — delegates to taba-security when needed.
taba-erasure -> taba-common
Erasure uses NodeId for shard distribution, config for erasure parameters (resilience percentage, fleet size).
taba-erasure -> taba-security
Post-reconstruction signature re-verification (INV-R1). Calls Verifier trait after decoding shards to ensure reconstructed units have valid signatures.
taba-gossip -> taba-common
Gossip uses NodeId, config for gossip parameters (probe interval, suspicion timeout).
taba-gossip -> taba-security
All gossip messages are signed (INV-R3, DL-009). Gossip calls Signer to sign outgoing messages and Verifier to verify incoming messages. Priority events (key revocation) originate from taba-security.
taba-node -> taba-common
Node uses all identity types, config for node parameters, tracing.
taba-node -> taba-core
Node creates and validates units locally, understands unit lifecycle states.
taba-node -> taba-security
Node uses key management for node identity (Ed25519 keypair), participates in ceremonies, enforces capabilities at runtime.
taba-node -> taba-graph
Node owns the local graph instance, manages WAL, triggers compaction, provides graph snapshots to solver.
taba-node -> taba-solver
Node invokes solver with graph snapshot + membership to compute local placements and reconcile.
taba-node -> taba-erasure
Node stores and retrieves erasure-coded shards, triggers reconstruction when notified of failures.
taba-node -> taba-gossip
Node runs gossip protocol for membership, receives membership changes, disseminates priority events.
taba-cli -> taba-common
CLI uses identity types for display and input parsing, config for connection parameters.
taba-cli -> taba-core
CLI uses unit types for authoring commands, validation for input checking, capability types for display.
taba-cli -> taba-node
CLI connects to the node daemon via gRPC (tonic client) for all operations. This is the primary integration point – CLI is a thin client.
taba-test-harness -> (multiple)
Dev-dependency only. Needs type definitions from common, core, security, graph, and solver to build fakes, builders, and proptest strategies.
Phase mapping
Which crates are needed for which project milestone (from specs/architecture/build-phases.md):
| Milestone | Crates required | Capability |
|---|---|---|
| M1: Types compile | taba-common, taba-core, taba-test-harness | Unit declarations parse and validate |
| M2: Single-node compose | + taba-security, taba-graph, taba-solver | Compose units on one node, signed, with conflict detection |
| M3: Persistent | + taba-node (WAL only) | Survives restart, local reconciliation |
| M4: Multi-node | + taba-gossip, taba-erasure | Distributed operation, failure tolerance |
| M5: Usable | + taba-cli | Human-operable system |
| M6: Hardened | taba-security (advanced features) | Shamir Tier 2/3, TPM, SLSA |
| M7: Migration | + k8s migration tool (new crate) | K8s users can onboard |
Phase-crate matrix
M1 M2 M3 M4 M5 M6 M7
common x x x x x x x
core x x x x x x x
security x x x x X x (X = advanced features)
graph x x x x x x
solver x x x x x x
observe x x x x x x (decision trails from M2)
erasure x x x x
gossip x x x x
node x x x x x
cli x x x
test-harness x x x x x x x (dev-only, grows with each phase)
Design notes
Why taba-solver does not depend on taba-graph
The solver reads GraphSnapshot, which is a type defined in taba-graph. This
creates a dependency edge from taba-solver to taba-graph. However, this can
be avoided by defining the snapshot trait/type in taba-core (as part of the
domain model) and having both taba-graph (implements it) and taba-solver
(consumes it) depend on taba-core. This keeps the DAG cleaner.
Decision: taba-solver depends on taba-core for the GraphSnapshot type.
taba-graph implements the snapshot. The solver never imports taba-graph
directly. This allows graph and solver to build in parallel at Level 3.
Why taba-erasure does not depend on taba-graph
Erasure coding operates on opaque byte slices. It does not need to understand graph structure. taba-graph serializes subgraphs to bytes before handing them to taba-erasure for encoding. taba-node orchestrates this interaction.
Why taba-gossip does not depend on taba-graph
Gossip is a transport and membership protocol. It carries graph deltas as opaque payloads. The graph deserialization and merge happen in taba-node after gossip delivers the message.
Cross-crate trait pattern
Several traits are defined in one crate and implemented in another:
GraphSnapshottrait: defined in taba-core, implemented in taba-graph. Wrapped in Arc for concurrent immutable access (A006).MembershipSnapshottype: defined in taba-core, populated by taba-gossip.Waltrait: defined in taba-core, implemented in taba-node (disk) and taba-test-harness (in-memory). taba-graph CALLS Wal, never implements it. This avoids a circular dependency (A003).CompositionGraphtrait: defined in taba-graph, implemented thereSolvertrait: defined in taba-solver, implemented thereSigner/Verifiertraits: defined in taba-security, implemented thereShardStoretrait: defined in taba-node, implemented there and in taba-test-harness (in-memory)MembershipServicetrait: defined in taba-gossip, implemented there and in taba-test-harness (fake)
This pattern enables testing without full dependency chains: taba-test-harness provides in-memory implementations of all traits, allowing any crate to be tested in isolation.
Build Order
Dependency DAG
taba-common (foundation: types, config, protobuf)
↓
taba-core (unit type system, capabilities, contracts)
↓
taba-security (signing, verification, capability enforcement)
↓
taba-graph (CRDT composition graph, operations, merge)
↓
taba-solver (composition resolution, placement, conflicts)
↓
taba-erasure (erasure coding, shard management)
↓
taba-gossip (membership, failure detection)
↓
taba-node (per-node daemon, reconciliation, WAL)
↓
taba-cli (CLI binary, user interface)
taba-test-harness (parallel: shared test utilities, no prod dependencies)
Implementation phases
Phase 1: Foundation (single-node, in-memory)
Build the core abstractions and prove they work on a single node.
1a. taba-common
- Shared types: NodeId, UnitId, AuthorId, TrustDomainId
- Configuration structures (TOML parsing)
- Protobuf definitions and generated bindings
- Tracing setup
1b. taba-core
- Unit type definitions (workload, data, policy, governance)
- Capability declaration model
- Unit validation (well-formedness checks)
- Capability matching algorithm
- Contract types (needs, provides, tolerates, trusts)
- Unit serialization/deserialization
1c. taba-security (initial)
- Ed25519 signing and verification
- Unit signing (author signs unit declaration)
- Capability enforcement (check declared vs allowed)
- Taint propagation rules
1d. taba-test-harness
- Unit builders (test data factories)
- In-memory fakes for graph and solver
- Property test strategies for core types
Phase 2: Graph and Solver (single-node, persistent)
Build the composition engine.
2a. taba-graph
- CRDT graph data structure
- Graph operations: insert unit, compose, query
- Merge semantics (commutative, associative, idempotent)
- Signature verification on merge
- Provenance chain tracking
- Hierarchical data unit support
- WAL integration for persistence
2b. taba-solver
- Composition resolution (match capabilities to needs)
- Conflict detection (security, ambiguity)
- Deterministic placement (given graph + nodes → placement)
- Policy application (resolve conflicts using policy units)
- Scaling computation (when to add/remove instances)
Phase 3: Distribution
Make it work across multiple nodes.
3a. taba-gossip
- SWIM-based membership protocol
- Node join/leave handling
- Failure detection (suspicion, confirmation)
- Membership view dissemination
3b. taba-erasure
- Reed-Solomon (or similar) erasure coding
- Graph shard distribution across nodes
- Shard reconstruction on node failure
- Dynamic re-coding when fleet size changes
3c. taba-node
- Per-node daemon (the main binary)
- Local reconciliation loop (desired vs actual)
- WAL management
- Graph shard storage
- Gossip participation
- Health reporting
Phase 4: User interface
Make it usable.
4a. taba-cli
- Unit authoring commands
- Composition commands
- Status and inspection
- Policy management
- Trust domain management
- Audit and lineage queries
Phase 5: Hardening
5a. taba-security (advanced)
- TPM attestation (optional, feature-gated)
- Shamir secret sharing for root key
- Node enrollment ceremony
- Build provenance verification (SLSA)
5b. K8s migration tool
- Read K8s manifests (Deployment, Service, ConfigMap, Secret, etc.)
- Generate taba unit declarations
- Surface conflicts and unmappable constructs
Parallelism opportunities
- taba-test-harness can be built alongside any phase
- taba-gossip and taba-erasure are independent of each other
- taba-cli can start as soon as taba-core exists (building up as features land)
- Property tests can be written as soon as types exist
Milestones
| Milestone | Crates | Capability |
|---|---|---|
| M1: Types compile | common, core | Unit declarations parse and validate |
| M2: Single-node compose | + graph, solver, security | Compose units on one node |
| M3: Persistent | + node (WAL only) | Survives restart |
| M4: Multi-node | + gossip, erasure | Distributed operation |
| M5: Usable | + cli | Human-operable |
| M6: Hardened | + security advanced | Production-grade security |
| M7: Migration | + k8s tool | K8s users can onboard |
Testing Strategy
Test pyramid
/ E2E \ Few, slow, high-value
/ Integr. \ Cross-crate, multi-component
/ Property \ Invariant verification, 10k+ cases
/ Unit tests \ Fast, isolated, per-function
/__________________\ Many, fast, foundation
Unit tests
- Location:
#[cfg(test)] mod testsin each source file - Scope: single function or small group of related functions
- Dependencies: none external. Use in-memory fakes.
- Speed: entire unit test suite completes in < 30 seconds
- Coverage target: 80%+ line coverage on non-trivial code
Property-based tests
- Framework:
proptest - Location: dedicated test modules, or
tests/properties/per crate - When to use: any code that maintains an invariant. Specifically:
- CRDT merge operations (commutativity, associativity, idempotency)
- Solver placement (determinism: same input = same output)
- Capability matching (reflexivity, no false positives, no false negatives)
- Erasure coding (encode then decode = original)
- Graph operations (insert then query = found)
- Taint propagation (PII in → PII out unless explicit policy)
- Minimum cases: 10,000 per property
- Shrinking: enable for all properties (default in proptest)
BDD / Acceptance tests
- Format: Gherkin feature files in
specs/features/ - Step definitions: Rust code in
tests/bdd/or per-cratetests/ - Framework:
cucumber-rs(Rust Cucumber implementation) - Scope: behavioral requirements from specs
- Each feature file maps to a user-visible capability
Integration tests
- Location:
tests/integration/per crate, or top-leveltests/ - Scope: cross-crate interactions
- May use real networking (localhost), real disk I/O
- Docker/containers for multi-node tests when needed
- Speed target: full integration suite < 5 minutes
Chaos / resilience tests
- For multi-node scenarios only (later implementation phases)
- Node failure injection (kill process, drop network)
- Partition simulation (iptables rules or network namespaces)
- Clock skew injection
- Disk full / slow disk simulation
- Framework: custom test harness or
toxiproxy
What to test per crate
taba-core
- Unit validation: well-formed declarations accepted, malformed rejected
- Capability matching: correct matches, no false positives
- Type system: type checking of unit declarations
- Property: for all valid units u, validate(serialize(deserialize(u))) == Ok
taba-graph
- CRDT operations: merge commutativity, associativity, idempotency
- Graph queries: traverse, filter, provenance chains
- Signature verification: reject unsigned/wrongly-signed entries
- Property: for all operations a, b: merge(apply(a), apply(b)) == merge(apply(b), apply(a))
taba-solver
- Placement: deterministic, respects constraints
- Conflict detection: finds all conflicts, no false negatives
- Policy resolution: applies policies correctly
- Property: for all graph states g, nodes n: solve(g, n) on node_1 == solve(g, n) on node_2
taba-node
- Local reconciliation: converges to desired state
- WAL: crash recovery preserves state
- Drift detection: detects all divergences
taba-gossip
- Membership: join, leave, failure detection
- Convergence: all nodes agree on membership eventually
- Property: in absence of failures, membership converges within bounded time
taba-erasure
- Encode/decode roundtrip
- Reconstruction from minimum shards
- Failure with too few shards
- Property: for all data d, k shards of n: decode(any_k_of(encode(d, n, k))) == d
taba-security
- Capability enforcement: declared only, deny by default
- Signature operations: sign, verify, reject tampered
- Taint propagation: correct inheritance and narrowing
- Property: for all units without explicit capability c: access(c) == Denied
Test data management
- Use
taba-test-harnesscrate for shared fixtures, builders, and fakes - Builder pattern for test data:
UnitBuilder::workload().with_capability("postgres").build() - No test data hardcoded in multiple places — centralize in harness
- Property tests generate their own data via
propteststrategies
Enforcement Map
Maps every invariant to the crate, trait method, runtime check, timing, and violation response that enforces it.
Covers: INV-S1–S10, INV-S8a, INV-C1–C7, INV-K1–K5, INV-D1–D5, INV-R1–R6, INV-E1–E3, INV-N1–N5, INV-A1–A2, INV-O1–O3, INV-T1–T3, INV-W1–W4a, INV-G1–G5, INV-X1–X6. (59 invariants total.)
Legend
- Crate: which taba crate owns the enforcement logic.
- Trait::method: the trait and method where the check executes.
- Check: what the code does at runtime.
- When: the lifecycle event that triggers the check.
- On Violation: the error variant raised and recovery action taken.
Security Invariants (INV-S1 through INV-S10)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-S1: Zero-default capabilities | taba-security | CapabilityGuard::check_access(unit, capability) | Verify the unit’s declared capabilities include the requested capability AND an approved policy exists granting it. Empty capability set = no access. | On every capability access request (runtime, before workload I/O) | SecurityError::CapabilityDenied. Access blocked. No fallback. |
| INV-S2: Security conflicts fail closed | taba-solver | Solver::detect_conflicts(composition) | Scan all capability declarations in a composition for incompatible pairs. If any pair is incompatible and no policy resolves it, refuse composition. | On composition (solver evaluation) | SolverError::ConflictDetected / SecurityError::SecurityConflictUnresolved. Composition refused. Requires policy unit creation. |
| INV-S3: Signed units, verified before merge | taba-security, taba-graph | SignatureVerifier::verify(unit) called by Graph::merge(unit) | (a) Verify Ed25519 signature over (unit || trust_domain_id || cluster_id || validity_window). (b) Confirm author had valid scope at creation time. (c) Confirm author’s key was not revoked before unit’s creation timestamp. Synchronous – blocks merge until complete. | On merge (before unit enters graph state) | SecurityError::SignatureInvalid, SecurityError::AuthorKeyRevoked, SecurityError::ValidityWindowExpired. Unit rejected. Not added to graph or WAL. |
| INV-S4: Taint propagation | taba-core, taba-security | TaintResolver::compute_taint(data_unit) | Traverse provenance graph from the queried data unit back through all inputs. Compute union (most restrictive) of all input classifications. Compare against the unit’s declared classification. | On query (not merge). Taint is eventually consistent across nodes. | SecurityError::TaintWidening if classification weaker than computed taint without policy. Query returns the computed (restrictive) classification regardless. |
| INV-S5: Author scope enforcement | taba-security | ScopeValidator::validate_author_scope(author, unit) | Check that author’s (unit_type_scope, trust_domain_scope) includes the unit’s type and trust domain. | On unit creation and on merge (pre-merge gate, part of INV-S3 verification) | SecurityError::AuthorScopeExceeded. Unit rejected. |
| INV-S6: Multi-party trust domain creation | taba-security | MultiPartyValidator::validate_signers(governance_unit, required_count) | Count distinct author signatures on the trust domain creation governance unit. Require >= 2. | On trust domain creation (before merge of governance unit) | SecurityError::TrustDomainCreationRequiresMultiParty. Creation refused. |
| INV-S7: Data hierarchy narrowing/widening | taba-core, taba-security | ClassificationValidator::validate_hierarchy(child, parent) | Compare child classification against parent using lattice (public < internal < confidential < PII). Narrowing (child more restrictive) always allowed. Widening (child less restrictive) requires an explicit declassification policy. | On data unit insertion (child added to parent) | SecurityError::TaintWidening. Child rejected unless policy exists. |
| INV-S8: Unique author scopes | taba-security | ScopeValidator::check_uniqueness(author, scope, existing_assignments) | Before persisting a role assignment governance unit, scan all existing role assignments. Reject if any other author already holds the identical (unit_type_scope, trust_domain_scope) tuple. | On role assignment creation (before merge of governance unit) | SecurityError::ScopeDuplicate. Role assignment rejected. |
| INV-S9: Multi-party declassification | taba-security | MultiPartyValidator::validate_declassification(policy) | Count distinct signers on the declassification policy. Require >= 2: one with policy scope, one with data-steward scope. | On declassification policy creation (before merge) | SecurityError::DeclassificationRequiresMultiParty. Policy rejected. |
| INV-S10: Multi-party trust domain (threshold) | taba-security | MultiPartyValidator::validate_signers(governance_unit, required_count) | Same mechanism as INV-S6. Verify the policy unit lists required signers and all required cryptographic signatures are present. | On trust domain creation (before merge) | SecurityError::TrustDomainCreationRequiresMultiParty. Creation refused. |
Consistency Invariants (INV-C1 through INV-C7)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-C1: Graph is single source of desired state | taba-graph, taba-core | Graph::query(predicate) (architectural – no other state store exists) | Structural enforcement: no alternative desired-state store is implemented. All desired-state reads go through Graph::query. Solver reads only from graph. | Always (architectural constraint) | N/A – violation is a design bug, not a runtime error. Code review and architecture enforcement. |
| INV-C2: CRDT merge properties | taba-graph | CrdtMerge::merge(local_state, remote_state) | Property-based tests verify commutativity (merge(A,B) == merge(B,A)), associativity, and idempotency. Runtime: merge is implemented as set union with LWW timestamps – algebraically guarantees properties. | On every merge (gossip reconciliation, peer sync) | GraphError::CrdtViolation. Internal bug. Merge aborted. Alert raised. Node should not apply partial merge. |
| INV-C3: Solver determinism (fixed-point, no float) | taba-solver | Solver::compute_placement(graph_state, membership) | All arithmetic uses Ppm/SignedPpm (u64/i64 fixed-point). Tiebreaker: lexicographically lowest NodeId. Compile-time lint: #[deny(clippy::float_arithmetic)] on solver crate. Runtime: hash solver output and compare across nodes via gossip. | On placement computation. Cross-node verification via gossip protocol. | SolverError::DeterminismViolation, SolverError::FloatingPointDetected. Placement paused. Critical alert. |
| INV-C4: WAL-before-effect | taba-graph | Wal::append(entry) called before Graph::apply(mutation) | WAL entry (Merged/Pending/Promoted) is fsync’d to disk before the in-memory graph state is updated. If WAL write fails, mutation is not applied. | On every mutation (insert, promote, merge) | GraphError::WalWriteFailed. Mutation rejected. Node stops accepting new mutations until WAL is writable. |
| INV-C5: Policy references existing conflicts | taba-core | PolicyValidator::check_references(policy) | On query: verify that the conflict tuple referenced by the policy still exists in the graph. Orphaned policies (referencing non-existent conflicts) are flagged for archival. | On query (not merge). Validity checked lazily. | CoreError::OrphanedPolicy. Policy ignored in solver decisions. Eligible for archival. |
| INV-C6: Insertion-order independence | taba-solver, taba-graph | Solver::compute_composition(units) | Solver re-evaluates all affected compositions on any unit addition. Capability lists sorted lexicographically before matching (INV-K2). Property tests verify same output regardless of insertion order. | On composition (after any unit insert/merge) | GraphError::InsertionOrderDependence. Internal bug. Critical alert. |
| INV-C7: Single non-revoked policy per conflict | taba-core | PolicyValidator::check_uniqueness(policy, conflict_tuple) | Before accepting a new policy for a conflict tuple, check if a non-revoked policy already exists. If yes, the new policy must have a supersedes field referencing the existing one. Solver uses latest non-revoked version in the chain. | On policy creation/merge | CoreError::PolicyConflict. New policy rejected unless it explicitly supersedes existing. |
Composition Invariants (INV-K1 through INV-K5)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-K1: All capability needs satisfied | taba-solver | Solver::match_capabilities(composition) | For every needs declaration in the composition, find a matching provides with compatible type. Unmatched needs cause composition failure. | On composition (solver evaluation) | SolverError::CapabilityUnsatisfied, CoreError::CompositionFailed. Composition refused. |
| INV-K2: Typed capability matching | taba-solver | CapabilityMatcher::matches(need, provide) | Compare (type, name, purpose?) tuples. Type must be compatible (exact or subtype). Name must match. If purpose is declared on either side, it must match (purpose mismatch triggers conflict requiring policy). Capabilities sorted lexicographically before matching. | On composition (during capability matching) | CoreError::CapabilityTypeMismatch. If purpose mismatch: SolverError::ConflictDetected. |
| INV-K3: Placement respects tolerances | taba-solver | Solver::score_placement(unit, node) | Evaluate unit tolerance declarations (latency budget, failure mode, resource requirements) against each candidate node’s reported capabilities. Node must satisfy all constraints. | On placement (after composition) | CoreError::PlacementConstraintViolation, SolverError::NoViableNode. Unit not placed. Retryable if nodes join. |
| INV-K4: Scaling from unit-declared parameters | taba-solver | Solver::compute_scaling(unit) | Scaling (min/max instances, triggers) read exclusively from unit declaration. Solver does not invent or infer scaling logic. Validate that min <= max, triggers are well-formed. | On placement and periodic re-evaluation | CoreError::ScalingParameterInvalid. Unit flagged. No scaling applied until fixed. |
| INV-K5: Cyclic recovery dependencies fail closed | taba-solver | Solver::detect_cycles(recovery_graph) | Build directed graph of recovery dependencies. Run cycle detection (Tarjan’s or DFS). If cycle found, require explicit policy declaring restart priority. Tiebreaker without policy: lexicographically lowest UnitId gets priority. | On composition (after recovery dependency resolution) | CoreError::CyclicRecoveryDependency, SolverError::CyclicDependency. Composition refused until policy breaks cycle. |
Data Invariants (INV-D1 through INV-D3)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-D1: Unbroken provenance chain | taba-core | ProvenanceValidator::validate_chain(data_unit) | Every data unit produced by a workload must reference its input data units and producing workload. On query, traverse provenance links and verify all referenced units exist. On merge, units with unsatisfied references enter pending queue (causal buffering per INV-C4). | On query (full validation). On merge (reference check, causal buffering). | CoreError::ProvenanceChainBroken. On merge: unit enters pending state. On query: provenance gap flagged. |
| INV-D2: Retention enforcement | taba-core | RetentionEnforcer::check_expiry(data_unit) | Compare data unit’s retention duration against current time. Expired units are marked eligible for compaction. Compaction is not optional. | Periodic (compaction sweep) and on query | CoreError::RetentionExpired. Unit eligible for compaction. Compactor removes from active graph. |
| INV-D3: No redundant children | taba-core | HierarchyValidator::check_redundancy(child, parent) | Compare child constraints against parent constraints. If identical, child is redundant and should not exist. Also enforce max hierarchy depth (16 levels). | On data unit insertion (child creation) | CoreError::RedundantChild, CoreError::HierarchyDepthExceeded. Child rejected. |
Resilience Invariants (INV-R1 through INV-R6)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-R1: Node failure does not corrupt graph | taba-erasure | ErasureCodec::reconstruct(shards), ReconstructionScheduler::schedule(priority_queue) | Reconstruct graph shards from surviving erasure-coded shards. After reconstruction, re-verify all unit signatures (via SignatureVerifier::verify). Backpressure: throttled rate, prioritized by shard criticality (governance > policy > data constraints > workload). Circuit breaker when queue depth exceeds threshold. | On node failure detection (from gossip) | ErasureError::ReconstructionFailed, ErasureError::ReconstructionBackpressure, ErasureError::SignatureVerificationPostReconstruct. If below threshold: degraded mode. |
| INV-R2: Partition consistency via CRDT | taba-graph, taba-gossip | CrdtMerge::merge(local, remote), PartitionResolver::resolve_duplicates(placements) | Both partition sides maintain graph via CRDT. On heal: merge produces correct state (INV-C2). Duplicate placements resolved by deterministic tiebreaker (lowest NodeId wins, INV-C3). Loser drains. Role-carrying units disabled on minority side. | On partition heal (gossip reconnection and merge) | Duplicate placement: loser receives drain signal. Role units on minority side: NodeError::DegradedMode for affected operations. |
| INV-R3: Gossip convergence with signed messages | taba-gossip | GossipProtocol::verify_message(msg), FailureDetector::declare_failure(node_id, witnesses) | All gossip messages verified with sending node’s Ed25519 key. Membership state changes (node declared failed) require corroboration from >= 2 independent witnesses (configurable via ClusterConfig::witness_count). | On every gossip message receipt. On failure declaration. | GossipError::MessageSignatureInvalid (message dropped). GossipError::InsufficientWitnesses (failure declaration deferred until witnesses met). |
| INV-R4: Shard reconstructability threshold | taba-erasure | ErasureCodec::check_threshold(available_shards, k, n) | Compute k = ceil(N * (1 - R/100)). If actual failures > floor(N - k), enter degraded mode and surface operator alert. | On node failure detection and periodically during health checks | ErasureError::ThresholdExceeded, ErasureError::InsufficientShards. System enters degraded mode. Operator alert. |
| INV-R5: Suspected nodes stay in placement pool | taba-gossip, taba-solver | Solver::score_placement(unit, node) with NodeHealth::Unknown handling | Suspected nodes remain in pool with health=‘unknown’. Solver deprioritizes them (lower score) but does not remove. Only SWIM multi-probe consensus confirms failure and triggers removal. | On placement (solver considers suspected nodes as last resort). On gossip protocol probe cycle. | No error – behavioral: solver avoids suspected nodes when alternatives exist. GossipError::NodeUnreachable tracks probe failures. |
| INV-R6: Graph memory limit with auto-compaction | taba-graph, taba-node | MemoryMonitor::check_usage(graph), Compactor::compact(graph, target_bytes) | Monitor active graph memory. At 80% of ClusterConfig::graph_memory_limit_bytes, trigger auto-compaction. At 100%, node enters degraded mode: refuses new placements. Governance units are fully replicated (not just erasure-coded). | Periodic (memory monitor interval). On every graph mutation (lightweight size check). | GraphError::MemoryLimitExceeded, NodeError::DegradedMode. At 80%: compaction runs. At 100%: new placements refused until compaction completes. |
Environment & Promotion Invariants (INV-E1 through INV-E3)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-E1: Promotion policy gates placement by env | taba-solver | PromotionEvaluator::evaluate(unit, promotions, gates), CapabilityFilter::filter(unit, nodes, promotions) | Workload can only be placed on nodes whose env tag matches a promotion policy. Exception: env:dev uses author affinity (no promotion needed). | On placement (solver evaluation) | SolverError::NoCapableNode with detail “no promotion policy for env:X”. Unit not placed. |
| INV-E2: Promotions are cumulative | taba-solver | PromotionEvaluator::evaluate() | Promotion to env:prod does NOT remove from env:test. Environments are independent targets. Solver evaluates each environment independently. | On placement (solver evaluation) | Structural — solver evaluates each env separately. No violation error. |
| INV-E3: No PromotionGate = all auto | taba-solver | PromotionEvaluator::evaluate() | If no PromotionGate governance unit exists, all transitions default to auto-promote. Zero config = full auto. | On promotion evaluation | Structural — default behavior when governance unit absent. |
Node Capability Invariants (INV-N1 through INV-N5)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-N1: Auto-discovery on startup, cached | taba-node | CapabilityDiscoverer::discover(), CapabilityDiscoverer::refresh() | Node probes system for capabilities at startup. Cache is authoritative until refresh. Fleet refresh via governance OperationalCommand. | On node startup. On taba refresh. On fleet RefreshCapabilities command. | NodeError::CapabilityDiscoveryFailed. Node starts with empty capability set (no workloads placed). |
| INV-N2: Capabilities are hard constraints | taba-solver | CapabilityFilter::filter(unit, nodes, promotions) | Binary match: artifact.type must match node runtime capability. No fallback. | On placement (solver capability filter) | SolverError::NoCapableNode. Unit not placed. |
| INV-N3: Resources are soft constraints | taba-solver | ResourceRanker::rank(unit, nodes, resources) | Among capability-matched nodes, rank by resource availability. Ppm arithmetic, versioned snapshots. | On placement (after capability filter) | No error — ranking always produces an ordering. Worst-fit node still valid if capabilities match. |
| INV-N4: Custom tags match like capabilities | taba-solver | CapabilityFilter::filter() | Freeform key:value tags checked identically to auto-discovered capabilities in solver matching. | On placement (capability filter) | Same as INV-N2 — SolverError::NoCapableNode if required tag not found. |
| INV-N5: Placement-on-failure defaults by env | taba-solver, taba-node | Solver::handle_node_failure(unit, env), Reconciler::reconcile() | env:dev defaults to leave-dead. Other envs default to auto-replace. Per-unit placement_on_failure overrides. | On node failure detection (gossip → solver re-evaluation) | Behavioral — not an error. Dev workloads left, prod workloads re-placed. |
Artifact Distribution Invariants (INV-A1 through INV-A2)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-A1: SHA256 digest verification | taba-node | ArtifactFetcher::fetch(ref, digest) | After fetching artifact (from peer or external), compute SHA256 and compare to expected digest. Mismatch = reject, report to graph. | On artifact fetch (before workload execution) | NodeError::ArtifactDigestMismatch. Artifact rejected. Retry from different source. Report to graph. |
| INV-A2: Peer cache first | taba-node | ArtifactFetcher::fetch() | Check peer cache before external source. Optimization, not security boundary — INV-A1 digest verification is the integrity guarantee. | On artifact fetch | Behavioral — if peer has artifact, skip external download. |
Observability Invariants (INV-O1 through INV-O3)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-O1: Every solver run produces decision trail | taba-observe | DecisionTrailRecorder::record(snapshot, membership, result, version) | Node calls recorder after each solver invocation. Trail persisted to graph via WAL. | After every solver run | ObserveError::TrailNotFound if recording fails. Alert. Solver continues (observability failure does not block placement). |
| INV-O2: Trail retention since last compaction | taba-observe, taba-graph | DecisionTrailQuery::query_by_range(), Compactor::compact() | Default retention = since last compaction. Unit decision_retention or governance policy can extend. Compaction respects retention. | On compaction sweep and trail query | ObserveError::TrailCompacted if querying beyond retention. |
| INV-O3: Progressive health checks | taba-node | HealthCheckOrchestrator::run_checks() | Default: OS-level process monitoring. If HealthCheck declared: execute that instead. Node never skips monitoring. | Periodic (configurable interval per workload) | Health failure reported to graph as HealthStatus. Triggers failure semantics (restart, re-place). |
Logical Clock Invariants (INV-T1 through INV-T3)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-T1: Monotonic logical clock, sync on communication | taba-common | LogicalClock::tick(), LogicalClock::sync(remote) | Every system action increments. On inter-node communication: local = max(local, remote) + 1. | On every action and every gossip/merge message | Structural — u64 overflow at 1.8×10^19 events. No practical violation. |
| INV-T2: Dual clock model | taba-common | DualClockEvent triple recorded on every event | Logical clock for ordering, wall clock for retention/compliance. Both recorded. System chooses by operation type. | On every event | Structural — type system enforces recording both clocks. |
| INV-T3: Causal revocation | taba-security | KeyManager::is_revoked_in_local_graph(author) | Check if revocation governance unit exists in local graph. No clock comparison. Optional grace window fallback. | On unit merge (pre-merge gate) | SecurityError::KeyRevoked. Unit rejected. |
Workload Lifecycle Invariants (INV-W1 through INV-W4a)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-W1: Services valid indefinitely | taba-core | UnitHeader::validity = None | No validity window → no automatic expiry. Terminated only explicitly or by drained placement. Key revocation does NOT invalidate existing services. | Structural (type: validity is Option) | N/A — services never auto-expire. |
| INV-W2: Bounded tasks auto-terminate | taba-node | Reconciler::reconcile(), deadline check in reconciliation loop | Check three triggers: completion (exit 0), failure (retries exhausted), deadline (LC range or wall time). Any trigger → terminate. | On task exit, on periodic reconciliation (deadline check) | Task transitions to Terminated. Ephemeral data eligible for removal (INV-D4). |
| INV-W3: Spawn depth enforced at merge | taba-graph, taba-core | Graph::merge() pre-check, SpawnContext::spawn_depth validation | On merge: traverse spawn provenance chain, compute depth. Reject if > max (default 4). | On graph merge of spawned task unit | GraphError::SpawnDepthExceeded. Unit rejected. |
| INV-W4: Delegation token signing | taba-security | DelegationValidator::validate(token, spawned_unit_lc) | Verify: (a) token signature valid, (b) LC range covers spawned task, (c) spawn count ≤ max, (d) token not revoked, (e) parent active. | On graph merge of delegation-signed unit | SecurityError::DelegationExpired, DelegationSpawnLimitExceeded, DelegationTokenForged. Unit rejected. |
| INV-W4a: No governance via delegation | taba-security | DelegationValidator::check_governance_block(token, unit_type) | Spawned tasks cannot create policy, governance, or declassification units. Hard block at merge. | On graph merge of delegation-signed unit with policy/governance type | SecurityError::DelegationGovernanceBlocked. Unit rejected. |
Data Lifecycle Invariants (INV-D4 through INV-D5)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-D4: Ephemeral data reference check | taba-graph | Compactor::remove_ephemeral(data_unit) | On producing task termination: check downstream references. Has refs → tombstone. No refs → full remove. Governance can mandate tombstone for all. | On bounded task termination | Behavioral — no error. Reference check determines treatment. |
| INV-D5: Local-only requires policy for classified data | taba-core, taba-security | RetentionValidator::validate_local_only(data_unit) | If retention = LocalOnly and classification > Public, require explicit policy authorization. | On unit creation / validation | CoreError::LocalOnlyRequiresPolicy. Unit rejected without policy. |
Compaction Invariants (INV-G1 through INV-G5)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-G1: Compaction eligibility deterministic | taba-graph | Compactor::compute_eligible(graph) | Given same graph state, all nodes agree on eligible units. Eligibility derived from graph state only (terminated status, retention expiry, supersession). | On compaction sweep | Structural — same algorithm on same input = same output. |
| INV-G2: Tombstones preserve provenance | taba-graph | Compactor::tombstone(unit) | Tombstone retains UnitId, AuthorId, type, LC range, termination reason, references, original digest. | On compaction (tombstone creation) | Structural — Tombstone struct includes required fields. |
| INV-G3: Governance never compacted | taba-graph | Compactor::is_exempt(unit) | Skip governance units, active policies, root ceremony chain during compaction sweep. | On compaction sweep | Structural — exemption check before tombstoning. |
| INV-G4: Eviction ≠ compaction | taba-node | MemoryManager::evict(unit) | Eviction drops content locally without tombstone. Unit remains live. Content recoverable from peers. | On local memory pressure (node-specific) | Behavioral — eviction is transparent to graph state. |
| INV-G5: Compaction priority order | taba-graph | Compactor::compact(graph, target_bytes) | Priority: ephemeral → trails → terminated tasks → superseded policies → terminated services → expired data. Mirrors reconstruction priority inverse. | On compaction sweep | Structural — priority ordering in compaction algorithm. |
Cross-Trust-Domain Invariants (INV-X1 through INV-X6)
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-X1: Bilateral policy for cross-domain | taba-gossip, taba-solver | CrossDomainGossip::validate_bilateral(query), CapabilityFilter::filter() | Before executing forwarding query, verify bilateral policy in both domains. Missing either side = fail closed. | On cross-domain forwarding query | GossipError::BilateralPolicyMissing. Query rejected. Composition blocked. |
| INV-X2: Read-only cross-domain views | taba-gossip | CrossDomainGossip::forward_query() returns ForwardingResult | Query results NOT merged into querying domain’s graph. Reference by UnitId only. | On forwarding query response | Structural — ForwardingResult is a read-only type, not a mergeable unit. |
| INV-X3: Fail-open cache default | taba-gossip | CrossDomainGossip::query_with_cache(cache_policy) | Default: serve stale if bridge down. Governance override: fail closed for freshness. | On cross-domain query with bridge unavailable | Behavioral — cache served (default) or GossipError::BridgeUnavailable (strict mode). |
| INV-X4: Emergent bridge default | taba-gossip | CrossDomainGossip::discover_bridges(target_domain) | Any node in multiple domains is a bridge. Governance can restrict to designated only. | On bridge discovery | Behavioral — gossip queries all multi-domain nodes (or designated only). |
| INV-X5: Cross-domain capability via governance | taba-gossip | CrossDomainGossip::propagate_advertisement(capability_def) | Bridge nodes relay CrossDomainCapability governance units across boundaries. | On governance unit merge (bridge detects cross-domain advertisement) | Behavioral — bridge gossips the advertisement to other domains. |
| INV-X6: No bridge = unresolved capability | taba-solver, taba-gossip | CapabilityFilter::filter(), CrossDomainGossip::discover_bridges() | If no bridge to target domain, solver surfaces as unresolved. No auto-creation. | On cross-domain composition attempt | SolverError::NoCapableNode with detail “no bridge to domain X”. Alert raised. |
INV-S8a: Overlapping Scopes for Decision-Making Types
| Invariant | Crate | Trait::method | Check | When | On Violation |
|---|---|---|---|---|---|
| INV-S8a: Overlapping policy/governance scopes | taba-security | ScopeValidator::check_uniqueness(author, scope, existing, unit_type) | For policy/governance types: skip uniqueness check (overlapping permitted). For workload/data: enforce INV-S8 (strict uniqueness). | On role assignment creation | Structural — check branches on unit_type. |
Error Taxonomy
Defines error types for every crate in the taba workspace. Each error enum uses
thiserror with typed variants. Every variant has a meaningful name and
sufficient context for diagnosis. Errors propagate with ?. User-facing errors
(surfaced through taba-cli) are actionable.
Conventions
- Error enums are named
{Crate}Error(e.g.,CommonError,CoreError). - Variants carry structured context (IDs, descriptions) not just strings.
#[source]chains preserve the full causal chain.- Retryable means the caller may retry the same operation after a delay.
- Fatal means the operation cannot succeed without external intervention (human action, configuration change, or code fix).
taba-common: CommonError
Foundation types and validation. Wrapped by most other crate errors.
| Variant | Description | Retryable | FM |
|---|---|---|---|
InvalidTimestamp { millis, counter, reason } | Timestamp fields fail validation (e.g., not_before > not_after in ValidityWindow). | Fatal | – |
InvalidPpm { value, reason } | Ppm/SignedPpm overflow or illegal operation. | Fatal | – |
ConfigParse { path, source: toml::de::Error } | TOML config file cannot be parsed. | Fatal | – |
ConfigValidation { field, reason } | Config value out of valid range (e.g., resilience_pct > 100, shamir_threshold > shamir_total_shares). | Fatal | – |
IdParse { input, source } | UUID parse failure for any identity newtype. | Fatal | – |
SerializationError { context, source: bincode::Error } | Wire serialization/deserialization failed. | Fatal | – |
taba-security: SecurityError
Cryptographic operations, signature verification, capability enforcement, and trust domain management.
| Variant | Description | Retryable | FM |
|---|---|---|---|
SignatureInvalid { unit_id, author_id, reason } | Cryptographic signature verification failed. Unit rejected at merge gate (INV-S3). | Fatal | FM-04, FM-05 |
SignatureContextMismatch { unit_id, expected_cluster, actual_cluster } | Unit signed for a different cluster/trust domain (cross-cluster replay). | Fatal | FM-04 |
AuthorKeyRevoked { author_id, revoked_at, unit_created_at } | Author’s key revocation governance unit was merged into local graph before this unit arrived (INV-S3 causal model). | Fatal | FM-05 |
AuthorScopeExceeded { author_id, unit_type, trust_domain } | Author attempted to create a unit outside their (type scope x trust domain scope) (INV-S5). | Fatal | FM-05 |
ScopeDuplicate { author_a, author_b, scope } | Two distinct authors have identical (unit_type_scope, trust_domain_scope) tuples for state-producing types (INV-S8). OK for policy/governance (INV-S8a). | Fatal | – |
DelegationExpired { token_id, lc_range, current_lc } | Delegation token LC range exceeded (INV-W4). | Fatal | – |
DelegationSpawnLimitExceeded { token_id, max, current } | Token’s max_spawns exceeded (INV-W4). | Fatal | – |
DelegationTokenForged { token_id, reason } | Delegation token signature invalid (INV-W4). | Fatal | FM-04 |
DelegationGovernanceBlocked { token_id, attempted_type } | Spawned task attempted governance authority via delegation (INV-W4a). | Fatal | – |
DelegationRevoked { token_id } | Delegation token has been revoked. | Fatal | – |
CapabilityDenied { unit_id, capability, reason } | Unit tried to access a capability it did not declare or that policy denied (INV-S1). | Fatal | – |
SecurityConflictUnresolved { units, capability } | Incompatible capability declarations with no policy resolution. Composition refused (INV-S2). | Fatal | FM-10 |
TaintWidening { data_unit_id, parent_classification, child_classification } | Child data unit attempts to widen (weaken) classification without policy (INV-S7). | Fatal | – |
DeclassificationRequiresMultiParty { policy_id, signer_count } | Declassification policy has fewer than 2 distinct signers (INV-S9). | Fatal | – |
TrustDomainCreationRequiresMultiParty { signers_present } | Trust domain creation has fewer than 2 distinct author signatures (INV-S10, INV-S6). | Fatal | – |
ValidityWindowExpired { unit_id, window } | Unit’s validity window has passed. | Fatal | – |
CeremonyError { ceremony_id, reason } | Shamir key ceremony failure (insufficient shares, wrong threshold, etc.). | Fatal | – |
KeyZeroizeFailed { context } | Zeroize of key material could not be confirmed. Defensive panic candidate. | Fatal | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
SignatureInvalid | Error: Unit {unit_id} has an invalid signature from author {author_id}. The unit was rejected. Verify the signing key and re-sign the unit. |
AuthorKeyRevoked | Error: Author {author_id}'s key was revoked at {revoked_at}. Units created after revocation are not accepted. Re-author with a valid key. |
AuthorScopeExceeded | Error: Author {author_id} is not authorized to create {unit_type} units in trust domain {trust_domain}. Check role assignments. |
SecurityConflictUnresolved | Error: Capability conflict on '{capability}' between units {units}. Create a policy unit to resolve this conflict before composition can proceed. |
TrustDomainCreationRequiresMultiParty | Error: Trust domain creation requires at least 2 distinct author signatures. Only {signers_present} provided. |
taba-core: CoreError
Unit lifecycle, composition, and aggregate management.
| Variant | Description | Retryable | FM |
|---|---|---|---|
UnitNotFound { unit_id } | Referenced unit does not exist in the local graph. | Retryable (may arrive via gossip) | – |
UnitAlreadyExists { unit_id } | Duplicate insertion of the same unit ID. CRDT merge is idempotent, so this is a no-op warning, not a hard error. | Fatal (no-op) | – |
InvalidUnitState { unit_id, current, expected } | Unit state transition is illegal (e.g., Terminated -> Running). | Fatal | – |
CompositionFailed { reason, unresolved_capabilities } | Composition could not satisfy all capability needs (INV-K1). | Fatal | – |
CapabilityTypeMismatch { need, provide, reason } | Typed capability matching failed (INV-K2). | Fatal | – |
CyclicRecoveryDependency { cycle } | Units form a circular recovery dependency chain (INV-K5). | Fatal | – |
PlacementConstraintViolation { unit_id, constraint, reason } | Unit tolerance declarations (latency, failure mode, resources) cannot be met (INV-K3). | Retryable (nodes may join) | FM-06 |
ScalingParameterInvalid { unit_id, reason } | Scaling declaration is malformed or contradictory (INV-K4). | Fatal | – |
HierarchyDepthExceeded { data_unit_id, depth, max } | Data unit exceeds maximum hierarchy depth (16 levels). | Fatal | – |
RedundantChild { child_id, parent_id } | Child data unit has identical constraints to parent (INV-D3). | Fatal | – |
ProvenanceChainBroken { data_unit_id, missing_ref } | Provenance reference cannot be resolved (INV-D1). Pending until ref arrives. | Retryable (causal buffering) | – |
RetentionExpired { data_unit_id, expired_at } | Data unit retention period has elapsed (INV-D2). Eligible for compaction. | Fatal (expected) | – |
PolicyConflict { conflict_tuple, existing_policy, new_policy } | Two non-revoked policies resolve the same conflict (INV-C7). New must supersede. | Fatal | FM-10 |
OrphanedPolicy { policy_id, references } | Policy references a non-existent conflict. Eligible for archival (INV-C5). | Fatal | – |
SpawnDepthExceeded { unit_id, depth, max } | Spawned task exceeds max spawn depth (INV-W3). | Fatal | FM-23 |
LocalOnlyRequiresPolicy { data_unit_id, classification } | Local-only data with classification > Public requires explicit policy (INV-D5). | Fatal | – |
PromotionPolicyCollision { policy_a, policy_b, env } | Two conflicting promotion policies for same env/unit (INV-S8a). | Fatal | FM-14 |
Security { source: SecurityError } | Propagated from taba-security. | Inherits | – |
Common { source: CommonError } | Propagated from taba-common. | Inherits | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
CompositionFailed | Error: Composition failed. Unresolved capabilities: {list}. Ensure all required capability providers are declared and deployed. |
CyclicRecoveryDependency | Error: Circular recovery dependency detected: {cycle}. Add a policy unit declaring restart priority to break the cycle. |
PlacementConstraintViolation | Error: Cannot place unit {unit_id}: constraint '{constraint}' unsatisfiable. {reason}. Consider relaxing tolerances or adding nodes. |
PolicyConflict | Error: Duplicate policy for conflict {conflict_tuple}. New policy must explicitly supersede {existing_policy} via the 'supersedes' field. |
taba-graph: GraphError
CRDT graph operations, merge, query, and WAL persistence.
| Variant | Description | Retryable | FM |
|---|---|---|---|
MergeRejected { unit_id, reason } | Unit failed pre-merge validation (signature, scope, etc.). Wraps SecurityError. | Fatal | FM-04 |
CrdtViolation { operation, reason } | Merge operation would violate commutativity/associativity/idempotency (INV-C2). Internal bug. | Fatal | FM-06 |
WalWriteFailed { path, source: io::Error } | WAL write failed (disk full, I/O error). Node must stop accepting mutations (INV-C4). | Retryable (after disk recovery) | FM-07 |
WalCorrupted { path, offset, reason } | WAL integrity check failed. Node must recover from peers. | Fatal (requires reconstruction) | FM-07 |
WalReplayFailed { entry_index, reason } | WAL entry cannot be replayed during recovery. | Fatal | FM-07 |
PendingRefTimeout { unit_id, missing_refs, waited } | Pending unit’s references never arrived within timeout. | Retryable (extended wait or manual) | – |
MemoryLimitExceeded { current_bytes, limit_bytes } | Active graph exceeds configured memory limit (INV-R6). Node enters degraded mode. | Retryable (after compaction) | FM-08 |
CompactionFailed { reason } | Auto-compaction could not free sufficient memory. | Fatal | FM-08 |
InsertionOrderDependence { unit_ids, reason } | Detected composition result differing by insertion order (INV-C6). Internal bug. | Fatal | FM-06 |
QueryError { query, reason } | Graph query failed (malformed query, internal state inconsistency). | Fatal | – |
SpawnDepthExceeded { unit_id, depth, max } | Spawned task depth exceeds max at merge (INV-W3). | Fatal | FM-23 |
ArchivalFailed { unit_id, backend, reason } | Archive write failed for mandatory-archive unit (FM-21). Compaction blocked. | Retryable | FM-21 |
Security { source: SecurityError } | Propagated from taba-security (signature verification at merge gate). | Inherits | – |
Core { source: CoreError } | Propagated from taba-core (unit validation). | Inherits | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
WalWriteFailed | Error: Cannot write to WAL at {path}: {source}. Node is pausing mutations. Check disk space and filesystem health. |
WalCorrupted | Error: WAL corrupted at {path} offset {offset}. Node will recover graph state from peers. Manual intervention may be required. |
MemoryLimitExceeded | Warning: Graph memory usage ({current_bytes} bytes) exceeds limit ({limit_bytes} bytes). Node entering degraded mode. Compaction in progress. |
taba-solver: SolverError
Deterministic placement, capability matching, and conflict detection.
| Variant | Description | Retryable | FM |
|---|---|---|---|
DeterminismViolation { input_hash, node_a_result, node_b_result } | Two evaluations of the same input produced different output (INV-C3). Critical bug. | Fatal | FM-06, FM-11 |
FloatingPointDetected { operation } | Floating-point arithmetic detected in solver path. Compile-time lint preferred, runtime backstop (INV-C3). | Fatal | FM-11 |
NoViableNode { unit_id, constraints } | No node in the membership satisfies placement constraints (INV-K3). | Retryable (nodes may join) | – |
CapabilityUnsatisfied { unit_id, need, available_provides } | No provider matches a required capability (INV-K1, INV-K2). | Fatal | – |
ConflictDetected { units, capability, conflict_type } | Incompatible declarations found. Requires policy resolution (INV-S2). | Fatal (awaits policy) | FM-10 |
VersionSkew { local_version, peer_versions } | Not all nodes report the same solver version. Placement paused (FM-12). | Retryable (after upgrade completes) | FM-12 |
ScalingComputationFailed { unit_id, reason } | Unit-declared scaling parameters are internally contradictory (INV-K4). | Fatal | – |
CyclicDependency { cycle } | Recovery dependency cycle detected (INV-K5). | Fatal | – |
Graph { source: GraphError } | Propagated from taba-graph. | Inherits | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
NoViableNode | Error: No node can satisfy placement constraints for unit {unit_id}: {constraints}. Add nodes with the required capabilities or relax unit tolerances. |
ConflictDetected | Error: Capability conflict on '{capability}' between units {units}. Create a policy unit to resolve this conflict. |
VersionSkew | Warning: Solver version mismatch across cluster. Placement paused until all nodes are at the same version. Local: {local_version}, peers: {peer_versions}. |
taba-erasure: ErasureError
Erasure coding, shard management, and reconstruction.
| Variant | Description | Retryable | FM |
|---|---|---|---|
InsufficientShards { available, required_k, total_n } | Not enough shards to reconstruct. System enters degraded mode (INV-R4). | Retryable (shards may arrive) | FM-02 |
ShardCorrupted { shard_id, node_id, reason } | Shard integrity check failed. Must fetch replacement from peers. | Retryable | FM-01, FM-02 |
ReconstructionFailed { unit_ids, reason } | Shard reconstruction could not complete. | Fatal (if below threshold) | FM-02, FM-13 |
ReconstructionBackpressure { queue_depth, threshold } | Reconstruction queue exceeded circuit breaker threshold (INV-R1). New reconstructions paused. | Retryable (after queue drains) | FM-13 |
EncodingFailed { unit_id, reason } | Erasure encoding of a unit/shard failed. | Fatal | – |
ThresholdExceeded { failures, max_tolerable } | Actual failures exceed floor(N - k). Degraded mode (INV-R4). | Fatal (operator intervention) | FM-02 |
SignatureVerificationPostReconstruct { unit_id, reason } | Reconstructed unit fails signature re-verification (INV-R1). Data integrity issue. | Fatal | FM-04 |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
InsufficientShards | Error: Only {available} of {required_k} required shards available (of {total_n} total). System entering degraded mode. Restore failed nodes to recover. |
ReconstructionBackpressure | Warning: Reconstruction queue depth ({queue_depth}) exceeds threshold ({threshold}). Pausing new reconstructions to prevent cascade. Monitor node health. |
ThresholdExceeded | Critical: {failures} node failures exceed erasure tolerance ({max_tolerable}). Data loss risk. Immediate operator intervention required. |
taba-gossip: GossipError
SWIM-based membership protocol, peer communication, and failure detection.
| Variant | Description | Retryable | FM |
|---|---|---|---|
MessageSignatureInvalid { from_node, reason } | Gossip message failed signature verification (INV-R3). Dropped. | Fatal (for this message) | FM-04 |
InsufficientWitnesses { node_id, witnesses, required } | Node failure declaration lacks required corroboration (INV-R3). | Retryable (more witnesses needed) | FM-09 |
NodeUnreachable { node_id, attempts, last_error } | Peer not responding to direct or indirect probes. | Retryable (SWIM protocol continues) | FM-01 |
FalsePositiveDetected { node_id } | A previously declared-dead node has rejoined. Graph/placement recovery needed. | Retryable (auto-recovery) | FM-09 |
BootstrapFailed { seed_nodes, reason } | Cannot contact any seed node during initial join. | Retryable (after network check) | – |
VersionAnnounceMismatch { node_id, solver_version } | Peer announces a different solver version. Flagged for version skew handling (FM-12). | Retryable (during upgrade) | FM-12 |
PartitionDetected { reachable_nodes, unreachable_nodes } | Gossip topology suggests network partition (FM-03). Informational. | Retryable (heal expected) | FM-03 |
BilateralPolicyMissing { consuming_domain, providing_domain } | Cross-domain forwarding rejected: no bilateral policy (INV-X1). | Fatal | FM-25 |
BridgeUnavailable { target_domain } | No bridge available to target domain (INV-X6). | Retryable (bridge may join) | FM-25 |
FleetCommandRateLimited { command_type, retry_after_lc } | Fleet command rate limit exceeded (F-A314). | Retryable | – |
Security { source: SecurityError } | Propagated from taba-security (message signature verification). | Inherits | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
BootstrapFailed | Error: Cannot reach any seed node ({seed_nodes}). Check network connectivity and seed node configuration. |
PartitionDetected | Warning: Possible network partition. {reachable_nodes} reachable, {unreachable_nodes} unreachable. CRDT merge will reconcile on heal. |
InsufficientWitnesses | Info: Node {node_id} suspected but only {witnesses}/{required} witnesses confirm. Awaiting corroboration before declaring failure. |
taba-node: NodeError
Node lifecycle, local state management, operational mode transitions.
| Variant | Description | Retryable | FM |
|---|---|---|---|
AlreadyInState { node_id, state } | Node state transition to current state. No-op. | Fatal (no-op) | – |
IllegalStateTransition { node_id, from, to } | Invalid node state machine transition (e.g., Failed -> Active without Joining). | Fatal | – |
DegradedMode { reason } | Node entered degraded mode. New placements refused (INV-R6, INV-R4). | Retryable (after recovery) | FM-02, FM-08 |
RecoveryInProgress { progress_pct } | Operation rejected because node is in recovery mode. | Retryable (after recovery) | – |
AttestationFailed { node_id, reason } | TPM or identity attestation failed during join. | Fatal | – |
LocalStateDiverged { expected_hash, actual_hash } | Local actual state does not match desired state from solver. Reconciliation needed. | Retryable (reconciliation) | FM-04 |
Graph { source: GraphError } | Propagated from taba-graph. | Inherits | – |
Gossip { source: GossipError } | Propagated from taba-gossip. | Inherits | – |
Erasure { source: ErasureError } | Propagated from taba-erasure. | Inherits | – |
Solver { source: SolverError } | Propagated from taba-solver. | Inherits | – |
ArtifactFetchFailed { artifact_ref, reason } | Artifact could not be fetched from any source (FM-15). | Retryable | FM-15 |
ArtifactDigestMismatch { expected, actual } | Fetched artifact SHA256 does not match expected digest (INV-A1). | Fatal | FM-15 |
CapabilityDiscoveryFailed { reason } | Auto-discovery could not probe system capabilities (INV-N1). | Retryable | FM-17 |
SpawnFailed { service_id, reason } | Bounded task spawn failed (delegation or depth). | Fatal | FM-22, FM-23 |
Observe { source: ObserveError } | Propagated from taba-observe. | Inherits | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
DegradedMode | Warning: Node entering degraded mode: {reason}. New placements suspended. Resolve the underlying issue and the node will recover automatically. |
AttestationFailed | Error: Node attestation failed: {reason}. Node cannot join the cluster. Verify TPM configuration or identity key. |
taba-cli: CliError
Top-level user-facing errors. Wraps all lower-level errors and translates them into actionable messages.
| Variant | Description | Retryable | FM |
|---|---|---|---|
Node { source: NodeError } | Any error propagated from node operations. | Inherits | – |
Security { source: SecurityError } | Security errors surfaced directly (e.g., ceremony commands). | Inherits | – |
Core { source: CoreError } | Core errors surfaced directly (e.g., unit creation commands). | Inherits | – |
InvalidArgument { flag, reason } | CLI argument validation failed. | Fatal | – |
ConnectionFailed { addr, source: io::Error } | Cannot connect to the local or remote node. | Retryable | – |
Timeout { operation, duration } | Operation timed out. | Retryable | – |
OutputFormatError { format, reason } | Cannot serialize output to requested format (JSON, table, etc.). | Fatal | – |
User-facing messages:
All CliError variants produce actionable messages. The CLI main() function
matches on CliError and prints:
- The human-readable message (from the variant’s user-facing table above)
- A suggestion for next steps
- The exit code (non-zero for fatal, specific codes for retryable)
taba-observe: ObserveError
Decision trail recording, solver replay, and integration export.
| Variant | Description | Retryable | FM |
|---|---|---|---|
TrailNotFound { trail_id } | Decision trail does not exist for the given ID. | Fatal | – |
TrailCompacted { trail_id } | Trail was compacted beyond retention window (INV-O2). | Fatal | – |
ReplayFailed { trail_id, reason } | Solver replay could not reconstruct inputs from trail. | Fatal | – |
ReplayDivergence { trail_id, expected_hash, actual_hash } | Replay produced different output than recorded (determinism regression FM-11). | Fatal | FM-11 |
ExportFailed { target, reason } | Integration export failed (Prometheus, OTEL, webhook). | Retryable | – |
User-facing messages (CLI-visible):
| Variant | User message |
|---|---|
TrailCompacted | Info: Decision trail {trail_id} has been compacted (beyond retention window). Increase decision_retention on the unit or trust domain to retain longer. |
ReplayDivergence | Critical: Solver replay for trail {trail_id} produced different results. This indicates a determinism regression. Investigate immediately. |
Error Propagation Map
Shows how errors wrap and propagate across crate boundaries.
taba-common::CommonError
├── wrapped by taba-core::CoreError::Common
├── wrapped by taba-security::SecurityError (implicit, via field types)
└── wrapped by taba-graph::GraphError (implicit, via field types)
taba-security::SecurityError
├── wrapped by taba-core::CoreError::Security
├── wrapped by taba-graph::GraphError::Security
├── wrapped by taba-gossip::GossipError::Security
└── wrapped by taba-cli::CliError::Security
taba-core::CoreError
├── wrapped by taba-graph::GraphError::Core
└── wrapped by taba-cli::CliError::Core
taba-graph::GraphError
├── wrapped by taba-solver::SolverError::Graph
└── wrapped by taba-node::NodeError::Graph
taba-solver::SolverError
└── wrapped by taba-node::NodeError::Solver
taba-erasure::ErasureError
└── wrapped by taba-node::NodeError::Erasure
taba-gossip::GossipError
└── wrapped by taba-node::NodeError::Gossip
taba-observe::ObserveError
└── wrapped by taba-node::NodeError::Observe
taba-node::NodeError
└── wrapped by taba-cli::CliError::Node
CLI user
│
▼
CliError ─────────────────────────────────────┐
│ wraps │ wraps
▼ ▼
NodeError SecurityError
│ wraps (4 sources) CoreError
▼
GraphError ── SolverError ── ErasureError ── GossipError
│ wraps
▼
SecurityError, CoreError
│ wraps
▼
CommonError
Failure Mode Coverage
Every failure mode (FM-01 through FM-27) is covered by at least one error variant.
| FM | Primary Error(s) | Crate(s) |
|---|---|---|
| FM-01: Single node failure | GossipError::NodeUnreachable, ErasureError::ShardCorrupted | taba-gossip, taba-erasure |
| FM-02: Multiple node failures | ErasureError::InsufficientShards, ErasureError::ThresholdExceeded, NodeError::DegradedMode | taba-erasure, taba-node |
| FM-03: Network partition | GossipError::PartitionDetected | taba-gossip |
| FM-04: Compromised node | SecurityError::SignatureInvalid, GossipError::MessageSignatureInvalid, ErasureError::SignatureVerificationPostReconstruct, SecurityError::DelegationTokenForged | taba-security, taba-gossip, taba-erasure |
| FM-05: Compromised author | SecurityError::AuthorKeyRevoked, SecurityError::AuthorScopeExceeded | taba-security |
| FM-06: Wrong solver placement | SolverError::DeterminismViolation, GraphError::CrdtViolation, GraphError::InsertionOrderDependence | taba-solver, taba-graph |
| FM-07: WAL corruption/disk full | GraphError::WalWriteFailed, GraphError::WalCorrupted, GraphError::WalReplayFailed | taba-graph |
| FM-08: Graph unbounded growth | GraphError::MemoryLimitExceeded, GraphError::CompactionFailed, NodeError::DegradedMode | taba-graph, taba-node |
| FM-09: Gossip false positive | GossipError::FalsePositiveDetected, GossipError::InsufficientWitnesses | taba-gossip |
| FM-10: Conflicting legal requirements | SolverError::ConflictDetected, SecurityError::SecurityConflictUnresolved, CoreError::PolicyConflict | taba-solver, taba-security, taba-core |
| FM-11: Solver determinism regression | SolverError::DeterminismViolation, SolverError::FloatingPointDetected, ObserveError::ReplayDivergence | taba-solver, taba-observe |
| FM-12: Solver version skew | SolverError::VersionSkew, GossipError::VersionAnnounceMismatch | taba-solver, taba-gossip |
| FM-13: Cascading reconstruction storm | ErasureError::ReconstructionBackpressure, ErasureError::ReconstructionFailed | taba-erasure |
| FM-14: Promotion policy collision | CoreError::PromotionPolicyCollision | taba-core |
| FM-15: Artifact unavailable | NodeError::ArtifactFetchFailed, NodeError::ArtifactDigestMismatch | taba-node |
| FM-16: Dev node offline | Behavioral (INV-N5 leave-dead default) | taba-solver |
| FM-17: Stale capability discovery | NodeError::CapabilityDiscoveryFailed | taba-node |
| FM-18: Role succession gap | SecurityError::AuthorKeyRevoked (break-glass via root key) | taba-security |
| FM-19: Tier 0→1 upgrade failure | SecurityError::CeremonyError | taba-security |
| FM-20: Compaction removes referenced unit | GraphError::CompactionFailed (reference check prevents) | taba-graph |
| FM-21: Archive backend unavailable | GraphError::ArchivalFailed | taba-graph |
| FM-22: Bounded task deadline exceeded | Behavioral (node forcefully terminates) | taba-node |
| FM-23: Spawn depth exceeded | CoreError::SpawnDepthExceeded, GraphError::SpawnDepthExceeded | taba-core, taba-graph |
| FM-24: Logical clock divergence | Behavioral (clock sync on reconnection) | taba-common |
| FM-25: Bridge failure isolates domains | GossipError::BridgeUnavailable | taba-gossip |
| FM-26: Compromised bridge | SecurityError::SignatureInvalid (standard + wider blast radius) | taba-security |
| FM-27: Stale cross-domain cache | GossipError::BilateralPolicyMissing (strict mode) | taba-gossip |
Event Catalog
This document defines every event type in taba, its producers and consumers, ordering guarantees, delivery semantics, propagation mechanism, and mapping to WAL entries. It is the authoritative reference for all cross-context communication.
Design principles
- Events are typed enums. Every variant carries the minimum payload needed for the consumer to act.
- Local events flow through
tokio::broadcastortokio::mpscchannels within a single node. They are never serialized to wire format. - Gossip events flow through the signed gossip protocol (INV-R3). They are serialized to protobuf and signed with the sending node’s identity key.
- WAL entries are the durable record. An event that mutates graph state must be WAL’d before the mutation is visible (INV-C4).
- No event carries mutable references or live pointers. All payloads are owned, cloneable, serializable values.
Event types
GraphEvent
Events produced by the composition graph after state changes.
#![allow(unused)]
fn main() {
/// Events emitted by taba-graph when graph state changes.
/// Consumed locally (never sent over gossip -- graph state
/// propagates via CRDT merge, not event replay).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GraphEvent {
/// A unit passed signature verification and was merged into the graph.
/// WAL: Merged(unit).
UnitMerged {
unit_id: UnitId,
unit_type: UnitType,
author: AuthorId,
trust_domain: TrustDomainId,
timestamp: Timestamp,
/// IDs of compositions affected by this insertion.
affected_compositions: Vec<UnitId>,
},
/// A unit was verified but has unsatisfied references.
/// Buffered in the pending queue until references arrive (INV-C4).
/// WAL: Pending(unit, missing_refs).
UnitPending {
unit_id: UnitId,
missing_refs: Vec<UnitId>,
timestamp: Timestamp,
},
/// A previously pending unit's references are now satisfied.
/// WAL: Promoted(unit_id).
UnitPromoted {
unit_id: UnitId,
timestamp: Timestamp,
},
/// A unit was rejected at the merge gate.
/// Not WAL'd (rejected units do not enter durable state).
UnitRejected {
unit_id: UnitId,
reason: MergeRejection,
timestamp: Timestamp,
},
/// A policy superseded a prior policy for the same conflict (INV-C7).
/// WAL: Merged(new_policy) -- the supersession is structural.
PolicySuperseded {
new_policy_id: UnitId,
old_policy_id: UnitId,
conflict_tuple: ConflictTuple,
timestamp: Timestamp,
},
/// A subgraph was archived (compaction, retention expiry).
/// WAL: dedicated compaction record.
SubgraphArchived {
root_unit_ids: Vec<UnitId>,
reason: ArchivalReason,
timestamp: Timestamp,
},
/// Graph memory usage crossed the 80% compaction threshold (INV-R6).
CompactionTriggered {
current_bytes: u64,
limit_bytes: u64,
timestamp: Timestamp,
},
}
}
Producer: taba-graph (composition graph module).
Consumers: taba-solver (reacts to UnitMerged, UnitPromoted, PolicySuperseded),
taba-node (reacts to CompactionTriggered, SubgraphArchived for local cleanup),
taba-security (reacts to UnitRejected for audit logging).
Channel: tokio::broadcast (multiple consumers per event).
Ordering: causal. Events for a single unit are ordered by Timestamp. Events
across unrelated units have no ordering guarantee.
Delivery: at-most-once within a node (broadcast drops if a receiver is lagging).
Acceptable because graph state is the source of truth, not the event stream. A
consumer that misses an event will see the state on its next graph query.
SolverEvent
Events produced by the solver after evaluating compositions and placements.
#![allow(unused)]
fn main() {
/// Events emitted by taba-solver after composition or placement computation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SolverEvent {
/// A composition was successfully resolved.
CompositionResolved {
composition_id: UnitId,
participating_units: Vec<UnitId>,
timestamp: Timestamp,
},
/// The solver detected an unresolvable conflict requiring policy.
ConflictDetected {
conflicting_units: Vec<UnitId>,
capability: CapabilityRef,
reason: ConflictReason,
timestamp: Timestamp,
},
/// A placement decision was computed.
/// Deterministic: any node with the same graph + membership
/// computes the same result (INV-C3).
PlacementDecision {
unit_id: UnitId,
target_node: NodeId,
score: Ppm,
timestamp: Timestamp,
},
/// A previously placed unit must be moved (node failure, rebalance).
PlacementRevoked {
unit_id: UnitId,
previous_node: NodeId,
reason: RevocationReason,
timestamp: Timestamp,
},
/// Scaling decision computed from unit-declared parameters (INV-K4).
ScalingDecision {
unit_id: UnitId,
current_instances: u32,
target_instances: u32,
trigger: ScalingTrigger,
timestamp: Timestamp,
},
/// Cyclic recovery dependency detected (INV-K5).
CyclicDependency {
cycle: Vec<UnitId>,
timestamp: Timestamp,
},
}
}
Producer: taba-solver.
Consumers: taba-node (reacts to PlacementDecision, PlacementRevoked, ScalingDecision
for local reconciliation), taba-graph (records placement as graph annotation).
Channel: tokio::mpsc (solver to node is point-to-point per node).
Ordering: total within a single solver evaluation pass. Across passes, causal
(a pass triggered by UnitMerged happens-after that merge).
Delivery: at-least-once. PlacementDecision is written to the graph as a placement
annotation. If the local channel drops the event, the node’s reconciliation loop
will discover the placement on its next graph read.
NodeEvent
Events produced by node operations reflecting local actual state.
#![allow(unused)]
fn main() {
/// Events emitted by taba-node about local workload and health state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NodeEvent {
/// A workload unit was successfully started on this node.
WorkloadStarted {
unit_id: UnitId,
node_id: NodeId,
timestamp: Timestamp,
},
/// A workload unit failed to start or crashed.
WorkloadFailed {
unit_id: UnitId,
node_id: NodeId,
error: WorkloadError,
timestamp: Timestamp,
},
/// A workload unit is draining (graceful shutdown in progress).
WorkloadDraining {
unit_id: UnitId,
node_id: NodeId,
reason: DrainReason,
timestamp: Timestamp,
},
/// A workload unit has terminated.
WorkloadTerminated {
unit_id: UnitId,
node_id: NodeId,
exit_status: ExitStatus,
timestamp: Timestamp,
},
/// Drift detected between desired and actual state.
DriftDetected {
unit_id: UnitId,
node_id: NodeId,
expected: UnitState,
actual: UnitState,
timestamp: Timestamp,
},
/// Node health status update.
HealthReport {
node_id: NodeId,
cpu_usage_ppm: Ppm,
memory_usage_ppm: Ppm,
wal_healthy: bool,
timestamp: Timestamp,
},
}
}
Producer: taba-node (reconciliation loop).
Consumers: taba-graph (WorkloadFailed and HealthReport update graph annotations
that the solver reads), taba-solver (indirect, via graph state updates).
Channel: tokio::mpsc (node to graph is point-to-point).
Ordering: causal per unit. Events for a single unit_id on a single node are
strictly ordered. Events across units have no ordering guarantee.
Delivery: at-least-once. Health reports are idempotent (latest wins). Workload
state changes are also visible to the reconciliation loop on its next tick, so a
dropped event self-heals.
GossipMessage
Messages exchanged between nodes over the gossip protocol. These are not local events – they are wire-format messages signed per INV-R3.
#![allow(unused)]
fn main() {
/// Signed messages exchanged between nodes via SWIM-like gossip.
/// Every message is signed: Sign(node_key, hash(payload || cluster_id)).
/// Unsigned or invalid messages are dropped and the sender is flagged.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GossipMessage {
// -- Membership (SWIM protocol) --
/// Ping: "are you alive?"
Ping {
from: NodeId,
sequence: u64,
},
/// Ack: "yes, I am alive."
Ack {
from: NodeId,
sequence: u64,
},
/// Indirect ping request: "please ping target on my behalf."
PingReq {
from: NodeId,
target: NodeId,
sequence: u64,
},
/// Membership state dissemination (piggybacked on ping/ack).
MembershipUpdate {
from: NodeId,
updates: Vec<MembershipEntry>,
lamport_clock: u64,
},
// -- Graph state synchronization --
/// CRDT delta for graph merge. Contains one or more signed units.
/// The receiving node verifies each unit signature before merging.
GraphDelta {
from: NodeId,
/// Serialized CRDT delta (units + tombstones).
delta: Vec<u8>,
/// Causal context for the delta.
vector_clock: VectorClock,
},
/// Request missing graph state identified by vector clock comparison.
GraphSyncRequest {
from: NodeId,
/// The requester's current vector clock.
have: VectorClock,
},
/// Response with missing graph state.
GraphSyncResponse {
from: NodeId,
delta: Vec<u8>,
vector_clock: VectorClock,
},
// -- Security (priority gossip) --
/// Key revocation notice. Propagated with priority (skips queue).
/// All nodes must process before accepting new units from the revoked key.
KeyRevocation {
from: NodeId,
revoked_author: AuthorId,
revocation_timestamp: Timestamp,
/// Signature from the revoking authority (not the revoked key).
revocation_signature: Vec<u8>,
},
// -- Erasure coding --
/// Shard assignment after membership change or reconstruction.
ShardAssignment {
from: NodeId,
shard_id: ShardId,
assigned_to: NodeId,
coding_params: ErasureParams,
},
/// Shard reconstruction request (node failure recovery).
ShardRecoveryRequest {
from: NodeId,
shard_id: ShardId,
/// Priority class per INV-R1.
priority: ShardPriority,
},
/// Shard reconstruction data (responding to recovery request).
ShardRecoveryData {
from: NodeId,
shard_id: ShardId,
fragment: Vec<u8>,
},
// -- Operational --
/// Solver version announcement (FM-12).
/// Placement pauses until all active nodes report the same version.
SolverVersionAnnounce {
from: NodeId,
solver_version: Version,
},
/// Operational mode transition.
ModeTransition {
from: NodeId,
new_mode: OperationalMode,
reason: ModeTransitionReason,
timestamp: Timestamp,
},
}
/// Priority classes for shard reconstruction (INV-R1).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ShardPriority {
/// Governance units -- highest priority.
Governance,
/// Policy units.
Policy,
/// Data constraint units.
DataConstraint,
/// Workload units -- lowest priority.
Workload,
}
}
Producer: every node (gossip is peer-to-peer). Consumer: every node. Channel: UDP for ping/ack/ping-req. TCP for large payloads (GraphDelta, ShardRecoveryData). Ordering: membership updates use Lamport clocks. Graph deltas use vector clocks for causal ordering. Key revocations have no ordering guarantee but are idempotent (processing the same revocation twice is a no-op). Delivery: at-least-once (gossip retransmits via piggybacking). Graph deltas are crdt-merged (idempotent), so duplicate delivery is harmless.
SecurityEvent
Events produced by the security subsystem. Some propagate locally, others via priority gossip.
#![allow(unused)]
fn main() {
/// Events emitted by taba-security.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SecurityEvent {
/// Signature verification succeeded.
/// Local only, not persisted.
VerificationPassed {
unit_id: UnitId,
author: AuthorId,
timestamp: Timestamp,
},
/// Signature verification failed.
/// Local only, logged for audit.
VerificationFailed {
unit_id: UnitId,
reason: VerificationFailure,
timestamp: Timestamp,
},
/// A key was revoked. Propagated via priority gossip (KeyRevocation message).
KeyRevoked {
author: AuthorId,
revocation_timestamp: Timestamp,
revoked_by: AuthorId,
},
/// Capability check result (runtime, on node).
CapabilityCheckResult {
unit_id: UnitId,
capability: CapabilityRef,
allowed: bool,
policy_id: Option<UnitId>,
timestamp: Timestamp,
},
/// Taint classification computed for a data unit (query-time, INV-S4).
TaintComputed {
data_unit_id: UnitId,
classification: DataClassification,
inherited_from: Vec<UnitId>,
timestamp: Timestamp,
},
/// Gossip message authentication failed.
/// Sender flagged for investigation.
GossipAuthFailure {
sender: NodeId,
reason: GossipAuthError,
timestamp: Timestamp,
},
}
}
Producer: taba-security.
Consumers: taba-graph (VerificationPassed/Failed gate merge), taba-node
(CapabilityCheckResult gates runtime access), taba-gossip (KeyRevoked triggers
priority gossip broadcast), audit log (all events).
Channel: local – tokio::broadcast. KeyRevoked additionally triggers a
GossipMessage::KeyRevocation for cluster-wide propagation.
Ordering: none guaranteed across events. Verification events are synchronous
within the merge path (the merge blocks until verification completes).
Delivery: at-most-once locally (acceptable – security decisions are synchronous
gates, not async reactions). KeyRevocation via gossip is at-least-once.
CeremonyEvent
Events related to the Shamir key ceremony lifecycle.
#![allow(unused)]
fn main() {
/// Events emitted during Shamir ceremony operations.
/// Recorded as governance units in the graph after ceremony completion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CeremonyEvent {
/// A new ceremony was initiated.
CeremonyStarted {
ceremony_id: CeremonyId,
initiator: AuthorId,
total_shares: u8,
threshold: u8,
timestamp: Timestamp,
},
/// A share was contributed to an in-progress ceremony.
ShareAdded {
ceremony_id: CeremonyId,
share_index: u8,
contributor: AuthorId,
timestamp: Timestamp,
},
/// Ceremony completed successfully. Root key reconstructed.
/// The ceremony record becomes a governance unit in the graph.
CeremonyCompleted {
ceremony_id: CeremonyId,
witness: AuthorId,
trust_domain_created: TrustDomainId,
timestamp: Timestamp,
},
/// Ceremony failed or was aborted.
CeremonyAborted {
ceremony_id: CeremonyId,
reason: CeremonyFailure,
timestamp: Timestamp,
},
}
}
Producer: taba-security (ceremony manager).
Consumers: taba-graph (CeremonyCompleted creates the root governance unit
that seeds the graph), audit log.
Channel: tokio::mpsc (ceremony to graph, point-to-point).
Ordering: total within a single ceremony (started < share_added* < completed/aborted).
Delivery: at-least-once. CeremonyCompleted must durably persist – it is the
bootstrap event for the entire trust chain.
DistributionEvent
Events related to membership changes and erasure shard management.
#![allow(unused)]
fn main() {
/// Events emitted by taba-gossip and taba-erasure about cluster topology.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DistributionEvent {
/// A new node has joined and completed attestation.
NodeJoined {
node_id: NodeId,
listen_addr: String,
timestamp: Timestamp,
},
/// A node is suspected of failure (SWIM suspicion phase).
NodeSuspected {
node_id: NodeId,
suspected_by: NodeId,
timestamp: Timestamp,
},
/// A node was confirmed failed (2+ witnesses, INV-R3).
NodeFailed {
node_id: NodeId,
witnesses: Vec<NodeId>,
timestamp: Timestamp,
},
/// A node has gracefully left the cluster.
NodeLeft {
node_id: NodeId,
timestamp: Timestamp,
},
/// Erasure shard redistribution triggered by membership change.
ShardRedistribution {
trigger: MembershipTrigger,
reassignments: Vec<(ShardId, NodeId)>,
timestamp: Timestamp,
},
/// Shard reconstruction completed after node failure.
ShardReconstructed {
shard_id: ShardId,
new_holder: NodeId,
timestamp: Timestamp,
},
/// Reconstruction backpressure activated (INV-R1, FM-13).
ReconstructionThrottled {
queue_depth: u32,
threshold: u32,
timestamp: Timestamp,
},
}
}
Producer: taba-gossip (membership events), taba-erasure (shard events).
Consumers: taba-graph (NodeFailed/NodeLeft triggers shard redistribution),
taba-solver (membership changes trigger placement re-evaluation),
taba-node (ShardRedistribution triggers local shard acceptance/release).
Channel: tokio::broadcast (multiple consumers).
Ordering: membership events use SWIM protocol ordering (suspect before confirm).
Shard events are causally ordered after the membership event that triggered them.
Delivery: at-least-once. Membership state is convergent (SWIM guarantee).
Shard redistribution is idempotent (assigning a shard to a node that already holds
it is a no-op).
ObserveEvent (NEW)
Events produced by the observability subsystem.
#![allow(unused)]
fn main() {
/// Events emitted by taba-observe.
/// These are local events consumed by the graph (for persistence)
/// and integration exporters (for external systems).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ObserveEvent {
/// A decision trail was recorded for a solver run (INV-O1).
DecisionTrailRecorded {
trail_id: u64,
graph_snapshot_id: String,
placements_count: u32,
conflicts_count: u32,
timestamp: DualClockEvent,
},
/// Health check result for a workload (INV-O3).
HealthCheckCompleted {
unit_id: UnitId,
node_id: NodeId,
healthy: bool,
check_type: String,
timestamp: DualClockEvent,
},
/// Capability change detected on a node after re-probe (INV-N1).
CapabilityChanged {
node_id: NodeId,
added: Vec<String>,
removed: Vec<String>,
trigger: String, // "startup", "manual_refresh", "fleet_refresh"
timestamp: DualClockEvent,
},
/// Drift detected between desired and actual state.
DriftDetected {
unit_id: UnitId,
node_id: NodeId,
expected: String,
actual: String,
timestamp: DualClockEvent,
},
/// Promotion policy applied to a workload version.
PromotionApplied {
unit_id: UnitId,
version: String,
environment: String,
author: AuthorId,
timestamp: DualClockEvent,
},
/// Bounded task spawned by a service.
TaskSpawned {
parent_service: UnitId,
child_task: UnitId,
delegation_token_id: u64,
spawn_depth: u8,
timestamp: DualClockEvent,
},
/// Bounded task terminated.
TaskTerminated {
unit_id: UnitId,
reason: String, // "completed", "failed", "deadline_exceeded"
timestamp: DualClockEvent,
},
/// Compaction completed.
CompactionCompleted {
units_tombstoned: u32,
units_removed: u32,
bytes_freed: u64,
timestamp: DualClockEvent,
},
/// Ephemeral data removed or tombstoned (INV-D4).
EphemeralDataHandled {
data_unit_id: UnitId,
action: String, // "removed" or "tombstoned"
had_references: bool,
timestamp: DualClockEvent,
},
/// Cross-domain forwarding query executed.
CrossDomainQueryExecuted {
querying_domain: TrustDomainId,
target_domain: TrustDomainId,
bridge_node: NodeId,
cached: bool,
timestamp: DualClockEvent,
},
/// Alert dispatched via webhook.
AlertDispatched {
event_type: String,
target_url: String,
success: bool,
timestamp: DualClockEvent,
},
}
}
Producer: taba-observe (aggregates from all contexts).
Consumers: taba-graph (DecisionTrailRecorded persisted as graph event),
integration exporters (Prometheus, OpenTelemetry, webhook).
Channel: tokio::broadcast (multiple consumers).
Ordering: none guaranteed across events. Decision trails are causally
ordered by logical clock.
Delivery: at-most-once. Observability events are informational — dropped
events do not affect system correctness.
GossipMessage additions (NEW)
New gossip message variants for the analyst-session concepts:
#![allow(unused)]
fn main() {
// Added to GossipMessage enum:
/// Node capability advertisement (INV-N1).
/// Sent on startup, refresh, and periodically on significant change.
CapabilityAdvertisement {
from: NodeId,
capabilities: NodeCapabilitySet,
logical_clock: u64,
},
/// Node resource snapshot (INV-N3).
/// Sent periodically (configurable interval).
ResourceSnapshot {
from: NodeId,
memory_available_bytes: u64,
cpu_load_ppm: u64,
disk_available_bytes: u64,
gpu_available: u32,
logical_clock: u64,
},
/// Cross-domain capability advertisement relay (INV-X5).
/// Sent by bridge nodes to relay advertisements across domain boundaries.
CrossDomainAdvertisement {
from: NodeId, // bridge node
source_domain: TrustDomainId,
capability: CapabilityRef,
conditions: String,
},
/// Cross-domain forwarding query (INV-X2).
ForwardingQuery {
from: NodeId,
target_domain: TrustDomainId,
query_type: String,
query_payload: Vec<u8>,
request_id: u64,
},
/// Cross-domain forwarding response.
ForwardingResponse {
from: NodeId, // bridge node
request_id: u64,
result: Vec<u8>, // serialized read-only view
},
/// Fleet-wide operational command (governance).
FleetCommand {
from: NodeId,
command_type: String, // "refresh_capabilities", etc.
governance_unit_id: UnitId,
logical_clock: u64,
},
}
Ordering guarantees summary
| Event type | Ordering | Mechanism |
|---|---|---|
| GraphEvent | Causal per unit | Timestamp (wall + logical counter) |
| SolverEvent | Total within evaluation pass | Sequential solver execution |
| NodeEvent | Causal per (unit, node) pair | Reconciliation loop sequence |
| GossipMessage (membership) | Causal | Lamport clock on MembershipUpdate |
| GossipMessage (graph delta) | Causal | Vector clock on GraphDelta |
| GossipMessage (key revocation) | None (idempotent) | Priority broadcast |
| SecurityEvent | None (synchronous gates) | Inline in calling path |
| CeremonyEvent | Total within ceremony | Sequential ceremony state machine |
| DistributionEvent | Causal (suspect < confirm < redistribute) | SWIM protocol + sequencing |
There is no total ordering across contexts. taba is a peer-to-peer system with no global clock and no consensus protocol for normal operations. Ordering is causal where needed and convergent everywhere else.
Delivery semantics summary
| Event type | Delivery | Why acceptable |
|---|---|---|
| GraphEvent | At-most-once (broadcast) | Graph state is source of truth; missed events self-heal on next query |
| SolverEvent | At-least-once | PlacementDecision persisted to graph; reconciliation loop catches drops |
| NodeEvent | At-least-once | Health/state is idempotent (latest wins); reconciliation loop retries |
| GossipMessage | At-least-once (retransmit) | CRDT merge is idempotent; membership converges |
| SecurityEvent | At-most-once (local) | Verification is synchronous gate; KeyRevocation is at-least-once via gossip |
| CeremonyEvent | At-least-once | Ceremony completion is critical bootstrap; must durably persist |
| DistributionEvent | At-least-once | Shard assignment is idempotent; membership converges |
No event requires exactly-once delivery. The system is designed so that all state-mutating operations are either idempotent or gated by synchronous checks. This is a deliberate consequence of the CRDT architecture.
Event-to-WAL mapping
WAL entries record durable state transitions. Not every event produces a WAL entry – only events that change graph state or local node state.
| Event | WAL entry type | Notes |
|---|---|---|
| GraphEvent::UnitMerged | Merged(unit) | Full unit payload, written before merge is visible (INV-C4) |
| GraphEvent::UnitPending | Pending(unit, missing_refs) | Unit + list of unsatisfied references |
| GraphEvent::UnitPromoted | Promoted(unit_id) | Reference to existing Pending entry |
| GraphEvent::PolicySuperseded | Merged(new_policy) | New policy is a Merged entry; old policy remains (supersession is structural) |
| GraphEvent::SubgraphArchived | Compaction record | References archived unit IDs; WAL entries for those units may be reclaimed |
| SolverEvent::PlacementDecision | Graph annotation | Placement stored as graph metadata, WAL’d as part of graph mutation |
| NodeEvent::WorkloadStarted | Local state record | Per-node WAL for actual state tracking |
| NodeEvent::WorkloadFailed | Local state record | Per-node WAL for actual state tracking |
| GossipMessage::GraphDelta | Merged(unit) per unit in delta | Each unit in the delta is individually verified and WAL’d |
| GossipMessage::KeyRevocation | Revocation record | Durable; must survive restart to prevent accepting revoked-key units |
| CeremonyEvent::CeremonyCompleted | Merged(governance_unit) | The root governance unit that seeds the graph |
| DistributionEvent::NodeFailed | Membership state record | Local membership view persisted for restart recovery |
Events not listed (e.g., SecurityEvent::VerificationPassed, GraphEvent::UnitRejected) are not WAL’d. They are transient observations, not state transitions.
Propagation: gossip vs. local channels
┌─────────────────────────────────────┐
│ Node boundary │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ taba- │ │ taba-solver │ │
│ │ graph │───>│ (reads graph │ │
│ │ │ │ snapshot) │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ GraphEvent SolverEvent │
│ (broadcast) (mpsc) │
│ │ │ │
│ v v │
│ ┌──────────┐ ┌──────────────┐ │
│ │ taba- │ │ taba-node │ │
│ │ security │ │ (reconcile) │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ SecurityEvent NodeEvent │
│ (broadcast) (mpsc) │
│ │ │ │
└───────┼─────────────────┼───────────┘
│ │
════════════╪═════════════════╪════════ node boundary
│ │
v v
┌─────────────────────────────────────┐
│ taba-gossip (wire) │
│ │
│ Signed GossipMessage over UDP/TCP │
│ - MembershipUpdate (piggybacked) │
│ - GraphDelta (CRDT sync) │
│ - KeyRevocation (priority) │
│ - ShardRecovery (on demand) │
│ - SolverVersionAnnounce │
│ - ModeTransition │
│ │
└──────────────┬──────────────────────┘
│
│ signed, serialized (protobuf)
│
┌───────┴───────┐
│ network │
│ (UDP / TCP) │
└───────┬───────┘
│
v
┌─────────────────────────────────────┐
│ other peer nodes │
└─────────────────────────────────────┘
Local channels carry typed Rust enums. Zero serialization cost. Bounded
by channel capacity (backpressure via tokio::broadcast lag detection or
mpsc bounded send).
Gossip carries signed protobuf messages. Serialization + signing cost on send, deserialization + verification cost on receive. Verification failure drops the message and flags the sender (SecurityEvent::GossipAuthFailure).
Key event flows
Flow 1: Unit insertion (happy path)
Author taba-core taba-security taba-graph taba-solver taba-node
│ │ │ │ │ │
│ submit unit │ │ │ │ │
│─────────────────────>│ │ │ │ │
│ │ sign(unit) │ │ │ │
│ │───────────────────>│ │ │ │
│ │ signed unit │ │ │ │
│ │<───────────────────│ │ │ │
│ │ insert(signed) │ │ │ │
│ │───────────────────────────────────────>│ │ │
│ │ │ verify(sig) │ │ │
│ │ │<───────────────────│ │ │
│ │ │ ok │ │ │
│ │ │───────────────────>│ │ │
│ │ │ │ │ │
│ │ │ [WAL: Merged(unit)] │ │
│ │ │ │ │ │
│ │ │ │ GraphEvent:: │ │
│ │ │ │ UnitMerged │ │
│ │ │ │───────────────────>│ │
│ │ │ │ │ │
│ │ │ │ │ SolverEvent:: │
│ │ │ │ │ PlacementDecision │
│ │ │ │ │─────────────────>│
│ │ │ │ │ │
│ │ │ │ │ [reconcile]
Flow 2: Key revocation propagation
Revoking authority taba-security taba-gossip all peer nodes
│ │ │ │
│ revoke(author_id) │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ SecurityEvent:: │ │
│ │ KeyRevoked │ │
│ │──────────────────>│ │
│ │ │ │
│ │ │ PRIORITY gossip: │
│ │ │ KeyRevocation │
│ │ │───────────────────>│
│ │ │ │
│ │ │ │ [WAL: revocation record]
│ │ │ │ [reject future units
│ │ │ │ from revoked key]
│ │ │ │
│ │ │ (retransmit via │
│ │ │ piggybacking │
│ │ │ until all nodes │
│ │ │ acknowledge) │
Key revocations bypass the normal gossip queue. They are transmitted immediately and piggybacked on every subsequent gossip round until all active nodes have acknowledged receipt.
Flow 3: Node failure and shard reconstruction
failed node taba-gossip (peers) taba-erasure taba-graph taba-solver
│ │ │ │ │
X (dies) │ │ │ │
│ │ │ │
[SWIM detect] │ │ │
│ │ │ │
DistributionEvent:: │ │ │
NodeSuspected │ │ │
│ │ │ │
[2+ witnesses confirm] │ │ │
│ │ │ │
DistributionEvent:: │ │ │
NodeFailed │ │ │
│ │ │ │
│────────────────────>│ │ │
│ │ │ │
│ ShardRecoveryRequest │ │
│ (priority queue: │ │
│ Gov > Policy > Data │ │
│ > Workload) │ │
│ │ │ │
│ │ [reconstruct] │ │
│ │ │ │
│ DistributionEvent:: │ │
│ ShardReconstructed │ │
│ │───────────────────>│ │
│ │ │──────────────────>│
│ │ │ │
│ │ │ [re-evaluate
│ │ │ placements for
│ │ │ orphaned units]
Flow 4: Operational mode transition
trigger taba-node taba-gossip all peer nodes
│ │ │ │
│ (erasure threshold │ │ │
│ exceeded / │ │ │
│ memory limit hit) │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ ModeTransition │ │
│ │ Normal -> Degraded │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ GossipMessage:: │
│ │ │ ModeTransition │
│ │ │────────────────────>│
│ │ │ │
│ │ │ │ [freeze authoring,
│ │ │ │ composition,
│ │ │ │ placement.
│ │ │ │ drain only.]
│ │ │ │
│ ... issue resolved ... │ │
│ │ │ │
│ │ ModeTransition │ │
│ │ Degraded -> Recovery │ │
│ │────────────────────>│────────────────────>│
│ │ │ │
│ ... re-coding completes ...│ │
│ │ │ │
│ │ ModeTransition │ │
│ │ Recovery -> Normal │ │
│ │────────────────────>│────────────────────>│
Supporting enums (referenced above)
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnitType {
Workload,
Data,
Policy,
Governance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MergeRejection {
InvalidSignature,
ScopeViolation,
KeyRevoked,
ContextMismatch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictTuple {
pub unit_ids: Vec<UnitId>,
pub capability_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ArchivalReason {
RetentionExpired,
CompactionThreshold,
ManualArchival,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConflictReason {
IncompatibleCapabilities,
SecurityConflict,
AmbiguousMatch,
CyclicRecoveryDependency,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RevocationReason {
NodeFailed,
ResourcesExhausted,
PolicyChange,
Rebalance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DrainReason {
PlacementRevoked,
NodeDraining,
ScaleDown,
PolicyConflict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnitState {
Declared,
Composed,
Placed,
Running,
Draining,
Terminated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OperationalMode {
Normal,
Degraded,
Recovery,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModeTransitionReason {
ErasureThresholdExceeded,
MemoryLimitExceeded,
OperatorTriggered,
RecoveryComplete,
RecodingComplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VerificationFailure {
InvalidSignature,
ExpiredValidity,
KeyRevoked { revocation_timestamp: Timestamp },
ScopeViolation,
ContextMismatch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DataClassification {
Public,
Internal,
Confidential,
Pii,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CeremonyFailure {
InsufficientShares,
InvalidShare,
Timeout,
Aborted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MembershipEntry {
pub node_id: NodeId,
pub state: MembershipState,
pub lamport_clock: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum MembershipState {
Alive,
Suspected,
Failed,
Left,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MembershipTrigger {
NodeJoined(NodeId),
NodeFailed(NodeId),
NodeLeft(NodeId),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErasureParams {
pub data_shards: u16,
pub parity_shards: u16,
}
}
Cross-reference to invariants and decisions
| Invariant / Decision | Relevant events |
|---|---|
| INV-S3 (units signed, verified before merge) | SecurityEvent::VerificationPassed/Failed, GraphEvent::UnitMerged/UnitRejected |
| INV-S4 (taint propagation at query time) | SecurityEvent::TaintComputed (computed on demand, not on merge) |
| INV-C3 (deterministic solver) | SolverEvent::PlacementDecision (same on every node) |
| INV-C4 (WAL-before-effect, entry types) | GraphEvent::UnitMerged/UnitPending/UnitPromoted map to Merged/Pending/Promoted |
| INV-C7 (single policy per conflict) | GraphEvent::PolicySuperseded |
| INV-R1 (reconstruction backpressure) | DistributionEvent::ReconstructionThrottled, ShardPriority enum |
| INV-R3 (signed gossip, 2 witnesses) | GossipMessage (all variants signed), DistributionEvent::NodeFailed.witnesses |
| INV-R4 (erasure threshold, degraded mode) | GossipMessage::ModeTransition, DistributionEvent |
| INV-K5 (cyclic recovery) | SolverEvent::CyclicDependency |
| DL-008 (WAL entry types) | Merged, Pending, Promoted mapped in WAL table above |
| DL-009 (signed gossip) | GossipMessage outer signing envelope |
| FM-12 (solver version skew) | GossipMessage::SolverVersionAnnounce |
| FM-13 (reconstruction storm) | DistributionEvent::ReconstructionThrottled |
Security Model
taba’s security model is built on six principles:
1. Zero-access default (INV-S1)
Units access nothing unless explicitly declared and policy-approved.
The CapabilityEnforcer trait checks that every capability a unit
exercises is in its granted set. Unknown units or missing
capabilities result in denial — there is no implicit grant.
#![allow(unused)]
fn main() {
pub trait CapabilityEnforcer {
fn check_access(&self, unit: &UnitId, capability: &Capability) -> Result<(), SecurityError>;
fn check_all(&self, unit: &UnitId, capabilities: &[Capability]) -> Result<(), SecurityError>;
}
}
2. Fail closed (INV-S2)
Security conflicts are never implicitly resolved. When the solver detects incompatible security requirements between units and no policy resolves the incompatibility, the composition is blocked. Ambiguous policies are denied, not guessed.
3. Signed units (INV-S3)
Every unit in the graph is signed with Ed25519. Signatures bind
context: Sign(key, hash(unit || trust_domain_id || cluster_id || validity_window)).
This prevents:
- Cross-cluster replay (different cluster ID)
- Cross-domain replay (different trust domain)
- Post-revocation forgery (causal model: units signed after revocation are rejected, units signed before remain valid)
Signature verification is a synchronous gate before any unit enters graph state. No unit is accepted without verification.
4. Capability-based with purpose qualifiers (INV-K2)
Capabilities are typed tuples: (cap_type, name, purpose?).
cap_type: the kind of resource (e.g., “storage”, “network”)name: the specific resource (e.g., “postgres-compatible”)purpose(optional): a qualifier that must match during composition
When purpose is declared on a need, the provide must have the
same purpose. When omitted, any provide of the same type+name is
acceptable.
Capability lists are sorted lexicographically before matching to ensure determinism regardless of declaration order (INV-K2).
5. Author scope enforcement (INV-S5, INV-S8)
Authors are parameterized by (unit_type_scope × trust_domain_scope).
- INV-S5: Authors cannot create units outside their scope. A workload-scoped author cannot create policy units. An author scoped to trust domain A cannot create units in trust domain B.
- INV-S8: For state-producing types (workload, data), no two distinct authors may have identical scope tuples. For decision-making types (policy, governance), overlapping scopes are permitted (INV-S8a).
6. Taint propagation (INV-S4, INV-S7)
Data classification is propagated through the provenance graph at query time (not cached at merge time). This makes taint eventually consistent across nodes.
- Single input: output inherits the input’s classification
- Multiple inputs: output inherits the union (most restrictive) of all input classifications
- Declassification: requires multi-party signing (INV-S9) — one policy-scoped signer + one data-steward-scoped signer
- Hierarchy: children can narrow freely (INV-S7), widening requires explicit policy
Classification lattice: Public < Internal < Confidential < PII
STRIDE Security Analysis
Date: 2026-09-15 Scope: All 14 crates (M1–M7) Method: STRIDE threat modeling framework
Overview
STRIDE categorizes threats into six types: Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, and Elevation of Privilege. Each is analyzed against taba’s attack surfaces.
Spoofing
Threat: Attacker impersonates a known author
Attack surface: Unit authoring, gossip message handling
Scenario: Attacker generates an Ed25519 keypair, claims
to be author “alice”, and submits units to the graph.
Mitigation: Units are Ed25519-signed with the author’s
private key (INV-S3). The graph’s Verifier (when configured)
checks the signature against the registered public key. Units
with invalid signatures are rejected at the synchronous gate
before WAL write.
Residual risk: Low — Ed25519 is cryptographically
sound. The main risk is key compromise, addressed by the
revocation protocol (causal model, INV-S3).
Threat: Attacker impersonates a node in gossip
Attack surface: SWIM membership protocol
Scenario: Attacker sends signed gossip messages claiming
to be a known node.
Mitigation: All gossip messages are signed with the
sending node’s identity key (INV-R3). The GossipTransport
verifies signatures before passing messages to
MembershipProtocol::handle_message.
Residual risk: Low — same as author impersonation.
Threat: Attacker forges a Shamir ceremony
Attack surface: Ceremony manager, enrollment Scenario: Attacker initiates a ceremony, collects shares, and reconstructs the root key. Mitigation: Only authorized authors can initiate ceremonies. Shares are zeroized after reconstruction. The reconstructed key is used to sign exactly one governance unit, then destroyed. Residual risk: Medium — if an attacker compromises enough shares (≥ threshold), they can reconstruct the key. This is inherent to Shamir secret sharing and mitigated by physical security of share holders.
Tampering
Threat: Attacker modifies a unit after signing
Attack surface: Graph storage, WAL, gossip
Scenario: Attacker modifies a unit in the graph, WAL, or
during gossip transmission.
Mitigation: Units are content-addressed (SHA-256 of type +
payload + author + timestamp). The graph stores SignedUnit
wrappers. The WAL uses CRC32C framing (DL-014). Gossip
messages are signed.
Residual risk: Low — any modification is detected by
signature verification or CRC check.
Threat: Attacker tampers with WAL during compaction
Attack surface: WAL compaction Scenario: Attacker (or crash) corrupts the WAL during compaction, losing data. Mitigation: Compaction writes the new segment and fsyncs before deleting old segments (FINDING-011 fix). If the process crashes, old segments are still intact for replay. Residual risk: Low — write-then-delete ordering ensures durability.
Threat: Attacker injects malicious TOML via K8s metadata
Attack surface: K8s converter
Scenario: K8s manifest contains a resource name with "
or \n, breaking the generated TOML or injecting arbitrary
keys.
Mitigation: All K8s metadata values are sanitized via
sanitize_toml_value() before interpolation (FINDING-028
fix). Double quotes, backslashes, and control characters are
escaped.
Residual risk: Low — all interpolation paths are
covered.
Repudiation
Threat: Author denies creating a unit
Attack surface: Audit trail, provenance Scenario: Author creates a unit, then denies it, claiming the graph was corrupted. Mitigation: Every unit carries a cryptographic signature (Ed25519) bound to context (trust domain, cluster, validity window). Decision trails (INV-O1) record every solver run with inputs and outputs. Provenance chains (INV-D1) are structural — they emerge from the composition graph. Residual risk: None — signatures are non-repudiable under standard cryptographic assumptions.
Threat: Node denies receiving a gossip message
Attack surface: Gossip protocol
Scenario: Node claims it never received a revocation
message, allowing revoked authors to continue operating.
Mitigation: Gossip uses piggybacked retransmission with
retransmit_multiplier × log₂(N) rounds. Key revocations
use double the normal rounds for rapid convergence (DL-016).
The causal model (INV-S3) means units signed after revocation
are rejected regardless of when the revocation arrives.
Residual risk: Low — eventual consistency ensures
all nodes converge.
Information Disclosure
Threat: Private key file is world-readable
Attack surface: CLI auth (LocalAuth)
Scenario: Another user on the system reads the private
key from ~/.taba/keypair.
Mitigation: Private key file is created with 0600
permissions on Unix (owner-only read/write). Non-Unix
systems use std::fs::write as a fallback (no permission
control, but non-Unix is not a production target).
Residual risk: Low on Unix, Medium on non-Unix
(documented limitation).
Threat: Data classification leakage through taint
Attack surface: Taint propagation, provenance traversal Scenario: A workload processes PII data and produces output classified as Public, leaking the PII. Mitigation: Taint propagation (INV-S4) computes classification at query time by traversing the provenance graph. Multi-input workloads inherit the union (most restrictive) of all input classifications. Declassification requires multi-party signing (INV-S9). Residual risk: Low — the lattice model ensures classification can only become more restrictive without explicit declassification.
Threat: Logs contain sensitive data
Attack surface: Tracing, structured events
Scenario: Tracing logs contain unit payloads, private
keys, or other secrets.
Mitigation: The tracing crate is configured with
release_max_level_info. Unit payloads are never logged —
only metadata (unit ID, author, kind). Key material is
zeroized on drop and never serialized.
Residual risk: Low — logging is metadata-only by
design.
Denial of Service
Threat: Attacker floods the graph with units
Attack surface: Graph insertion, WAL Scenario: Attacker submits thousands of units per second, exhausting memory and WAL space. Mitigation: Graph memory limit (INV-R6) triggers auto-compaction at 80% and degraded mode at 100%. WAL segments are bounded (default 64 MB each). Compaction removes discardable entries. Residual risk: Medium — no rate limiting on insertion in M5 local mode. Production multi-node would use gossip rate limiting.
Threat: Erasure reconstruction storm
Attack surface: Erasure coding, reconstruction scheduler Scenario: Multiple nodes fail simultaneously, triggering many reconstructions that overwhelm surviving nodes. Mitigation: Reconstruction scheduler uses a priority queue (governance > policy > data > workload) with a circuit breaker. When queue depth exceeds threshold, new reconstructions are rejected (INV-R1, FM-13). Residual risk: Low — circuit breaker prevents cascading failures.
Threat: Sybil attack via rapid join/leave
Attack surface: Gossip membership Scenario: Attacker rapidly joins and leaves the cluster, disrupting membership convergence. Mitigation: 2-witness failure confirmation (INV-R3) prevents false positives from a single attacker. Suspected nodes remain in the pool with health=‘unknown’ (INV-R5). Residual risk: Medium — no rate limiting on join/leave in M4. FINDING-022 tracked.
Threat: YAML billion laughs
Attack surface: K8s converter, TOML parser
Scenario: A crafted YAML file with deeply nested aliases
causes exponential memory usage during parsing.
Mitigation: serde_yaml 0.9 does not support YAML
aliases (deprecated feature). The TOML parser uses toml
crate which does not support recursive structures.
Residual risk: Low — neither parser supports the
alias mechanism needed for billion laughs.
Elevation of Privilege
Threat: Author creates units outside their scope
Attack surface: Unit authoring, graph insertion
Scenario: Workload-scoped author attempts to create a
policy unit, or an author scoped to domain A creates units
in domain B.
Mitigation: ScopeChecker::check_author_scope (INV-S5)
is invoked at graph insertion for all unit types (FINDING-018
fix). Scope uniqueness (INV-S8) is enforced for
state-producing types (workload, data).
Residual risk: Low — scope is checked at the gate,
not just at authoring time.
Threat: Attacker escalates spawn depth beyond limit
Attack surface: Graph insertion, spawn context
Scenario: Workload at depth 5 declares spawn_depth: 1
to bypass the limit of 4.
Mitigation: DefaultGraph::insert computes the actual
spawn depth by walking the parent chain (FINDING-017 fix).
Declared depth must match computed depth. Exceeding
max_spawn_depth is rejected.
Residual risk: Low — depth is computed, not trusted.
Threat: Spawned task creates governance units
Attack surface: Task spawner, delegation tokens
Scenario: A spawned bounded task attempts to create a
policy or governance unit, escalating its authority.
Mitigation: DelegationValidator::check_governance_block
(INV-W4a) rejects spawned tasks attempting governance
operations. The delegation token grants operational
authority only.
Residual risk: None — governance block is absolute.
Threat: SLSA level forgery
Attack surface: Provenance verification
Scenario: Attacker creates a unit with slsa_level = 3
and an empty builder_signature, bypassing supply chain
verification.
Mitigation: DefaultProvenanceVerifier rejects empty
signatures when slsa_level >= 2 (FINDING-008 fix). When
trusted builders are configured, only signed provenance from
known builders is accepted.
Residual risk: Low — level 1 (no signature required)
is explicitly a progressive disclosure default.
Summary
| Threat Type | Count | Critical | High | Medium | Low | None |
|---|---|---|---|---|---|---|
| Spoofing | 3 | 0 | 0 | 1 | 2 | 0 |
| Tampering | 3 | 0 | 0 | 0 | 3 | 0 |
| Repudiation | 2 | 0 | 0 | 0 | 1 | 1 |
| Info Disclosure | 3 | 0 | 0 | 1 | 2 | 0 |
| DoS | 4 | 0 | 0 | 2 | 2 | 0 |
| Elevation | 4 | 0 | 0 | 0 | 3 | 1 |
| Total | 19 | 0 | 0 | 4 | 13 | 2 |
All Critical and High findings from the adversary implementation sweep (30 findings) have been resolved. The 4 remaining Medium risks are:
- Shamir ceremony compromise — inherent to the algorithm; mitigated by physical security
- No insertion rate limiting (M5 local mode) — production multi-node would use gossip rate limiting
- No join/leave rate limiting (M4) — FINDING-022 tracked
- Non-Unix key permissions — documented limitation; non-Unix is not a production target
Capability Enforcement
taba enforces a zero-access default (INV-S1): units access nothing unless explicitly declared and policy-approved.
How it works
The CapabilityEnforcer trait checks that every capability a unit
exercises is in its granted set. Unknown units or missing
capabilities result in denial.
#![allow(unused)]
fn main() {
pub trait CapabilityEnforcer {
fn check_access(&self, unit: &UnitId, capability: &Capability) -> Result<(), SecurityError>;
fn check_all(&self, unit: &UnitId, capabilities: &[Capability]) -> Result<(), SecurityError>;
}
}
check_all short-circuits on the first denial.
Capability matching
Capabilities are typed tuples: (cap_type, name, purpose?).
cap_type: the kind of resource (e.g., “storage”, “network”)name: the specific resource (e.g., “postgres-compatible”)purpose(optional): a qualifier that must match during composition (INV-K2)
When purpose is declared on a need, the provide must have the
same purpose. When omitted, any provide of the same type+name is
acceptable.
Capability lists are sorted lexicographically before matching to ensure determinism regardless of declaration order.
Fail-closed
Ambiguous security decisions are denied, not guessed (INV-S2). If the capability check is ambiguous or the policy is missing, access is denied.
Taint Propagation
Data classification is propagated through the provenance graph at query time (INV-S4), not cached at merge time. This makes taint eventually consistent across nodes.
Classification lattice
Public < Internal < Confidential < PII
- Single input: output inherits the input’s classification
- Multiple inputs: output inherits the union (most restrictive) of all input classifications
- Declassification: requires multi-party signing (INV-S9) — one policy-scoped signer + one data-steward-scoped signer
Hierarchy
Children can narrow freely (INV-S7) — a child data unit with more restrictive constraints than its parent is always allowed. Widening requires explicit policy.
Implementation
The TaintComputer trait traverses the provenance graph:
#![allow(unused)]
fn main() {
pub trait TaintComputer {
fn compute_taint(&self, data_unit: &UnitId) -> Result<Classification, SecurityError>;
fn validate_declassification(&self, policy_unit: &UnitId) -> Result<(), SecurityError>;
}
}
compute_taint walks backward from the data unit through all
producing workloads and their inputs. validate_declassification
checks that the declassification policy has at least 2 distinct
signer scopes (INV-S9).
Delegation Tokens
When a service is placed on a node, the author pre-signs a delegation token. The node uses this token to sign spawned bounded tasks on behalf of the author. The node never holds the author’s private key.
INV-W4: Delegation model
- Author signs a token binding:
service_id,node_id,trust_domain,LC range,max_spawns - Node validates the token before each spawn
- Spawn count is tracked and checked against
max_spawns - Token can be revoked
INV-W4a: Governance block
Delegation grants operational authority only. Spawned tasks cannot:
- Create policy units
- Create governance units
- Participate in multi-party declassification
The DelegationValidator::check_governance_block method
enforces this at spawn time.
Token lifecycle
create_token → use (spawn) → revoke
↓
expired (LC range exceeded)
exhausted (spawn count exceeded)
revoked (explicit)
Zero-Access Default
taba’s security model starts from zero access. Every capability a unit needs must be explicitly declared and policy-approved. There is no implicit grant, no default permission, no “allow if not denied.”
How it works
- A workload unit declares
needs— capabilities it requires - The solver matches
needsagainstprovidesfrom other units - If a match exists, the
CapabilityEnforcerchecks that the match is permitted by policy - If no policy exists, the match is denied (fail-closed, INV-S2)
- Only after all checks pass does the unit enter
Composedstate
Contrast with Kubernetes
In Kubernetes, pods are “allowed by default” — network policies, RBAC, and admission controllers must explicitly deny access. taba inverts this: everything is denied until explicitly allowed.
Scope enforcement
Authors are parameterized by (unit_type_scope × trust_domain_scope):
- An author scoped to
workloadinacme-prodcannot create policy units or units inacme-staging - For state-producing types (workload, data), no two distinct authors may have identical scope tuples (INV-S8)
- For decision-making types (policy, governance), overlapping scopes are permitted (INV-S8a)
Troubleshooting
Common issues
taba init fails
| Error | Cause | Fix |
|---|---|---|
Io: Permission denied | Cannot create ~/.taba/ | Check home directory permissions, or use --state-dir |
Security: KeyError | Key generation failed | Ensure rand crate is available (Linux: /dev/urandom) |
taba apply fails
| Error | Cause | Fix |
|---|---|---|
TomlParse | Invalid TOML syntax | Validate with taba unit validate <file> |
Core: MalformedUnit | Missing required fields | Check the unit authoring guide |
Graph: SignatureRejected | Signature verification failed | Ensure the unit is signed by a known author |
Graph: ScopeViolation | Author lacks scope | Check role assignments in governance units |
Graph: MemoryLimitExceeded | Graph at capacity | Run taba compose to trigger compaction, or increase memory limit |
taba compose produces no placements
| Cause | Fix |
|---|---|
| No units in graph | Run taba unit list to verify |
| All units unplaceable | Check unplaceable section in output for reasons |
| Solver version mismatch | All nodes must agree on solver version (FM-12) |
taba status shows Degraded mode
| Reason | Action |
|---|---|
ErasureThresholdExceeded | Add nodes or reduce resilience_pct |
MemoryLimitExceeded | Run compaction or increase graph_memory_limit_bytes |
WalFailure | Check disk space, verify WAL directory |
OperatorTriggered | Run taba node normal to clear |
WAL corruption
If the WAL is corrupted (CRC mismatch on replay):
- The node stops replay at the corrupted position
- Returns
NodeError::WalCorrupted { position, reason } - Entries before the corruption are intact
- Entries after may be lost
- Recovery: delete the corrupted segment, restart the node
Node not joining cluster
- Check seed node addresses in
NodeConfig - Verify network connectivity (gossip uses UDP port 7946)
- Check attestation requirements (
require_tpmin config) - Verify the node’s keypair is valid (not revoked)
Debug logging
RUST_LOG=debug taba status
RUST_LOG=trace cargo test -p taba-graph
Getting help
Performance Tuning
Benchmarks
Graph and solver benchmarks use criterion.
cargo bench -p taba-graph
cargo bench -p taba-solver
Results (post-M7, 2026-09-15)
Graph operations
| Operation | N=10 | N=100 | N=1000 | N=5000 |
|---|---|---|---|---|
| insert | ~1.9 µs | ~13 µs | ~137 µs | ~710 µs |
| merge | ~18 µs | ~29 µs | ~155 µs | ~720 µs |
| snapshot | ~2.9 µs | ~34 µs | ~370 µs | ~6.8 ms |
| query_get | ~224 ns | ~206 ns | ~214 ns | ~216 ns |
| query_provenance | ~4.5 µs | ~46 µs | ~430 µs | ~6.1 ms |
| memory_estimate | ~61 ns | ~603 ns | ~6.0 µs | ~33.8 µs |
Solver operations
| Operation | N=10 | N=100 | N=500 |
|---|---|---|---|
| solve | ~2.6 µs | ~46 µs | ~770 µs |
| detect_conflicts | ~490 ns | ~23 µs | ~590 µs |
| detect_cycles | ~1.6 µs | ~25 µs | ~189 µs |
| rank_nodes | ~158 ns | ~3.6 µs | ~64 µs |
Memory usage
| N units | Estimated memory | Per unit |
|---|---|---|
| 10 | ~3.9 KB | ~400 B |
| 100 | ~39.1 KB | ~400 B |
| 1000 | ~390.6 KB | ~400 B |
| 5000 | ~1.9 MB | ~400 B |
Actual heap usage (including allocation overhead, string data) is likely 2-3× higher.
Tuning recommendations
Auto-compaction (INV-R6)
- Default: 1 GB memory limit, compaction at 80% (800 MB)
- At ~10,000 units (~3.9 MB), compaction triggers at ~3.1 MB
- For larger graphs: increase
graph_memory_limit_bytesor implement sharding (trust domain partitioning)
Solver performance
- The O(N²) conflict detection is the primary bottleneck
- At N=500, solve takes ~770 µs (acceptable for real-time)
- At N=10,000 (extrapolated), solve takes ~300 ms (unacceptable)
- Sharding by trust domain is the planned solution for N > 10,000
Insert performance
- Each insert calls
recompute_memory()which is O(N) - At N=5000, insert takes ~710 µs (sub-millisecond)
- A future optimization could use incremental memory tracking (add/subtract on insert/remove) instead of full recompute
Snapshot performance
- Snapshot clones all entries (O(N))
- At N=5000, takes ~6.8 ms due to cache effects (~1.9 MB of data)
- This is the most expensive graph operation
- The solver takes a snapshot before each run — consider
incremental solver updates (already implemented as
solve_incremental) to avoid full snapshots
Adversary Findings
Spec review (pre-architect)
Date: 2026-04-12 Status: Complete — all critical/high resolved
| Severity | Count | Status |
|---|---|---|
| Critical | 5 | All resolved |
| High | 32 | All resolved |
| Medium | 23 | Tracked |
| Low | 4 | Tracked |
See specs/findings/INDEX.md for the full list.
Architecture review
Date: 2026-04-13 Status: Complete — all critical/high resolved
| Severity | Count | Status |
|---|---|---|
| Critical | 4 | All resolved |
| High | 41 | All resolved |
Implementation sweep (post-M7)
Date: 2026-09-15 Status: Complete — 3 Critical resolved, 6 High tracked
| Severity | Count | Status |
|---|---|---|
| Critical | 3 | All resolved (WAL compaction, unsigned units, key permissions) |
| High | 6 | Tracked (SLSA fail-open, WAL replay, spawn depth, scope checker, witness verification, TOML injection) |
| Medium | 12 | Tracked |
| Low | 8 | Tracked |
| Info | 1 | Noted |
See specs/findings/adversary-implementation-sweep.md for the full list.
Resolved Critical findings
| ID | Title | Resolution |
|---|---|---|
| F-011 | Compaction deletes old segments before writing new ones | Write-then-delete: new segment is fsynced before old segments are removed |
| F-016 | Default graph accepts unsigned units | Documented: verifier is None by default in M5 local mode; must be explicitly enabled via with_verifier() |
| F-026 | Private key file world-readable | Use OpenOptions::mode(0o600) on Unix; non-Unix fallback via std::fs::write |
Remaining High findings (tracked)
| ID | Title | Component |
|---|---|---|
| F-008 | Empty builder_signature accepted by default — fail-open | taba-security/provenance.rs |
| F-012 | replay ignores the from parameter — replays from beginning | taba-node/wal.rs |
| F-017 | Spawn depth taken from unit’s own declaration, not computed | taba-graph/graph.rs |
| F-018 | Scope checker not invoked for workload units (and None by default) | taba-graph/graph.rs |
| F-020 | declare_failed does not verify witnesses are known or distinct | taba-gossip/swim.rs |
| F-028 | TOML injection via K8s metadata — unsanitized interpolation | taba-k8s/converter.rs |
CLI Reference
taba init
Initialize a local node. Generates an Ed25519 keypair, trust domain, and root governance unit.
taba init [--force] [--state-dir <DIR>]
| Flag | Default | Description |
|---|---|---|
--force | false | Re-initialize even if already initialized |
--state-dir | ~/.taba | State directory (keypair, config, graph) |
taba apply
Parse a TOML unit declaration, fill in identity, validate, and insert into the graph.
taba apply <FILE> [--dry-run] [--state-dir <DIR>] [--format <FORMAT>]
| Flag | Default | Description |
|---|---|---|
--dry-run | false | Validate and display without inserting |
--state-dir | ~/.taba | State directory |
--format | table | Output format: table or json |
taba unit list
List all active (non-archived) units in the graph.
taba unit list [--state-dir <DIR>] [--format <FORMAT>]
taba unit inspect
Show details of a specific unit.
taba unit inspect <ID> [--state-dir <DIR>] [--format <FORMAT>]
taba unit validate
Validate a TOML file without inserting into the graph.
taba unit validate <FILE>
taba unit archive
Archive a unit (soft-delete). Archived units are removed from the active set but retained for provenance integrity. Governance units cannot be archived (INV-G3).
taba unit archive <ID> [--state-dir <DIR>]
taba status
Show graph stats, operational mode, and node information.
taba status [--state-dir <DIR>] [--format <FORMAT>]
taba compose
Run the deterministic solver on the current graph. Shows placements, unplaceable units, and detected conflicts.
taba compose [--state-dir <DIR>] [--format <FORMAT>]
taba audit provenance
Show the provenance chain for a data unit.
taba audit provenance <UNIT-ID> [--state-dir <DIR>] [--format <FORMAT>]
taba audit trails
List all decision trails.
taba audit trails [--state-dir <DIR>] [--format <FORMAT>]
taba push
Push an artifact to the local peer cache. Computes SHA-256 and stores content-addressed.
taba push <FILE> [--cache-dir <DIR>]
taba-k8s convert
Convert K8s manifests to taba unit declarations.
taba-k8s convert <FILE> [--output-dir <DIR>] [--trust-domain <TD>] [--provenance]
| Flag | Default | Description |
|---|---|---|
--output-dir | stdout | Directory for generated .taba.toml files |
--trust-domain | — | Trust domain for generated units |
--provenance | false | Generate SLSA provenance placeholders |
Unit Declaration TOML Schema (DL-015)
The unit declaration format uses TOML for human authoring (A7). The schema follows progressive disclosure: simple cases are simple, complexity is available but never mandatory. One file = one unit.
Key principles:
- As simple as a Dockerfile for simple cases
- Richness available but not mandatory
- Defaults are the progressive disclosure path (INV-E3, INV-N5, INV-O3)
- Names scoped to trust domain (not globally unique)
- Duration strings: “50ms”, “30s”, “2h”, “7y”
- One file per unit — multi-unit bundles are a directory convention
Level 0 — Minimal workload (3 lines)
[unit]
name = "hello-web"
image = "hello:latest"
Everything else defaults:
type = "workload"(inferred fromimage)kind = "service"(default)- Trust domain: current (from
taba init) - Author: current (from keypair)
- Capabilities: none needed, none provided
- Health: OS-level process monitoring (INV-O3)
- Placement on failure: from environment (INV-N5: dev=leave-dead, prod=auto-replace)
- Scaling: min=1, max=1
Level 1 — Capabilities
[unit]
name = "api-server"
image = "api:v2.1.0"
digest = "sha256:abc123..."
[needs]
postgres = { type = "storage" }
redis = { type = "storage", purpose = "cache" }
[provides]
http-api = { type = "network", purpose = "user-facing" }
[scaling]
min = 2
max = 10
[[scaling.triggers]]
metric = "cpu_ppm"
threshold = 700_000 # 70%
direction = "up"
Capability shorthand
Capabilities are declared as tables under [needs] and [provides]. Each
key is the capability name. Value is a table with type (required) and
optional purpose qualifier (INV-K2).
# Full form:
postgres = { type = "storage", purpose = "primary" }
# Minimal form (type only):
postgres = { type = "storage" }
Level 2 — Behavioral contracts
[unit]
name = "payment-processor"
image = "payments:v3.0.0"
kind = "service"
[tolerates]
max_latency = "50ms"
consistency = "strong"
[failure]
on_oom = "backoff-inputs"
on_crash = { restart_with_backoff = 3 }
on_shutdown = { drain = "30s" }
[recovery]
strategy = "replay-from-offset"
stream = "payments-events"
[health]
type = "http"
path = "/healthz"
port = 8080
interval = "10s"
timeout = "2s"
Failure semantics
| Field | Values | Default |
|---|---|---|
on_oom | "backoff-inputs", "restart", "fail-permanent" | "restart" |
on_crash | { restart_with_backoff = N }, "unexpected" | { restart_with_backoff = 3 } |
on_shutdown | { drain = "duration" }, "immediate" | { drain = "30s" } |
Health check types (progressive, INV-O3)
| Type | Fields | Default |
|---|---|---|
"http" | path, port, interval, timeout | — |
"tcp" | port, interval, timeout | — |
"command" | command, interval, timeout | — |
| (omitted) | — | OS-level process monitoring |
Recovery strategies
| Strategy | Fields | Default |
|---|---|---|
"stateless" | — | (default) |
"replay-from-offset" | stream, offset (optional) | — |
"require-quorum" | min_peers | — |
Level 3 — Data unit
[unit]
name = "customer-profiles"
type = "data"
[schema]
format = "json-schema"
definition = "schemas/customer.json"
[classification]
level = "pii"
[retention]
mode = "persistent"
duration = "7y"
legal_basis = "GDPR Art. 6(1)(b)"
mandatory = true
[storage]
encrypted_at_rest = true
jurisdictions = ["EU", "CH"]
[provides]
customer-data = { type = "dataset", purpose = "analytics" }
Classification levels (INV-S4, INV-S7)
Lattice: "public" < "internal" < "confidential" < "pii"
Retention modes (INV-D2, INV-D4)
| Mode | Behavior | Default |
|---|---|---|
"persistent" | Governed by duration + legal_basis. Tombstoned on expiry. | (default) |
"ephemeral" | Auto-removed when producing bounded task terminates. Ref check determines tombstone vs full remove. | — |
"local-only" | Never enters graph. Requires policy for classification > public (INV-D5). | — |
Hierarchical data
[unit]
name = "eu-customers"
type = "data"
parent = "customer-profiles"
[classification]
level = "pii"
[storage]
jurisdictions = ["EU"]
# Inherits encrypted_at_rest from parent
# Narrows jurisdiction (always allowed per INV-S7)
Level 4 — Policy unit
[unit]
name = "allow-analytics-access"
type = "policy"
[conflict]
units = ["customer-profiles", "analytics-pipeline"]
capability = "customer-data"
[resolution]
action = "allow"
rationale = "Analytics pipeline has DPA and processes within EU jurisdiction"
Resolution actions
| Action | Behavior |
|---|---|
"allow" | Allow the composition unconditionally |
"deny" | Deny the composition |
"conditional" | Allow under conditions: conditions = ["condition1", "condition2"] |
Policy supersession
[unit]
name = "updated-analytics-policy"
type = "policy"
supersedes = "allow-analytics-access"
[conflict]
units = ["customer-profiles", "analytics-pipeline"]
capability = "customer-data"
[resolution]
action = "conditional"
conditions = ["processing within EU only", "audit trail enabled"]
rationale = "Updated per legal review 2026-03"
Level 5 — Bounded task with spawning
[unit]
name = "nightly-etl"
kind = "bounded-task"
image = "etl:v1.2.0"
[deadline]
wall_time = "2h"
[spawn]
max_depth = 2
max_count = 10
Bounded task fields
| Field | Type | Default |
|---|---|---|
kind | "bounded-task" | "service" |
deadline.wall_time | duration string | (required for bounded-task) |
deadline.lc_range | integer (logical clock delta) | (optional) |
spawn.max_depth | integer (1-4) | governed by INV-W3 default (4) |
spawn.max_count | integer | (required if spawning) |
Governance units
Governance units use type = "governance" with a governance_type field.
Trust domain definition
[unit]
name = "production-domain"
type = "governance"
governance_type = "trust-domain"
[trust_domain]
description = "Production workloads for ACME Corp"
Promotion gate (INV-E3)
[unit]
name = "prod-gate"
type = "governance"
governance_type = "promotion-gate"
[[transitions]]
from = "dev"
to = "test"
mode = "auto"
[[transitions]]
from = "test"
to = "prod"
mode = "human-approval"
Cross-domain capability advertisement (INV-X5)
[unit]
name = "shared-data-api"
type = "governance"
governance_type = "cross-domain-capability"
[cross_domain]
provides = { type = "dataset", name = "shared-metrics", purpose = "monitoring" }
conditions = "Bilateral policy required. Read-only access."
Artifact type shorthand
The [unit] section uses shorthand keys to infer artifact type:
| Key | Artifact type | Example |
|---|---|---|
image | OCI container | image = "myapp:v1.0" |
binary | Native binary | binary = "bin/myapp" |
wasm | WebAssembly module | wasm = "module.wasm" |
k8s | Kubernetes manifest | k8s = "pod-spec.yaml" |
microvm | MicroVM kernel | microvm = "vmlinux-5.10" |
MicroVM workloads also require kernel and rootfs paths:
[unit]
name = "secure-worker"
microvm = "vmlinux-5.10"
kernel = "/opt/vmlinux"
rootfs = "/opt/rootfs.ext4"
digest = "sha256:abc123..."
Only one artifact key per unit. digest (SHA256) is always recommended
and required for production environments (INV-A1).
Defaults summary
| Field | Default | Source |
|---|---|---|
type | "workload" | inferred from artifact key |
kind | "service" | — |
trust_domain | current | from taba init |
author | current | from keypair |
scaling.min | 1 | — |
scaling.max | 1 | — |
health | OS-level process monitoring | INV-O3 |
placement_on_failure | env-derived (dev=leave-dead, else=auto-replace) | INV-N5 |
failure.on_oom | "restart" | — |
failure.on_crash | { restart_with_backoff = 3 } | — |
failure.on_shutdown | { drain = "30s" } | — |
recovery.strategy | "stateless" | — |
retention.mode | "persistent" | — |
classification.level | "internal" | — |
File conventions
- Extension:
.taba.toml(e.g.,api-server.taba.toml) - One file per unit
- Multi-unit deployments: directory of
.taba.tomlfiles - Policy files can reference units by name (scoped to trust domain)
- Names must match
[a-z0-9][a-z0-9-]*(lowercase, hyphens, no dots)
K8s Migration
taba includes a K8s manifest converter (taba-k8s) that reads
existing Kubernetes YAML and generates taba unit declarations.
Quick start
# Convert a single deployment
taba-k8s convert deployment.yaml --output-dir ./units
# Apply the generated units
taba apply units/web-server.taba.toml
Supported resources
| K8s Resource | taba Unit | Mapping |
|---|---|---|
| Deployment | WorkloadUnit (service) | Container image → artifact, replicas → scaling |
| StatefulSet | WorkloadUnit (service) | + state recovery (require-quorum), PVC references |
| DaemonSet | WorkloadUnit (service) | + one-per-node constraint, node selector |
| Pod | WorkloadUnit (service) | Direct pod (no controller, no auto-scaling) |
| Service | WorkloadUnit (provides) | Network capability from ports |
| ConfigMap | DataUnit | Internal classification, text/plain schema |
| Secret | DataUnit | Confidential classification, encrypted_at_rest |
| NetworkPolicy | PolicyUnit | Pod selector → conflict units, ingress/egress → resolution |
| Role | GovernanceUnit (RoleAssignment) | Rules → unit_type_scope, namespace → trust_domain_scope |
| ClusterRole | GovernanceUnit (RoleAssignment) | Cluster-wide scope |
| RoleBinding | GovernanceUnit (RoleAssignment) | Binds Role to subjects |
| ClusterRoleBinding | GovernanceUnit (RoleAssignment) | Binds ClusterRole to subjects, cluster-wide |
Unmappable resources
The converter surfaces resources it cannot map, with a reason and suggestion:
| Resource | Reason | Suggestion |
|---|---|---|
| Ingress | taba has no ingress concept | Map the service backend directly |
| PersistentVolume | No direct taba equivalent | Use a data unit with storage_requirements |
| PersistentVolumeClaim | No direct taba equivalent | Use a data unit with storage_requirements |
| CRDs | Unbounded, no generic mapping | Write a custom taba unit declaration |
| Job | Bounded task (manual) | Set kind = “bounded-task”, add [deadline] |
| CronJob | Bounded task (manual) | Set kind = “bounded-task”, add [deadline] |
| HPA | Scaling triggers (manual) | Add triggers to [scaling] section |
| Namespace | No taba equivalent needed | — |
| ServiceAccount | No taba equivalent needed | — |
Multi-document YAML
The converter supports --- separated multi-document YAML:
taba-k8s convert multi-resource.yaml --output-dir ./units
Each resource is converted independently. The conversion report shows what was generated, skipped, and unmappable.
Conversion report
taba-k8s convert deployment.yaml
=== K8s → taba Conversion Report ===
Generated: 1 units
✓ web-server
Unmappable: 1 resources
✗ Ingress/web-ingress: taba has no ingress concept.
→ Map the service backend directly.
Total processed: 2
Provenance placeholders
taba-k8s convert deployment.yaml --provenance
Adds SLSA provenance placeholders to generated units, for filling in build system details (builder, source repo, digest, SLSA level).
Architecture Decision Records
ADRs document significant architectural decisions — the context, the decision, and the consequences. They are append-only: supersede old ADRs with new ones, don’t edit.
| ID | Title | Status |
|---|---|---|
| ADR-001 | Unit Model — four typed units | Accepted |
| ADR-002 | CRDT Graph — δ-state, no consensus | Accepted |
| ADR-003 | Capability Security — zero-default, fail-closed | Accepted |
| ADR-004 | No Masters — all nodes are peers | Accepted |
| ADR-005 | Erasure Coding — Reed-Solomon, not replication | Accepted |
| ADR-006 | Role Model — scoped authority | Accepted |
| ADR-007 | Runtime model — per-artifact-type dispatch | Accepted |
ADR-001: Self-describing typed units as the core infrastructure primitive
Status
Accepted
Context
Current infrastructure models separate workload description (container image + orchestration config) from operational semantics (controller logic in the orchestrator). This creates two problems simultaneously: the workload is too dumb (opaque black box with no behavioral contract) and the control plane compensates by being too smart (generic reconciliation engine with unbounded extension via CRDs).
The result is that control plane complexity is O(everything) regardless of actual workload complexity, and operational behavior is defined in controllers rather than in the workload itself.
Decision
The fundamental unit of deployment in taba is a typed, self-describing, contract-carrying unit. Units declare their capabilities, needs, tolerances, trust requirements, failure semantics, and recovery procedures as part of their definition — not as external configuration.
Unit types: workload, data, policy, governance.
The unit model is a type system for infrastructure: units carry typed contracts that the solver (composition engine) uses to match, validate, and place workloads.
Consequences
Positive
- Control plane complexity scales linearly with deployed complexity
- Security is structural (capability-based, zero default access)
- Data lineage is a natural byproduct of composition
- Audit trail is the composition graph itself
- Units are testable in isolation (contracts are verifiable)
Negative
- Higher upfront authoring cost compared to a Dockerfile
- Requires users to learn a new declaration format
- Migration from existing systems requires tooling
- The type system itself is a design surface that must be gotten right
Risks
- If the unit declaration format is too complex, adoption will fail
- If the type system is too restrictive, valid use cases will be blocked
- If the type system is too permissive, it provides no guarantees
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| Enhanced container + sidecar model | Familiar, incremental | Still opaque core, still needs external orchestrator | Doesn’t solve the fundamental split |
| FaaS / serverless | Simple unit, managed platform | Vendor lock-in, limited workload types | Too restrictive, sovereignty concerns |
| Configuration overlay on K8s | Leverages existing ecosystem | Still K8s complexity underneath | Doesn’t reduce fundamental complexity |
References
docs/vision/SYSTEM_VISION.md§ The unit modelspecs/domain-model.md(when written)
ADR-002: CRDT composition graph with no consensus protocol
Status
Accepted
Context
The composition graph (the system’s desired state) must be replicated across all nodes for resilience and to enable local solver computation. Traditional approaches use consensus protocols (Raft, Paxos) to ensure strong consistency of replicated state. Kubernetes uses etcd (Raft) for this purpose.
However, the taba unit and author model provides a property that makes consensus unnecessary: all mutations to the graph are authored by entities with non-overlapping scopes. Two authors cannot legitimately produce conflicting state for the same scope, and if they do, the solver detects it as a conflict requiring explicit policy resolution. The political consensus happened at authoring time, not at replication time.
Decision
The composition graph is a CRDT (Conflict-free Replicated Data Type), replicated peer-to-peer with no consensus protocol, no leaders, and no external metadata store.
Every unit in the graph is signed by its author. Nodes validate signatures before merging. The graph is erasure-coded across nodes for resilience.
The graph supports: append (new units), supersede (updated policies), hierarchical containment (data sub-units), and provenance chains.
Desired state is the CRDT graph. Actual state is what’s running locally. Each node reconciles independently.
Consequences
Positive
- No single point of failure (no leader, no etcd)
- No serialization bottleneck (no API server)
- Two architectural layers instead of four (K8s: etcd → API server → scheduler → kubelet)
- Scale-invariant: same protocol for 1 node or 1000
- Partition-tolerant by design
Negative
- Eventually consistent — brief windows where nodes see different graph state
- Custom CRDT implementation required (no off-the-shelf graph CRDT fits exactly)
- Debugging distributed state is harder without a single source of truth
- Graph compaction and garbage collection become the operator’s responsibility
Risks
- If the authoring model has a scope overlap bug, the CRDT cannot resolve it
- CRDT merge semantics must be formally verified (bugs here are system-wide)
- Erasure coding adds complexity to node join/leave operations
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| Raft consensus | Well-understood, strong consistency | Leader bottleneck, etcd-like SPOF | Contradicts no-masters design |
| External store (etcd, consul) | Mature, proven | External dependency, operational burden | Violates self-contained design |
| Full replication (no erasure) | Simpler | Higher bandwidth, wasteful | Doesn’t scale to large clusters |
References
docs/vision/SYSTEM_VISION.md§ The CRDT composition graphdocs/vision/SYSTEM_VISION.md§ Peer-to-peer architecturememory/OPEN_QUESTIONS.mdOQ-001 (CRDT variant selection)
ADR-003: Capability-based security with fail-closed conflict resolution
Status
Accepted
Context
Current infrastructure security is layered after the fact: container isolation (weak), namespace isolation (administrative), network policy (bolt-on), RBAC (separate system), secrets management (separate system), mTLS (sidecar). It’s defense-in-depth by accident, not design. The default is effectively “can reach anything unless explicitly denied.”
Decision
taba uses a capability-based security model where:
- The default is zero access — units can only reach what they explicitly declare
- Declarations that are compatible compose automatically (no policy needed)
- Declarations that conflict require an explicit policy unit to resolve
- No implicit resolution of security conflicts, ever
- Taint propagation is structural: sensitive data classifications propagate through the composition graph unless explicitly declassified by policy
- Every policy resolution creates an auditable artifact
The role model enforces this: authors have scoped authority (unit type × trust domain), and scope conflicts at the author level also fail closed.
Consequences
Positive
- Security is structural, not layered
- Blast radius of compromise is limited by capability declarations
- No lateral movement by default
- Audit trail for every security decision
- Policy surface area proportional to actual security complexity
Negative
- More upfront work for authors (must declare all capabilities)
- Simple deployments that “just work” in K8s may require explicit policy in taba
- Policy resolution requires human judgment — can’t be fully automated
Risks
- Overly restrictive defaults may frustrate users into workarounds
- Policy proliferation if declarations are too granular
- The fail-closed model means a missing policy blocks deployment entirely
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| RBAC (like K8s) | Familiar | Doesn’t compose, doesn’t propagate taints | Insufficient for the unit model |
| Allow-by-default + deny lists | Easier onboarding | Insecure default, easy to miss denials | Violates security-first design |
| Policy-as-code only (OPA) | Powerful, flexible | External system, not integrated with unit model | Can complement but not replace capability model |
References
docs/vision/SYSTEM_VISION.md§ Security modeldocs/vision/SYSTEM_VISION.md§ Role model
ADR-004: No master nodes — fully peer-to-peer architecture
Status
Accepted
Context
Kubernetes requires a separate control plane (etcd cluster + API server + scheduler
- controller manager) that must be provisioned, maintained, and scaled independently from worker nodes. This creates operational overhead, a class of “control plane down” failures, and an architectural phase transition between “dev mode” (minikube) and “production” (HA control plane).
Decision
taba has no master nodes. Every node is a peer running the same software with the same role. The control plane runs on the same nodes as workloads. There is no external metadata store, no leader election for normal operations, and no architectural distinction between “control plane node” and “worker node.”
Node join uses attestation (TPM when available, relaxed for dev). Node failure is detected via gossip (SWIM protocol). Graph resilience uses erasure coding across all nodes.
The system is scale-invariant: one node to thousands, same architecture, same protocols, different parameters (erasure coding ratio, gossip fanout).
Consequences
Positive
- No control plane failures (there is no separate control plane)
- No operational overhead of maintaining control plane infrastructure
- No architectural phase transition between dev and production
- Genuine linear scaling — add nodes, system gets more capacity and resilience
- Single binary to deploy (node daemon does everything)
Negative
- Some operations are inherently global (DNS, certificate roots) and need bootstrap
- Debugging is harder without a central point to query
- Monitoring requires aggregating from all nodes
- Initial cluster bootstrap requires at least one node to seed the graph
Risks
- Gossip protocol may not scale to very large clusters (>10,000 nodes) without hierarchical gossip or federation
- Erasure coding parameters need to adapt as fleet size changes
- Without a leader, “who decides” questions become “the math decides” — which requires the deterministic solver to be correct
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| Small fixed control plane (3-5 nodes) | Familiar, proven | Separate infra, SPOF class | Contradicts design goal |
| Elected leader for coordination | Simpler consistency | Leader is bottleneck and SPOF | Contradicts no-masters design |
| Hybrid (embedded leader per region) | Scalable, still distributed | Complex, two modes | Unnecessary if CRDT works |
References
docs/vision/SYSTEM_VISION.md§ Peer-to-peer architecturedocs/decisions/ADR-002-crdt-graph.md(CRDT eliminates need for consensus)memory/OPEN_QUESTIONS.mdOQ-008 (gossip parameters)
ADR-005: Erasure coding (not replication) for graph resilience
Status
Accepted
Context
The CRDT composition graph must survive node failures. Traditional approaches use full replication (3 copies of everything, as in etcd). This is wasteful and doesn’t scale — with 100 nodes, you still only have 3 copies. Erasure coding provides configurable resilience with lower storage overhead.
Decision
The composition graph is erasure-coded across active nodes. The coding parameters (n total shards, k required for reconstruction) are a function of fleet size and desired resilience level. Users configure the resilience property they want (“tolerate loss of 20% of nodes”) and the system computes the coding parameters.
Graph state is segmented per trust domain or composition scope, so losing nodes only requires reconstruction of the segments they held, and different segments can have different redundancy levels.
Consequences
Positive
- Storage efficient: k-of-n coding is much less overhead than 3x replication
- Scalable: redundancy adapts to fleet size automatically
- Per-segment coding allows differentiated resilience (GxP data gets higher redundancy)
- Reconstruction is distributed — no single node bears the full load
Negative
- Erasure coding has CPU overhead (encoding/decoding)
- Reconstruction after node failure takes time proportional to data volume
- More complex than simple replication — harder to reason about
- Node join/leave requires re-coding of affected segments
Risks
- If too many nodes fail simultaneously (more than n-k), data is lost
- Reconstruction during a cascading failure could make things worse
- The coding parameters need to adapt dynamically — getting this wrong is dangerous
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| Full 3x replication | Simple, proven | Wasteful, fixed redundancy | Doesn’t scale, over-provisions small clusters |
| Chain replication | Efficient reads | Complex failure handling | Requires ordering, contradicts peer model |
| No redundancy (WAL only) | Simplest | Single node failure = data loss | Unacceptable for production |
References
docs/vision/SYSTEM_VISION.md§ Peer-to-peer architecturememory/OPEN_QUESTIONS.mdOQ-002 (algorithm selection)
ADR-006: Role model — one primitive parameterized by unit type scope and trust domain scope
Status
Accepted
Context
Traditional RBAC systems suffer from role explosion as organizations grow. K8s RBAC is a separate system from workload management, leading to drift between what’s configured and what’s enforced. The taba unit model needs a role system that’s consistent with its own design philosophy: typed, composable, auditable.
Decision
There is one role primitive, parameterized by two dimensions:
- Unit type scope: which types of units can this author create (workload, data, policy, governance)
- Trust domain scope: in which trust domains can this author operate
This yields all necessary roles without RBAC explosion:
- Developer = workload author in their trust domain
- Security team = policy author across trust domains
- Data steward = data unit author in their trust domain
- Regulatory/QA = governance (certification) unit author
- Operator = all unit types in their trust domain + placement authority
Trust domains are themselves units, created through composition. Creating a trust domain is a conflict (multiple parties claim authority) that requires explicit policy resolution — the management ceremony IS the implementation.
Role complexity is proportional to: (number of unit types) × (number of trust domains). Both grow with organizational reality, not platform abstraction tax.
Consequences
Positive
- No RBAC explosion — dimensionality is fixed at two
- Roles map to existing organizational functions (no new structures needed)
- Audit is trivial — every unit has an author, every author has a scope
- Trust domain governance uses the same model as everything else
- Small team: one person, all scopes. Large org: many scoped instances. Same model.
Negative
- Trust domain boundaries must be well-defined (messy orgs = messy domains)
- “Who creates the first trust domain?” is a bootstrap problem
- Migration from existing RBAC systems requires mapping exercise
Risks
- If trust domains are drawn wrong, security boundaries are wrong
- Overly broad scopes (someone with all types × all domains) defeats the purpose
- The meta-authority problem (who governs trust domain creation) needs the Shamir root key ceremony, which is operationally heavy
References
docs/vision/SYSTEM_VISION.md§ Role modeldocs/decisions/ADR-003-capability-security.md(security model context)
ADR-007: Runtime model — per-artifact-type dispatch
Date: 2026-09-25 Status: Accepted
Context
taba supports four workload types: containers (OCI), microVMs
(Firecracker, cloud-hypervisor, QEMU), WebAssembly modules
(wasmtime), and native binaries. The RuntimeExecutor trait
abstracts how workloads are started, stopped, and monitored.
Previously, the CLI’s LocalClient had a single DockerRuntime
field — all units were passed to Docker regardless of their
ArtifactType. Native, Wasm, and MicroVm workloads would fail
because Docker tried to pull their artifact references as Docker
images.
Decision
Introduce a RuntimeSelector that dispatches to the correct
RuntimeExecutor based on the unit’s ArtifactType:
ArtifactType | RuntimeExecutor | Detection |
|---|---|---|
Oci | DockerRuntime (bollard) | Docker::connect_with_defaults() |
Native | NativeRuntime (std::process) | Always available |
Wasm | WasmRuntime (state tracking) | Always available |
MicroVm | MicroVmRuntime (subprocess) | which firecracker, which cloud-hypervisor, which qemu-system-* |
RuntimeSelector::new() detects all available runtimes on the
node. RuntimeSelector::available() returns a Vec<RuntimeCapability>
for node advertisement. RuntimeSelector::start(&unit) dispatches
to the correct executor.
LocalClient replaces docker: Option<DockerRuntime> with
runtime: Option<RuntimeSelector>. The reconcile() method
now uses the selector to start/stop units of any type.
Consequences
- Positive: All four workload types can be applied, composed, and reconciled via the CLI. The solver can place workloads on nodes with matching capabilities.
- Positive: New runtime types can be added by implementing
RuntimeExecutorand adding a case toRuntimeSelector. - Negative:
NativeRuntimeandWasmRuntimeare minimal implementations (state tracking, not full execution). Full Wasm execution requires adding thewasmtimecrate. - Negative:
MicroVmRuntimerequires a VM monitor binary (firecracker, cloud-hypervisor, or QEMU) to be installed.
Alternatives Considered
| Alternative | Pros | Cons | Why rejected |
|---|---|---|---|
| Docker-only | Simple, well-tested | Can’t run native/Wasm/MicroVm | Violates vision doc |
| One runtime per node | Simpler dispatch | Can’t mix workload types | Violates INV-N2 |
| Plugin system | Extensible | Over-engineered for 4 types | Premature abstraction |