Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

taba

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

  1. Self-describing typed units — workload, data, policy, governance. Each carries capability declarations, behavioral contracts, and security requirements.
  2. 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.
  3. Security as first class — zero-access default, capability-based, fail-closed on conflicts. Every unit is signed by its author. Taint propagation is structural.
  4. Data as first-class unit — datasets carry schema, classification, provenance, retention, and consent. Lineage falls out of the composition graph.
  5. Peer-to-peer — no masters, no leaders, no external metadata store. CRDT graph, erasure-coded, gossip membership.

Load-bearing decisions

DecisionRationale
No mastersAll nodes are peers. Same binary, same protocol, 1 node or 10,000.
CRDT graphNo consensus for normal operations. Eventually consistent, partition-tolerant.
Fail closedSecurity conflicts are never implicitly resolved.
Deterministic solverSame graph + same nodes = same placement on any node. Fixed-point arithmetic (ppm).
Signed unitsEvery unit is signed with context binding (trust domain, cluster, validity window).
Erasure codingNot 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

TierWhatWhenCommand
1 (fast)Unit tests + BDD @smoke + integrationBetween every editjust or just test
2 (slow)Tier 1 + slow-marked tests + full BDDPre-PRjust test-slow
3 (full)Tier 2 + slow integration testsPre-merge / nightlyjust 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:

ProjectLanguagePurpose
pactRustHPC configuration management
latticeRustHPC workload scheduling
sovraGoFederated key management
tabaRustNext-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 0600 permissions)
  • 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:

  1. Parsed from TOML
  2. Identity filled in (author, trust domain, cluster ID from your keypair)
  3. Signed with Ed25519 (INV-S3)
  4. Structurally validated (DefaultValidator)
  5. Scope-checked (INV-S8: no duplicate author scopes)
  6. Signature verified by the graph’s verifier
  7. Inserted into the composition graph
  8. 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

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 from image)
  • 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

FieldValuesDefault
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

TypeFieldsDefault
"http"path, port, interval, timeout—
"tcp"port, interval, timeout—
"command"command, interval, timeout—
(omitted)—OS-level process monitoring

Recovery strategies

StrategyFieldsDefault
"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

ModeBehaviorDefault
"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

ActionBehavior
"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

FieldTypeDefault
kind"bounded-task""service"
deadline.wall_timeduration string(required for bounded-task)
deadline.lc_rangeinteger (logical clock delta)(optional)
spawn.max_depthinteger (1-4)governed by INV-W3 default (4)
spawn.max_countinteger(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

KeyArtifact typeExample
imageOCI containerimage = "myapp:v1.0"
binaryNative binarybinary = "bin/myapp"
wasmWebAssembly modulewasm = "module.wasm"
k8sKubernetes manifestk8s = "pod-spec.yaml"
microvmMicroVM kernelmicrovm = "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

FieldDefaultSource
type"workload"inferred from artifact key
kind"service"—
trust_domaincurrentfrom taba init
authorcurrentfrom keypair
scaling.min1—
scaling.max1—
healthOS-level process monitoringINV-O3
placement_on_failureenv-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.toml files
  • 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

  1. Capability matching — For each unit with needs, the solver searches the graph for units with matching provides. 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).

  2. 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
  3. 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.

  4. 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).

  5. 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

OperationN=10N=100N=500N=1000 (extrapolated)
solve2.6 µs46 µs770 µs~3 ms
detect_conflicts490 ns23 µs590 µs~2.4 ms
detect_cycles1.6 µs25 µs189 µs~380 µs
rank_nodes158 ns3.6 µs64 µ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

RuntimeArtifactTypeTOML keyExample
ContainerOciimageimage = "myapp:v1.0"
Native binaryNativebinarybinary = "/bin/daemon"
WebAssemblyWasmwasmwasm = "module.wasm"
MicroVMMicroVmmicrovmmicrovm = "vmlinux-5.10"
K8s manifestK8sManifestk8sk8s = "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

  1. Capability filter (hard constraint): DefaultCapabilityFilter checks if the node’s RuntimeCapability list matches the workload’s ArtifactType. Nodes without a matching capability are excluded.

  2. Placement scorer (soft constraint): DefaultPlacementScorer ranks eligible nodes by resource availability, environment match, author affinity, and health status. Exact runtime matches get a bonus.

  3. RuntimeSelector: At reconcile time, RuntimeSelector dispatches to the correct RuntimeExecutor (DockerRuntime, NativeRuntime, WasmRuntime, or MicroVmRuntime) based on the unit’s ArtifactType.

Node capability advertisement

Nodes advertise their capabilities via the NodeCapabilitySet:

CapabilityDescriptionDetection
OciDocker/Podman with root daemonDocker::connect_with_defaults()
OciRootlessRootless Docker/PodmanSocket probe
K8sKubernetes API accesskubectl available
WasmWebAssembly runtimeAlways available
NativeNative binary executionAlways available
MicroVmVM 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 Resourcetaba UnitMapping
DeploymentWorkloadUnit (service)Container image → artifact, replicas → scaling
StatefulSetWorkloadUnit (service)+ state recovery (require-quorum), PVC references
DaemonSetWorkloadUnit (service)+ one-per-node constraint, node selector
PodWorkloadUnit (service)Direct pod (no controller, no auto-scaling)
ServiceWorkloadUnit (provides)Network capability from ports
ConfigMapDataUnitInternal classification, text/plain schema
SecretDataUnitConfidential classification, encrypted_at_rest
NetworkPolicyPolicyUnitPod selector → conflict units, ingress/egress → resolution
RoleGovernanceUnit (RoleAssignment)Rules → unit_type_scope, namespace → trust_domain_scope
ClusterRoleGovernanceUnit (RoleAssignment)Cluster-wide scope
RoleBindingGovernanceUnit (RoleAssignment)Binds Role to subjects
ClusterRoleBindingGovernanceUnit (RoleAssignment)Binds ClusterRole to subjects, cluster-wide

Unmappable resources

The converter surfaces resources it cannot map, with a reason and suggestion:

ResourceReasonSuggestion
Ingresstaba has no ingress conceptMap the service backend directly
PersistentVolumeNo direct taba equivalentUse a data unit with storage_requirements
PersistentVolumeClaimNo direct taba equivalentUse a data unit with storage_requirements
CRDsUnbounded, no generic mappingWrite a custom taba unit declaration
JobBounded task (manual)Set kind = “bounded-task”, add [deadline]
CronJobBounded task (manual)Set kind = “bounded-task”, add [deadline]
HPAScaling triggers (manual)Add triggers to [scaling] section
NamespaceNo taba equivalent needed—
ServiceAccountNo 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 ID
  • graph.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:

  1. Each node runs taba init independently
  2. Nodes discover each other via seed nodes (gossip)
  3. Units are replicated via CRDT merge (commutative, associative, idempotent)
  4. Graph shards are erasure-coded across N nodes (k-of-n)
  5. 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.

FieldTypeDefaultDescription
cluster_idUUID(generated)Unique identifier for this cluster
resilience_pctu833Erasure coding resilience percentage. k = ceil(N * (1 - R/100))
graph_memory_limit_bytesu641,073,741,824Maximum memory for active graph per node (INV-R6)
shamir_total_sharesu85Total shares in Shamir ceremony
shamir_thresholdu83Minimum shares for reconstruction
max_data_hierarchy_depthu816Maximum depth for hierarchical data units
gossip_intervalDuration500msSWIM probe interval
gossip_suspicion_timeoutDuration5sBase suspicion timeout
witness_countu82Independent witnesses for failure (INV-R3)
indirect_probe_countu83Peers asked for indirect probes
retransmit_multiplieru84Piggyback rounds = mult × log₂(N)
max_piggyback_entriesu88Max membership changes per message
suspicion_multiplieru84Effective timeout = mult × ceil(log₂(N)) × interval
max_spawn_depthu84Maximum spawn depth for bounded tasks (INV-W3)
revocation_grace_windowOptionNoneGrace window in logical clock delta (INV-S3)
fleet_command_rate_limit_lcu641000Min LC delta between fleet commands of same type

Node configuration (NodeConfig)

FieldTypeDefaultDescription
wal_dirString“./wal”Directory for WAL segment files
listen_addrString“0.0.0.0:7946”Bind address for peer communication
seed_nodesVec[]Seed nodes for gossip bootstrap
require_tpmboolfalseWhether TPM attestation is required (A5)
environmentOptionNoneEnvironment tag (e.g., “env:dev”)
custom_tagsVec<(String, String)>[]Additional custom tags
archive_backendOptionNoneArchive 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)

TypeWhenDiscardable when
MergedUnit verified and merged into graphErasure-coded to cluster (durable beyond this node)
PendingUnit verified but references not satisfiedPromoted (refs arrived) or expired (1-hour timeout)
PromotedPreviously pending unit activatedImmediately 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):

  1. Ephemeral data (highest priority)
  2. Decision trails
  3. Terminated bounded tasks
  4. Superseded policies
  5. Terminated services
  6. 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)

TriggerThresholdAction
Memory pressure80% of limitAuto-compaction begins
Memory limit100% of limitNode enters Degraded mode
OperatorManualCompaction 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:

MetricDescription
modeCurrent OperationalMode (Normal, Degraded, Recovery)
units_runningCount of running workload units
units_failedCount of failed workload units
wal_size_bytesCurrent WAL size in bytes
graph_memory_bytesCurrent graph memory usage
graph_memory_limit_bytesConfigured memory limit
memory_pressure_pctMemory usage as percentage of limit (0-100)

Triggers

TriggerThresholdAction
Memory pressure≥ 80%Auto-compaction begins (INV-R6)
Memory limit≥ 100%Degraded mode (reason: MemoryLimitExceeded)
WAL failureI/O errorDegraded mode (reason: WalFailure)

Reconciliation

The Reconciler trait compares desired state (from solver placements) with actual state (what’s running on this node).

Reconciliation actions

DesiredActualAction
RunningStoppedStart the unit
RunningFailedRestart (respecting failure semantics)
RunningUnknownStart the unit
StoppedRunningDrain 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:

ExecutorArtifactTypeUse caseRequires
SimulatedRuntime—Tests, devNothing (in-memory state machine)
DockerRuntimeOciContainersDocker daemon (via bollard crate)
NativeRuntimeNativeStandalone binariesNothing (std::process)
WasmRuntimeWasmWebAssembly modulesNothing (state tracking)
MicroVmRuntimeMicroVmMicroVMsFirecracker, 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

ReasonDescription
CompromisedKey material was compromised
DepartedAuthor has left the organization
RotatedKey rotation — replaced by a new public key
AdministrativeAdministrative 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 init in 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:

  1. An authorized author initiates enrollment for a new node
  2. A new Ed25519 key pair is generated, private key split into n shares
  3. Existing nodes contribute shares to the new node
  4. Once k shares are collected, the new node reconstructs its key
  5. The key is zeroized; only the public key persists
  6. 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

TierNameFlowUse case
0Solotaba init (one command)Dev, single-operator
1Basicstart → add shares → complete with witnessSmall teams
2Password-protectedEach share encrypted with Argon2idProduction
3Offline two-factorSeed code + passwordHigh-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_secret over GF(2^8)
  • Returns a CeremonyId and 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 VerifyingKey from 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 Failed state

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 k shares 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 EnrollmentId and 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 k out of n existing 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:

ProviderSecurityUse case
SoftwareAttestationWeak (SHA-256 of binary + OS info)Dev, testing
TpmAttestationStrong (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

LevelDescriptiontaba enforcement
0No provenanceNo requirement (default)
1Build process documentedProvenance exists
2Hosted build serviceBuilder identified, source repo + digest
3Hardened build platformBuilder 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:

  1. Structural validation: all required fields present
  2. SLSA level check: provenance.slsa_level >= min_level
  3. Source digest comparison: if expected digest is provided, it must match exactly
  4. 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 tss feature

Use case: production, high-security environments.

Verification

#![allow(unused)]
fn main() {
pub fn verify_software_attestation(
    result: &AttestationResult,
    expected_nonce: &[u8],
) -> Result<(), SecurityError>
}

Checks:

  1. The quote matches the expected value (binary_hash + os + arch + nonce)
  2. The signature matches the quote (self-signed for software)
  3. 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

  1. Self-describing typed units — workload, data, policy, governance. Each carries capability declarations, behavioral contracts, and security requirements.
  2. 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.
  3. Security as first class — zero-access default, capability-based, fail-closed on conflicts. Units are Ed25519-signed by their author. Taint propagation is structural.
  4. Data as first-class unit — datasets carry schema, classification, provenance, retention, and consent. Lineage falls out of the composition graph.
  5. 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

CrateResponsibilityTests
taba-commonIdentity newtypes, Ppm arithmetic, clocks, config31
taba-coreUnit model, capabilities, validation, contracts86
taba-securityEd25519, scope, taint, delegation, ceremony, SLSA106
taba-graphδ-state CRDT composition graph, merge, policy chains162
taba-solverDeterministic placement, conflict detection, cycles143
taba-observeDecision trails, structured events, health, Prometheus45
taba-nodeWAL, reconciler, runtime (Docker, Native, Wasm, MicroVm), health, mode, discovery97
taba-gossipSWIM membership, signed messages, failure detection41
taba-erasureReed-Solomon GF(2^8), shard distribution, reconstruction78
taba-cliclap CLI (init, apply, unit, status, compose, audit, reconcile, daemon)55
taba-k8sK8s manifest converter31
taba-acceptanceBDD acceptance tests (cucumber-rs, 268 scenarios)—
taba-test-harnessBuilders, InMemoryUnitStore, proptest strategies33
taba-integrationEnd-to-end integration tests15

Total: 960 unit tests + 47 e2e tests + 268 BDD scenarios.

Key invariants

IDDescription
INV-C1Graph is the single source of desired state
INV-C2CRDT merge: commutative, associative, idempotent
INV-C3Solver determinism: fixed-point, no float
INV-C4WAL-before-effect
INV-C6Composition is order-independent
INV-C7One non-revoked policy per conflict tuple
INV-S1Zero-default capabilities
INV-S2Security conflicts fail closed
INV-S3Signed units, causal revocation
INV-S4Taint propagation at query time
INV-S5Author scope enforcement
INV-S7Data hierarchy: narrow freely, widen with policy
INV-S8Unique author scopes (state-producing)
INV-S9Multi-party declassification
INV-S10Multi-party trust domain creation
INV-W3Spawn depth ≤ 4
INV-R32-witness failure confirmation
INV-R6Graph memory limit, auto-compaction at 80%
INV-G3Governance units exempt from compaction
INV-O1Every 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, Ppm arithmetic
  • config – TOML deserialization, validation, defaults
  • proto – prost/tonic build output
  • tracing – 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: Vec), PromotionTransition (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):

  • GraphSnapshot trait: immutable point-in-time graph view. Implemented by taba-graph. Consumed by taba-solver. Wrapped in Arc for safe concurrent access — snapshot is frozen at creation, never mutated.
  • MembershipSnapshot: current cluster membership view. Implemented by taba-gossip. Consumed by taba-solver.
  • Wal trait: 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, matching
  • contract – tolerance, trust, scaling, failure, recovery, placement-on-failure
  • data – classification lattice, provenance, retention (persistent/ephemeral/ local-only), hierarchy (max depth 16)
  • policy – conflict tuples, resolution, supersession, promotion policies
  • governance – trust domain, role assignment, certification, operational command, promotion gate, cross-domain capability advertisement subtypes
  • validation – well-formedness checks, cross-field consistency, spawn depth
  • artifact – artifact type, ref, digest, requires
  • delegation – delegation token type, scope, lifetime
  • tombstone – tombstone type, reference preservation
  • node_capability – capability set, runtime capabilities, environment tags, privilege level, custom tags
  • health – 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, zeroize
  • signing – unit signing with bound context
  • verification – 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 enforcement
  • taint – provenance graph traversal, classification union for multi-input, declassification multi-party check
  • ceremony – Shamir split/reconstruct, ceremony state machine, witness tracking, tier-specific logic
  • revocation – 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 checks
  • merge – merge entry point, signature verification gate (calls taba-security), causal reference resolution
  • pending – pending queue, reference tracking, promotion logic
  • provenance – chain construction, query
  • hierarchy – data unit parent-child relationships, constraint inheritance
  • wal – WAL entry types, write-ahead semantics, replay on startup
  • compaction – retention-driven cleanup, auto-compaction at 80% memory, archival of cold subgraphs
  • snapshot – immutable graph snapshot for solver consumption
  • query – 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 matching
  • capability_filter – hard constraint filtering: runtime matching (artifact.type → node runtime capability), environment tags, privilege requirements, custom tag matching
  • resource_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 validation
  • policy – policy lookup, supersession chain resolution, validity check
  • scaling – 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 GraphSnapshot defined in taba-core, implemented by taba-graph, wrapped in Arc for concurrent safety)
  • Membership (reads MembershipSnapshot defined 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 implementation
  • params – parameter computation (k = ceil(N * (1 - R/100)))
  • distribution – shard-to-node mapping, rebalancing
  • reconstruction – decode + signature re-verification (calls taba-security), priority queue, backpressure, circuit breaker
  • recoding – 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 probes
  • membership – membership view, state transitions, convergence tracking
  • failure – suspicion tracking, witness collection, 2-witness confirmation threshold, suspected-node retention (INV-R5)
  • messages – message types, signing (calls taba-security), verification
  • transport – UDP/TCP gossip transport, message serialization
  • priority – priority event dissemination (key revocation, version announcements)
  • capability – capability/resource advertisement, periodic resource snapshots
  • cross_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 handling
  • init – taba init flow: key generation, Tier 0 bootstrap, node setup (userspace or system)
  • reconciliation – desired-vs-actual comparison, action planning, execution
  • wal_manager – WAL lifecycle, replay on startup, periodic checkpoint
  • shard_store – on-disk shard persistence, retrieval
  • health – health status computation, peer health observation
  • health_check – health check orchestration (OS-level, HTTP, TCP, command)
  • mode – operational mode state machine, transition rules, degraded-mode freeze (no new placements), recovery throttling
  • runtime – workload execution via RuntimeSelector which dispatches to DockerRuntime (Oci), NativeRuntime (Native, std::process), WasmRuntime (Wasm, state tracking), or MicroVmRuntime (MicroVm, Firecracker/cloud-hypervisor/QEMU subprocess). RuntimeSelector::new() auto-detects available runtimes and advertises RuntimeCapability to the solver.
  • discovery – auto-discovery probes (Docker, Podman, K8s, Wasm, TPM, GPU), capability set construction, refresh on command
  • artifact – artifact fetching (peer cache → external), digest verification, P2P peer cache, push mode for air-gapped
  • spawner – 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:

  • PrometheusExporter trait — per-node metrics endpoint
  • OpenTelemetryExporter trait — trace/metric/log export
  • AlertWebhook trait — 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, query
  • replay – solver replay from historical trail (deterministic)
  • events – structured event types, emission, buffering
  • health – health check result aggregation, status computation
  • prometheus – Prometheus exposition format endpoint
  • otel – OpenTelemetry SDK integration
  • alert – 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, archive
  • compose – trigger composition, inspect result, list conflicts
  • status – node health, graph stats, placement map, operational mode
  • policy – create, supersede, list, inspect resolution chain
  • promote – create promotion policy for a unit version + environment
  • trust-domain – create (ceremony), inspect, list members
  • audit – lineage query, provenance chain, taint report, decision trail
  • ceremony – initiate Shamir ceremony, add share, complete
  • node – join, leave, drain, health
  • refresh – re-probe capabilities (single node)
  • fleet – fleet-wide commands (refresh-capabilities, etc.)
  • observe – query decision trails, solver replay, health status
  • push – 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 dispatch
  • client – 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 types
  • fakes – in-memory trait implementations
  • strategies – proptest Arbitrary implementations
  • bdd – cucumber-rs step helpers, test cluster setup
  • assertions – 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):

MilestoneCrates requiredCapability
M1: Types compiletaba-common, taba-core, taba-test-harnessUnit declarations parse and validate
M2: Single-node compose+ taba-security, taba-graph, taba-solverCompose units on one node, signed, with conflict detection
M3: Persistent+ taba-node (WAL only)Survives restart, local reconciliation
M4: Multi-node+ taba-gossip, taba-erasureDistributed operation, failure tolerance
M5: Usable+ taba-cliHuman-operable system
M6: Hardenedtaba-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:

  • GraphSnapshot trait: defined in taba-core, implemented in taba-graph. Wrapped in Arc for concurrent immutable access (A006).
  • MembershipSnapshot type: defined in taba-core, populated by taba-gossip.
  • Wal trait: 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).
  • CompositionGraph trait: defined in taba-graph, implemented there
  • Solver trait: defined in taba-solver, implemented there
  • Signer/Verifier traits: defined in taba-security, implemented there
  • ShardStore trait: defined in taba-node, implemented there and in taba-test-harness (in-memory)
  • MembershipService trait: 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

MilestoneCratesCapability
M1: Types compilecommon, coreUnit declarations parse and validate
M2: Single-node compose+ graph, solver, securityCompose units on one node
M3: Persistent+ node (WAL only)Survives restart
M4: Multi-node+ gossip, erasureDistributed operation
M5: Usable+ cliHuman-operable
M6: Hardened+ security advancedProduction-grade security
M7: Migration+ k8s toolK8s 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 tests in 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-crate tests/
  • 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-level tests/
  • 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-harness crate 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 proptest strategies

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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-S1: Zero-default capabilitiestaba-securityCapabilityGuard::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 closedtaba-solverSolver::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 mergetaba-security, taba-graphSignatureVerifier::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 propagationtaba-core, taba-securityTaintResolver::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 enforcementtaba-securityScopeValidator::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 creationtaba-securityMultiPartyValidator::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/wideningtaba-core, taba-securityClassificationValidator::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 scopestaba-securityScopeValidator::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 declassificationtaba-securityMultiPartyValidator::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-securityMultiPartyValidator::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-C1: Graph is single source of desired statetaba-graph, taba-coreGraph::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 propertiestaba-graphCrdtMerge::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-solverSolver::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-effecttaba-graphWal::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 conflictstaba-corePolicyValidator::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 independencetaba-solver, taba-graphSolver::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 conflicttaba-corePolicyValidator::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/mergeCoreError::PolicyConflict. New policy rejected unless it explicitly supersedes existing.

Composition Invariants (INV-K1 through INV-K5)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-K1: All capability needs satisfiedtaba-solverSolver::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 matchingtaba-solverCapabilityMatcher::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 tolerancestaba-solverSolver::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 parameterstaba-solverSolver::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-evaluationCoreError::ScalingParameterInvalid. Unit flagged. No scaling applied until fixed.
INV-K5: Cyclic recovery dependencies fail closedtaba-solverSolver::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-D1: Unbroken provenance chaintaba-coreProvenanceValidator::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 enforcementtaba-coreRetentionEnforcer::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 queryCoreError::RetentionExpired. Unit eligible for compaction. Compactor removes from active graph.
INV-D3: No redundant childrentaba-coreHierarchyValidator::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-R1: Node failure does not corrupt graphtaba-erasureErasureCodec::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 CRDTtaba-graph, taba-gossipCrdtMerge::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 messagestaba-gossipGossipProtocol::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 thresholdtaba-erasureErasureCodec::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 checksErasureError::ThresholdExceeded, ErasureError::InsufficientShards. System enters degraded mode. Operator alert.
INV-R5: Suspected nodes stay in placement pooltaba-gossip, taba-solverSolver::score_placement(unit, node) with NodeHealth::Unknown handlingSuspected 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-compactiontaba-graph, taba-nodeMemoryMonitor::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-E1: Promotion policy gates placement by envtaba-solverPromotionEvaluator::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 cumulativetaba-solverPromotionEvaluator::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 autotaba-solverPromotionEvaluator::evaluate()If no PromotionGate governance unit exists, all transitions default to auto-promote. Zero config = full auto.On promotion evaluationStructural — default behavior when governance unit absent.

Node Capability Invariants (INV-N1 through INV-N5)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-N1: Auto-discovery on startup, cachedtaba-nodeCapabilityDiscoverer::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 constraintstaba-solverCapabilityFilter::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 constraintstaba-solverResourceRanker::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 capabilitiestaba-solverCapabilityFilter::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 envtaba-solver, taba-nodeSolver::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-A1: SHA256 digest verificationtaba-nodeArtifactFetcher::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 firsttaba-nodeArtifactFetcher::fetch()Check peer cache before external source. Optimization, not security boundary — INV-A1 digest verification is the integrity guarantee.On artifact fetchBehavioral — if peer has artifact, skip external download.

Observability Invariants (INV-O1 through INV-O3)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-O1: Every solver run produces decision trailtaba-observeDecisionTrailRecorder::record(snapshot, membership, result, version)Node calls recorder after each solver invocation. Trail persisted to graph via WAL.After every solver runObserveError::TrailNotFound if recording fails. Alert. Solver continues (observability failure does not block placement).
INV-O2: Trail retention since last compactiontaba-observe, taba-graphDecisionTrailQuery::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 queryObserveError::TrailCompacted if querying beyond retention.
INV-O3: Progressive health checkstaba-nodeHealthCheckOrchestrator::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-T1: Monotonic logical clock, sync on communicationtaba-commonLogicalClock::tick(), LogicalClock::sync(remote)Every system action increments. On inter-node communication: local = max(local, remote) + 1.On every action and every gossip/merge messageStructural — u64 overflow at 1.8×10^19 events. No practical violation.
INV-T2: Dual clock modeltaba-commonDualClockEvent triple recorded on every eventLogical clock for ordering, wall clock for retention/compliance. Both recorded. System chooses by operation type.On every eventStructural — type system enforces recording both clocks.
INV-T3: Causal revocationtaba-securityKeyManager::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)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-W1: Services valid indefinitelytaba-coreUnitHeader::validity = NoneNo 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-terminatetaba-nodeReconciler::reconcile(), deadline check in reconciliation loopCheck 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 mergetaba-graph, taba-coreGraph::merge() pre-check, SpawnContext::spawn_depth validationOn merge: traverse spawn provenance chain, compute depth. Reject if > max (default 4).On graph merge of spawned task unitGraphError::SpawnDepthExceeded. Unit rejected.
INV-W4: Delegation token signingtaba-securityDelegationValidator::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 unitSecurityError::DelegationExpired, DelegationSpawnLimitExceeded, DelegationTokenForged. Unit rejected.
INV-W4a: No governance via delegationtaba-securityDelegationValidator::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 typeSecurityError::DelegationGovernanceBlocked. Unit rejected.

Data Lifecycle Invariants (INV-D4 through INV-D5)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-D4: Ephemeral data reference checktaba-graphCompactor::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 terminationBehavioral — no error. Reference check determines treatment.
INV-D5: Local-only requires policy for classified datataba-core, taba-securityRetentionValidator::validate_local_only(data_unit)If retention = LocalOnly and classification > Public, require explicit policy authorization.On unit creation / validationCoreError::LocalOnlyRequiresPolicy. Unit rejected without policy.

Compaction Invariants (INV-G1 through INV-G5)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-G1: Compaction eligibility deterministictaba-graphCompactor::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 sweepStructural — same algorithm on same input = same output.
INV-G2: Tombstones preserve provenancetaba-graphCompactor::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 compactedtaba-graphCompactor::is_exempt(unit)Skip governance units, active policies, root ceremony chain during compaction sweep.On compaction sweepStructural — exemption check before tombstoning.
INV-G4: Eviction ≠ compactiontaba-nodeMemoryManager::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 ordertaba-graphCompactor::compact(graph, target_bytes)Priority: ephemeral → trails → terminated tasks → superseded policies → terminated services → expired data. Mirrors reconstruction priority inverse.On compaction sweepStructural — priority ordering in compaction algorithm.

Cross-Trust-Domain Invariants (INV-X1 through INV-X6)

InvariantCrateTrait::methodCheckWhenOn Violation
INV-X1: Bilateral policy for cross-domaintaba-gossip, taba-solverCrossDomainGossip::validate_bilateral(query), CapabilityFilter::filter()Before executing forwarding query, verify bilateral policy in both domains. Missing either side = fail closed.On cross-domain forwarding queryGossipError::BilateralPolicyMissing. Query rejected. Composition blocked.
INV-X2: Read-only cross-domain viewstaba-gossipCrossDomainGossip::forward_query() returns ForwardingResultQuery results NOT merged into querying domain’s graph. Reference by UnitId only.On forwarding query responseStructural — ForwardingResult is a read-only type, not a mergeable unit.
INV-X3: Fail-open cache defaulttaba-gossipCrossDomainGossip::query_with_cache(cache_policy)Default: serve stale if bridge down. Governance override: fail closed for freshness.On cross-domain query with bridge unavailableBehavioral — cache served (default) or GossipError::BridgeUnavailable (strict mode).
INV-X4: Emergent bridge defaulttaba-gossipCrossDomainGossip::discover_bridges(target_domain)Any node in multiple domains is a bridge. Governance can restrict to designated only.On bridge discoveryBehavioral — gossip queries all multi-domain nodes (or designated only).
INV-X5: Cross-domain capability via governancetaba-gossipCrossDomainGossip::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 capabilitytaba-solver, taba-gossipCapabilityFilter::filter(), CrossDomainGossip::discover_bridges()If no bridge to target domain, solver surfaces as unresolved. No auto-creation.On cross-domain composition attemptSolverError::NoCapableNode with detail “no bridge to domain X”. Alert raised.

INV-S8a: Overlapping Scopes for Decision-Making Types

InvariantCrateTrait::methodCheckWhenOn Violation
INV-S8a: Overlapping policy/governance scopestaba-securityScopeValidator::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 creationStructural — 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.

VariantDescriptionRetryableFM
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.

VariantDescriptionRetryableFM
SignatureInvalid { unit_id, author_id, reason }Cryptographic signature verification failed. Unit rejected at merge gate (INV-S3).FatalFM-04, FM-05
SignatureContextMismatch { unit_id, expected_cluster, actual_cluster }Unit signed for a different cluster/trust domain (cross-cluster replay).FatalFM-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).FatalFM-05
AuthorScopeExceeded { author_id, unit_type, trust_domain }Author attempted to create a unit outside their (type scope x trust domain scope) (INV-S5).FatalFM-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).FatalFM-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).FatalFM-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):

VariantUser message
SignatureInvalidError: Unit {unit_id} has an invalid signature from author {author_id}. The unit was rejected. Verify the signing key and re-sign the unit.
AuthorKeyRevokedError: Author {author_id}'s key was revoked at {revoked_at}. Units created after revocation are not accepted. Re-author with a valid key.
AuthorScopeExceededError: Author {author_id} is not authorized to create {unit_type} units in trust domain {trust_domain}. Check role assignments.
SecurityConflictUnresolvedError: Capability conflict on '{capability}' between units {units}. Create a policy unit to resolve this conflict before composition can proceed.
TrustDomainCreationRequiresMultiPartyError: Trust domain creation requires at least 2 distinct author signatures. Only {signers_present} provided.

taba-core: CoreError

Unit lifecycle, composition, and aggregate management.

VariantDescriptionRetryableFM
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.FatalFM-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).FatalFM-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).FatalFM-14
Security { source: SecurityError }Propagated from taba-security.Inherits–
Common { source: CommonError }Propagated from taba-common.Inherits–

User-facing messages (CLI-visible):

VariantUser message
CompositionFailedError: Composition failed. Unresolved capabilities: {list}. Ensure all required capability providers are declared and deployed.
CyclicRecoveryDependencyError: Circular recovery dependency detected: {cycle}. Add a policy unit declaring restart priority to break the cycle.
PlacementConstraintViolationError: Cannot place unit {unit_id}: constraint '{constraint}' unsatisfiable. {reason}. Consider relaxing tolerances or adding nodes.
PolicyConflictError: 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.

VariantDescriptionRetryableFM
MergeRejected { unit_id, reason }Unit failed pre-merge validation (signature, scope, etc.). Wraps SecurityError.FatalFM-04
CrdtViolation { operation, reason }Merge operation would violate commutativity/associativity/idempotency (INV-C2). Internal bug.FatalFM-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.FatalFM-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.FatalFM-08
InsertionOrderDependence { unit_ids, reason }Detected composition result differing by insertion order (INV-C6). Internal bug.FatalFM-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).FatalFM-23
ArchivalFailed { unit_id, backend, reason }Archive write failed for mandatory-archive unit (FM-21). Compaction blocked.RetryableFM-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):

VariantUser message
WalWriteFailedError: Cannot write to WAL at {path}: {source}. Node is pausing mutations. Check disk space and filesystem health.
WalCorruptedError: WAL corrupted at {path} offset {offset}. Node will recover graph state from peers. Manual intervention may be required.
MemoryLimitExceededWarning: 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.

VariantDescriptionRetryableFM
DeterminismViolation { input_hash, node_a_result, node_b_result }Two evaluations of the same input produced different output (INV-C3). Critical bug.FatalFM-06, FM-11
FloatingPointDetected { operation }Floating-point arithmetic detected in solver path. Compile-time lint preferred, runtime backstop (INV-C3).FatalFM-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):

VariantUser message
NoViableNodeError: No node can satisfy placement constraints for unit {unit_id}: {constraints}. Add nodes with the required capabilities or relax unit tolerances.
ConflictDetectedError: Capability conflict on '{capability}' between units {units}. Create a policy unit to resolve this conflict.
VersionSkewWarning: 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.

VariantDescriptionRetryableFM
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.RetryableFM-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.FatalFM-04

User-facing messages (CLI-visible):

VariantUser message
InsufficientShardsError: Only {available} of {required_k} required shards available (of {total_n} total). System entering degraded mode. Restore failed nodes to recover.
ReconstructionBackpressureWarning: Reconstruction queue depth ({queue_depth}) exceeds threshold ({threshold}). Pausing new reconstructions to prevent cascade. Monitor node health.
ThresholdExceededCritical: {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.

VariantDescriptionRetryableFM
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).FatalFM-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):

VariantUser message
BootstrapFailedError: Cannot reach any seed node ({seed_nodes}). Check network connectivity and seed node configuration.
PartitionDetectedWarning: Possible network partition. {reachable_nodes} reachable, {unreachable_nodes} unreachable. CRDT merge will reconcile on heal.
InsufficientWitnessesInfo: 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.

VariantDescriptionRetryableFM
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).RetryableFM-15
ArtifactDigestMismatch { expected, actual }Fetched artifact SHA256 does not match expected digest (INV-A1).FatalFM-15
CapabilityDiscoveryFailed { reason }Auto-discovery could not probe system capabilities (INV-N1).RetryableFM-17
SpawnFailed { service_id, reason }Bounded task spawn failed (delegation or depth).FatalFM-22, FM-23
Observe { source: ObserveError }Propagated from taba-observe.Inherits–

User-facing messages (CLI-visible):

VariantUser message
DegradedModeWarning: Node entering degraded mode: {reason}. New placements suspended. Resolve the underlying issue and the node will recover automatically.
AttestationFailedError: 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.

VariantDescriptionRetryableFM
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.

VariantDescriptionRetryableFM
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).FatalFM-11
ExportFailed { target, reason }Integration export failed (Prometheus, OTEL, webhook).Retryable–

User-facing messages (CLI-visible):

VariantUser message
TrailCompactedInfo: Decision trail {trail_id} has been compacted (beyond retention window). Increase decision_retention on the unit or trust domain to retain longer.
ReplayDivergenceCritical: 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.

FMPrimary Error(s)Crate(s)
FM-01: Single node failureGossipError::NodeUnreachable, ErasureError::ShardCorruptedtaba-gossip, taba-erasure
FM-02: Multiple node failuresErasureError::InsufficientShards, ErasureError::ThresholdExceeded, NodeError::DegradedModetaba-erasure, taba-node
FM-03: Network partitionGossipError::PartitionDetectedtaba-gossip
FM-04: Compromised nodeSecurityError::SignatureInvalid, GossipError::MessageSignatureInvalid, ErasureError::SignatureVerificationPostReconstruct, SecurityError::DelegationTokenForgedtaba-security, taba-gossip, taba-erasure
FM-05: Compromised authorSecurityError::AuthorKeyRevoked, SecurityError::AuthorScopeExceededtaba-security
FM-06: Wrong solver placementSolverError::DeterminismViolation, GraphError::CrdtViolation, GraphError::InsertionOrderDependencetaba-solver, taba-graph
FM-07: WAL corruption/disk fullGraphError::WalWriteFailed, GraphError::WalCorrupted, GraphError::WalReplayFailedtaba-graph
FM-08: Graph unbounded growthGraphError::MemoryLimitExceeded, GraphError::CompactionFailed, NodeError::DegradedModetaba-graph, taba-node
FM-09: Gossip false positiveGossipError::FalsePositiveDetected, GossipError::InsufficientWitnessestaba-gossip
FM-10: Conflicting legal requirementsSolverError::ConflictDetected, SecurityError::SecurityConflictUnresolved, CoreError::PolicyConflicttaba-solver, taba-security, taba-core
FM-11: Solver determinism regressionSolverError::DeterminismViolation, SolverError::FloatingPointDetected, ObserveError::ReplayDivergencetaba-solver, taba-observe
FM-12: Solver version skewSolverError::VersionSkew, GossipError::VersionAnnounceMismatchtaba-solver, taba-gossip
FM-13: Cascading reconstruction stormErasureError::ReconstructionBackpressure, ErasureError::ReconstructionFailedtaba-erasure
FM-14: Promotion policy collisionCoreError::PromotionPolicyCollisiontaba-core
FM-15: Artifact unavailableNodeError::ArtifactFetchFailed, NodeError::ArtifactDigestMismatchtaba-node
FM-16: Dev node offlineBehavioral (INV-N5 leave-dead default)taba-solver
FM-17: Stale capability discoveryNodeError::CapabilityDiscoveryFailedtaba-node
FM-18: Role succession gapSecurityError::AuthorKeyRevoked (break-glass via root key)taba-security
FM-19: Tier 0→1 upgrade failureSecurityError::CeremonyErrortaba-security
FM-20: Compaction removes referenced unitGraphError::CompactionFailed (reference check prevents)taba-graph
FM-21: Archive backend unavailableGraphError::ArchivalFailedtaba-graph
FM-22: Bounded task deadline exceededBehavioral (node forcefully terminates)taba-node
FM-23: Spawn depth exceededCoreError::SpawnDepthExceeded, GraphError::SpawnDepthExceededtaba-core, taba-graph
FM-24: Logical clock divergenceBehavioral (clock sync on reconnection)taba-common
FM-25: Bridge failure isolates domainsGossipError::BridgeUnavailabletaba-gossip
FM-26: Compromised bridgeSecurityError::SignatureInvalid (standard + wider blast radius)taba-security
FM-27: Stale cross-domain cacheGossipError::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

  1. Events are typed enums. Every variant carries the minimum payload needed for the consumer to act.
  2. Local events flow through tokio::broadcast or tokio::mpsc channels within a single node. They are never serialized to wire format.
  3. Gossip events flow through the signed gossip protocol (INV-R3). They are serialized to protobuf and signed with the sending node’s identity key.
  4. WAL entries are the durable record. An event that mutates graph state must be WAL’d before the mutation is visible (INV-C4).
  5. 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 typeOrderingMechanism
GraphEventCausal per unitTimestamp (wall + logical counter)
SolverEventTotal within evaluation passSequential solver execution
NodeEventCausal per (unit, node) pairReconciliation loop sequence
GossipMessage (membership)CausalLamport clock on MembershipUpdate
GossipMessage (graph delta)CausalVector clock on GraphDelta
GossipMessage (key revocation)None (idempotent)Priority broadcast
SecurityEventNone (synchronous gates)Inline in calling path
CeremonyEventTotal within ceremonySequential ceremony state machine
DistributionEventCausal (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 typeDeliveryWhy acceptable
GraphEventAt-most-once (broadcast)Graph state is source of truth; missed events self-heal on next query
SolverEventAt-least-oncePlacementDecision persisted to graph; reconciliation loop catches drops
NodeEventAt-least-onceHealth/state is idempotent (latest wins); reconciliation loop retries
GossipMessageAt-least-once (retransmit)CRDT merge is idempotent; membership converges
SecurityEventAt-most-once (local)Verification is synchronous gate; KeyRevocation is at-least-once via gossip
CeremonyEventAt-least-onceCeremony completion is critical bootstrap; must durably persist
DistributionEventAt-least-onceShard 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.

EventWAL entry typeNotes
GraphEvent::UnitMergedMerged(unit)Full unit payload, written before merge is visible (INV-C4)
GraphEvent::UnitPendingPending(unit, missing_refs)Unit + list of unsatisfied references
GraphEvent::UnitPromotedPromoted(unit_id)Reference to existing Pending entry
GraphEvent::PolicySupersededMerged(new_policy)New policy is a Merged entry; old policy remains (supersession is structural)
GraphEvent::SubgraphArchivedCompaction recordReferences archived unit IDs; WAL entries for those units may be reclaimed
SolverEvent::PlacementDecisionGraph annotationPlacement stored as graph metadata, WAL’d as part of graph mutation
NodeEvent::WorkloadStartedLocal state recordPer-node WAL for actual state tracking
NodeEvent::WorkloadFailedLocal state recordPer-node WAL for actual state tracking
GossipMessage::GraphDeltaMerged(unit) per unit in deltaEach unit in the delta is individually verified and WAL’d
GossipMessage::KeyRevocationRevocation recordDurable; must survive restart to prevent accepting revoked-key units
CeremonyEvent::CeremonyCompletedMerged(governance_unit)The root governance unit that seeds the graph
DistributionEvent::NodeFailedMembership state recordLocal 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 / DecisionRelevant 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 TypeCountCriticalHighMediumLowNone
Spoofing300120
Tampering300030
Repudiation200011
Info Disclosure300120
DoS400220
Elevation400031
Total19004132

All Critical and High findings from the adversary implementation sweep (30 findings) have been resolved. The 4 remaining Medium risks are:

  1. Shamir ceremony compromise — inherent to the algorithm; mitigated by physical security
  2. No insertion rate limiting (M5 local mode) — production multi-node would use gossip rate limiting
  3. No join/leave rate limiting (M4) — FINDING-022 tracked
  4. 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

  1. A workload unit declares needs — capabilities it requires
  2. The solver matches needs against provides from other units
  3. If a match exists, the CapabilityEnforcer checks that the match is permitted by policy
  4. If no policy exists, the match is denied (fail-closed, INV-S2)
  5. Only after all checks pass does the unit enter Composed state

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 workload in acme-prod cannot create policy units or units in acme-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

ErrorCauseFix
Io: Permission deniedCannot create ~/.taba/Check home directory permissions, or use --state-dir
Security: KeyErrorKey generation failedEnsure rand crate is available (Linux: /dev/urandom)

taba apply fails

ErrorCauseFix
TomlParseInvalid TOML syntaxValidate with taba unit validate <file>
Core: MalformedUnitMissing required fieldsCheck the unit authoring guide
Graph: SignatureRejectedSignature verification failedEnsure the unit is signed by a known author
Graph: ScopeViolationAuthor lacks scopeCheck role assignments in governance units
Graph: MemoryLimitExceededGraph at capacityRun taba compose to trigger compaction, or increase memory limit

taba compose produces no placements

CauseFix
No units in graphRun taba unit list to verify
All units unplaceableCheck unplaceable section in output for reasons
Solver version mismatchAll nodes must agree on solver version (FM-12)

taba status shows Degraded mode

ReasonAction
ErasureThresholdExceededAdd nodes or reduce resilience_pct
MemoryLimitExceededRun compaction or increase graph_memory_limit_bytes
WalFailureCheck disk space, verify WAL directory
OperatorTriggeredRun taba node normal to clear

WAL corruption

If the WAL is corrupted (CRC mismatch on replay):

  1. The node stops replay at the corrupted position
  2. Returns NodeError::WalCorrupted { position, reason }
  3. Entries before the corruption are intact
  4. Entries after may be lost
  5. Recovery: delete the corrupted segment, restart the node

Node not joining cluster

  1. Check seed node addresses in NodeConfig
  2. Verify network connectivity (gossip uses UDP port 7946)
  3. Check attestation requirements (require_tpm in config)
  4. 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

OperationN=10N=100N=1000N=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

OperationN=10N=100N=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 unitsEstimated memoryPer 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_bytes or 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

SeverityCountStatus
Critical5All resolved
High32All resolved
Medium23Tracked
Low4Tracked

See specs/findings/INDEX.md for the full list.

Architecture review

Date: 2026-04-13 Status: Complete — all critical/high resolved

SeverityCountStatus
Critical4All resolved
High41All resolved

Implementation sweep (post-M7)

Date: 2026-09-15 Status: Complete — 3 Critical resolved, 6 High tracked

SeverityCountStatus
Critical3All resolved (WAL compaction, unsigned units, key permissions)
High6Tracked (SLSA fail-open, WAL replay, spawn depth, scope checker, witness verification, TOML injection)
Medium12Tracked
Low8Tracked
Info1Noted

See specs/findings/adversary-implementation-sweep.md for the full list.

Resolved Critical findings

IDTitleResolution
F-011Compaction deletes old segments before writing new onesWrite-then-delete: new segment is fsynced before old segments are removed
F-016Default graph accepts unsigned unitsDocumented: verifier is None by default in M5 local mode; must be explicitly enabled via with_verifier()
F-026Private key file world-readableUse OpenOptions::mode(0o600) on Unix; non-Unix fallback via std::fs::write

Remaining High findings (tracked)

IDTitleComponent
F-008Empty builder_signature accepted by default — fail-opentaba-security/provenance.rs
F-012replay ignores the from parameter — replays from beginningtaba-node/wal.rs
F-017Spawn depth taken from unit’s own declaration, not computedtaba-graph/graph.rs
F-018Scope checker not invoked for workload units (and None by default)taba-graph/graph.rs
F-020declare_failed does not verify witnesses are known or distincttaba-gossip/swim.rs
F-028TOML injection via K8s metadata — unsanitized interpolationtaba-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>]
FlagDefaultDescription
--forcefalseRe-initialize even if already initialized
--state-dir~/.tabaState 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>]
FlagDefaultDescription
--dry-runfalseValidate and display without inserting
--state-dir~/.tabaState directory
--formattableOutput 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]
FlagDefaultDescription
--output-dirstdoutDirectory for generated .taba.toml files
--trust-domain—Trust domain for generated units
--provenancefalseGenerate 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 from image)
  • 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

FieldValuesDefault
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)

TypeFieldsDefault
"http"path, port, interval, timeout—
"tcp"port, interval, timeout—
"command"command, interval, timeout—
(omitted)—OS-level process monitoring

Recovery strategies

StrategyFieldsDefault
"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)

ModeBehaviorDefault
"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

ActionBehavior
"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

FieldTypeDefault
kind"bounded-task""service"
deadline.wall_timeduration string(required for bounded-task)
deadline.lc_rangeinteger (logical clock delta)(optional)
spawn.max_depthinteger (1-4)governed by INV-W3 default (4)
spawn.max_countinteger(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:

KeyArtifact typeExample
imageOCI containerimage = "myapp:v1.0"
binaryNative binarybinary = "bin/myapp"
wasmWebAssembly modulewasm = "module.wasm"
k8sKubernetes manifestk8s = "pod-spec.yaml"
microvmMicroVM kernelmicrovm = "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

FieldDefaultSource
type"workload"inferred from artifact key
kind"service"—
trust_domaincurrentfrom taba init
authorcurrentfrom keypair
scaling.min1—
scaling.max1—
healthOS-level process monitoringINV-O3
placement_on_failureenv-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.toml files
  • 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 Resourcetaba UnitMapping
DeploymentWorkloadUnit (service)Container image → artifact, replicas → scaling
StatefulSetWorkloadUnit (service)+ state recovery (require-quorum), PVC references
DaemonSetWorkloadUnit (service)+ one-per-node constraint, node selector
PodWorkloadUnit (service)Direct pod (no controller, no auto-scaling)
ServiceWorkloadUnit (provides)Network capability from ports
ConfigMapDataUnitInternal classification, text/plain schema
SecretDataUnitConfidential classification, encrypted_at_rest
NetworkPolicyPolicyUnitPod selector → conflict units, ingress/egress → resolution
RoleGovernanceUnit (RoleAssignment)Rules → unit_type_scope, namespace → trust_domain_scope
ClusterRoleGovernanceUnit (RoleAssignment)Cluster-wide scope
RoleBindingGovernanceUnit (RoleAssignment)Binds Role to subjects
ClusterRoleBindingGovernanceUnit (RoleAssignment)Binds ClusterRole to subjects, cluster-wide

Unmappable resources

The converter surfaces resources it cannot map, with a reason and suggestion:

ResourceReasonSuggestion
Ingresstaba has no ingress conceptMap the service backend directly
PersistentVolumeNo direct taba equivalentUse a data unit with storage_requirements
PersistentVolumeClaimNo direct taba equivalentUse a data unit with storage_requirements
CRDsUnbounded, no generic mappingWrite a custom taba unit declaration
JobBounded task (manual)Set kind = “bounded-task”, add [deadline]
CronJobBounded task (manual)Set kind = “bounded-task”, add [deadline]
HPAScaling triggers (manual)Add triggers to [scaling] section
NamespaceNo taba equivalent needed—
ServiceAccountNo 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.

IDTitleStatus
ADR-001Unit Model — four typed unitsAccepted
ADR-002CRDT Graph — δ-state, no consensusAccepted
ADR-003Capability Security — zero-default, fail-closedAccepted
ADR-004No Masters — all nodes are peersAccepted
ADR-005Erasure Coding — Reed-Solomon, not replicationAccepted
ADR-006Role Model — scoped authorityAccepted
ADR-007Runtime model — per-artifact-type dispatchAccepted

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

AlternativeProsConsWhy rejected
Enhanced container + sidecar modelFamiliar, incrementalStill opaque core, still needs external orchestratorDoesn’t solve the fundamental split
FaaS / serverlessSimple unit, managed platformVendor lock-in, limited workload typesToo restrictive, sovereignty concerns
Configuration overlay on K8sLeverages existing ecosystemStill K8s complexity underneathDoesn’t reduce fundamental complexity

References

  • docs/vision/SYSTEM_VISION.md § The unit model
  • specs/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

AlternativeProsConsWhy rejected
Raft consensusWell-understood, strong consistencyLeader bottleneck, etcd-like SPOFContradicts no-masters design
External store (etcd, consul)Mature, provenExternal dependency, operational burdenViolates self-contained design
Full replication (no erasure)SimplerHigher bandwidth, wastefulDoesn’t scale to large clusters

References

  • docs/vision/SYSTEM_VISION.md § The CRDT composition graph
  • docs/vision/SYSTEM_VISION.md § Peer-to-peer architecture
  • memory/OPEN_QUESTIONS.md OQ-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

AlternativeProsConsWhy rejected
RBAC (like K8s)FamiliarDoesn’t compose, doesn’t propagate taintsInsufficient for the unit model
Allow-by-default + deny listsEasier onboardingInsecure default, easy to miss denialsViolates security-first design
Policy-as-code only (OPA)Powerful, flexibleExternal system, not integrated with unit modelCan complement but not replace capability model

References

  • docs/vision/SYSTEM_VISION.md § Security model
  • docs/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

AlternativeProsConsWhy rejected
Small fixed control plane (3-5 nodes)Familiar, provenSeparate infra, SPOF classContradicts design goal
Elected leader for coordinationSimpler consistencyLeader is bottleneck and SPOFContradicts no-masters design
Hybrid (embedded leader per region)Scalable, still distributedComplex, two modesUnnecessary if CRDT works

References

  • docs/vision/SYSTEM_VISION.md § Peer-to-peer architecture
  • docs/decisions/ADR-002-crdt-graph.md (CRDT eliminates need for consensus)
  • memory/OPEN_QUESTIONS.md OQ-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

AlternativeProsConsWhy rejected
Full 3x replicationSimple, provenWasteful, fixed redundancyDoesn’t scale, over-provisions small clusters
Chain replicationEfficient readsComplex failure handlingRequires ordering, contradicts peer model
No redundancy (WAL only)SimplestSingle node failure = data lossUnacceptable for production

References

  • docs/vision/SYSTEM_VISION.md § Peer-to-peer architecture
  • memory/OPEN_QUESTIONS.md OQ-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:

  1. Unit type scope: which types of units can this author create (workload, data, policy, governance)
  2. 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 model
  • docs/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:

ArtifactTypeRuntimeExecutorDetection
OciDockerRuntime (bollard)Docker::connect_with_defaults()
NativeNativeRuntime (std::process)Always available
WasmWasmRuntime (state tracking)Always available
MicroVmMicroVmRuntime (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 RuntimeExecutor and adding a case to RuntimeSelector.
  • Negative: NativeRuntime and WasmRuntime are minimal implementations (state tracking, not full execution). Full Wasm execution requires adding the wasmtime crate.
  • Negative: MicroVmRuntime requires a VM monitor binary (firecracker, cloud-hypervisor, or QEMU) to be installed.

Alternatives Considered

AlternativeProsConsWhy rejected
Docker-onlySimple, well-testedCan’t run native/Wasm/MicroVmViolates vision doc
One runtime per nodeSimpler dispatchCan’t mix workload typesViolates INV-N2
Plugin systemExtensibleOver-engineered for 4 typesPremature abstraction