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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Changelog
## 0.3.0 - 2026-08-02
- Added deterministic compilation of admitted plans, typed provider contracts and
resolution, explicit execution state and evidence, dry run, retry, compensation,
and interruption-safe in-memory resume through a non-operational fake provider.
- Added an `execute` demonstration command and runtime architecture ADR. Durable
repeated reconciliation and operational providers remain deferred.

## 0.2.0 - 2026-08-02
- Added canonical world and manifest digests, revision lineage, semantic change sets, offline observed-state evidence, mandate-aware admission, explicit risks and approvals, and deterministic provider-neutral reconciliation plans.
- Added `diff`, `admit`, and `plan` commands. Planning is deliberately non-executable and keeps canonical intent, capabilities, provider bindings, and observed drift separate.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ADR: Compile admitted plans into provider-bound executable operations

**Status:** Accepted

## Context

v0.2 plans express sovereign meaning and provenance. Provider selection inside them
would make replaceable implementation details canonical. Execution also needs
recovery state and attributable evidence that ordinary logs cannot provide.

## Decision

Keep reconciliation plans provider-neutral and add a deterministic compilation stage
that resolves capabilities into provider-bound executable operations. Provider apply
success and observed convergence remain separate records. Operation state and valid
transitions are explicit, and evidence is an execution product with authority,
mandate, resource, capability, provider, and revision provenance.

Providers do not offer a distributed transaction. The runtime may retry and perform
reverse-order semantic compensation, but does not promise atomicity, snapshots, or
equivalence between compensation, rollback, and cleanup. A complete in-memory fake
precedes operational providers so contracts and failures can be proven safely.

## Consequences

Compilation is deterministic and independently testable. Execution is explainable
and resumable within an injected in-memory repository. Callers must supply a registry
and bindings. Persistence, crash-safe recovery, and repeated reconciliation are
deferred to v0.4; operational naming is deferred to v0.5.
4 changes: 4 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# NetSovereign roadmap

Implementation status: v0.1, v0.2, and the bounded in-memory v0.3 runtime core are
implemented and tested. v0.4 durability and v0.5 naming materialisation remain
planned and are not implied by the v0.3 interfaces.

NetSovereign develops authority and intent before operational adapters. Versions v0.2 through v0.4
are a single dependency chain: real DNS, PKI, identity, or gateway providers must not begin until
change planning, the runtime core, and durable local control have established their boundaries.
Expand Down
58 changes: 58 additions & 0 deletions docs/runtime-core-v0.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# NetEngine runtime core (v0.3)

v0.3 adds a provider-neutral execution boundary without replacing the legacy
phase-oriented application. The phase runner and its operational handlers remain a
compatibility path; the runtime core never calls them and no existing handler is
presented as a provider.

## Pipeline and contracts

An admitted v0.2 `ReconciliationPlan` remains an intent artefact. `compile_plan`
topologically orders its steps and deterministically resolves the generic
`resource.manage` capability through an injected `ProviderRegistry`. It emits a
provider-bound `ExecutablePlan` with revision, authority, mandate, target, binding,
preconditions, expected result, idempotency key, retry policy, failure posture, and a
deterministic integrity fingerprint. Compilation describes providers but never calls
validation, mutation, or observation.

The provider protocol separates `validate`, `apply`, `observe`, `compensate`, and
`delete`. Apply acceptance is not convergence: only a later matching observation
verifies an operation. Resolution rejects missing, incompatible, ambiguous,
explicitly unsupported, and unhealthy providers with stable error codes.

## State, evidence, and recovery

Every operation owns explicit transitions, attempts, provider results, observations,
and failure classification. Invalid transitions fail closed. A run derives status
from operation state rather than logs. Evidence separately records validation,
application, observation, conformance, simulation, compensation, and failure with
world revision and authority provenance. Secret-looking mapping keys are redacted.

Retries repeat the same transition only for explicit retryable classes. Compensation
is a semantic counter-action and runs in reverse completion order; rollback represents
restoring prior state; cleanup removes orphaned artefacts. They are distinct and no
cross-provider atomicity is claimed. If interruption leaves an operation running
after apply, resume observes first and accepts convergence without applying twice.
Changed plan fingerprints are rejected. This lasts only as long as the in-memory
repository: crash-safe persistence belongs to v0.4.

## Dry run and demonstration

Dry run resolves and validates normally, records predictions and evidence, and never
calls a mutating method. Providers must declare dry-run support.

```bash
netsovereign plan examples/minimal/world.yaml proposed.yaml --output admitted-plan.json
netsovereign execute admitted-plan.json
netsovereign execute admitted-plan.json --dry-run
```

The fake supports deterministic create/update/delete-style application, observation,
idempotent replay, stable resource IDs, typed failure injection, and compensation
without network, process, container, database, DNS, or host access.

## Boundaries

v0.3 is sequential and in-process. It adds no daemon, polling, operational provider,
database journal, lock, infrastructure snapshot, distributed transaction, or API.
Durable storage and repeated reconciliation are v0.4; authoritative naming is v0.5.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "hatchling.build"

[project]
name = "netsovereign"
version = "0.2.0"
description = "Provider-neutral domain foundation for sovereign digital worlds"
version = "0.3.0"
description = "Provider-neutral compiler and reconciliation runtime for sovereign worlds"
readme = "README.md"
requires-python = ">=3.12"
license = {file = "LICENSE"}
Expand Down
29 changes: 29 additions & 0 deletions src/netsovereign/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import json
from datetime import datetime
from pathlib import Path
Expand All @@ -17,10 +18,14 @@
ApprovalEvidence,
ObservedStateSnapshot,
ParentRevisionReference,
ReconciliationPlan,
admit_change,
build_plan,
compare_worlds,
)
from .providers.fake import FakeProvider
from .providers.registry import ProviderRegistry
from .runtime import InMemoryExecutionRepository, RuntimeExecutor, compile_plan
from .specification import WorldSpec
from .validation import has_errors, validate_spec

Expand Down Expand Up @@ -197,5 +202,29 @@ def plan(
raise typer.Exit(3)


@app.command("execute")
def execute_command(
plan_file: Path,
dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
) -> None:
"""Compile and execute an admitted plan through the in-memory fake provider."""
try:
intent_plan = ReconciliationPlan.model_validate(
yaml.safe_load(plan_file.read_text(encoding="utf-8"))
)
registry = ProviderRegistry()
registry.register(FakeProvider())
executable = compile_plan(intent_plan, registry)
report = asyncio.run(
RuntimeExecutor(registry, InMemoryExecutionRepository()).execute(
executable, dry_run=dry_run
)
)
except (OSError, ValidationError, ValueError) as exc:
typer.echo(f"EXECUTION_ERROR {plan_file}: {exc}", err=True)
raise typer.Exit(1) from exc
_emit(report)


if __name__ == "__main__":
app()
7 changes: 7 additions & 0 deletions src/netsovereign/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Provider-neutral capability contracts and the non-operational fake provider."""

from .contracts import * # noqa: F403
from .fake import FakeProvider
from .registry import ProviderRegistry

__all__ = ["FakeProvider", "ProviderRegistry"]
89 changes: 89 additions & 0 deletions src/netsovereign/providers/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Small, capability-oriented provider contract used by the runtime compiler."""

from __future__ import annotations

from enum import StrEnum
from typing import Any, Protocol, runtime_checkable

from pydantic import Field

from ..base import DomainModel


class FailureClass(StrEnum):
VALIDATION = "validation"
CAPABILITY_UNAVAILABLE = "capability_unavailable"
PRECONDITION = "precondition_failed"
CONFLICT = "conflict"
TRANSIENT = "transient_provider_failure"
PERMANENT = "permanent_provider_failure"
TIMEOUT = "timeout"
OBSERVATION_MISMATCH = "observation_mismatch"
COMPENSATION = "compensation_failure"
INTERNAL = "internal_engine_failure"


class CapabilityRequirement(DomainModel):
id: str
version: str = "1.0"


class CapabilityDeclaration(DomainModel):
id: str
version: str = "1.0"
dry_run: bool = True
idempotent: bool = True
compensation: bool = True
limitations: list[str] = Field(default_factory=list)


class ProviderDescriptor(DomainModel):
id: str
version: str
binding_id: str
available: bool = True
healthy: bool = True
capabilities: list[CapabilityDeclaration]


class ProviderContext(DomainModel):
run_id: str
operation_id: str
idempotency_key: str
dry_run: bool = False


class ValidationResult(DomainModel):
valid: bool
predicted_action: str | None = None
evidence: dict[str, Any] = Field(default_factory=dict)
failure: FailureClass | None = None
message: str | None = None


class ProviderResult(DomainModel):
success: bool
provider_resource_id: str | None = None
output: dict[str, Any] = Field(default_factory=dict)
evidence: dict[str, Any] = Field(default_factory=dict)
failure: FailureClass | None = None
retryable: bool = False
message: str | None = None


class ObservationResult(DomainModel):
observed: bool
matches_expected: bool
state: dict[str, Any] = Field(default_factory=dict)
evidence: dict[str, Any] = Field(default_factory=dict)
message: str | None = None


@runtime_checkable
class Provider(Protocol):
def describe(self) -> ProviderDescriptor: ...
async def validate(self, operation: Any, context: ProviderContext) -> ValidationResult: ...
async def apply(self, operation: Any, context: ProviderContext) -> ProviderResult: ...
async def observe(self, operation: Any, context: ProviderContext) -> ObservationResult: ...
async def compensate(self, operation: Any, context: ProviderContext) -> ProviderResult: ...
async def delete(self, operation: Any, context: ProviderContext) -> ProviderResult: ...
114 changes: 114 additions & 0 deletions src/netsovereign/providers/fake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Inspectable, deterministic provider with no operational side effects."""

from __future__ import annotations

from collections import defaultdict
from hashlib import sha256
from typing import Any

from .contracts import (
CapabilityDeclaration,
FailureClass,
ObservationResult,
ProviderContext,
ProviderDescriptor,
ProviderResult,
ValidationResult,
)


class FakeProvider:
def __init__(self, provider_id: str = "fake", *, dry_run: bool = True) -> None:
self.provider_id = provider_id
self.dry_run = dry_run
self.available = True
self.healthy = True
self.state: dict[str, Any] = {}
self.call_history: list[tuple[str, str]] = []
self.failures: dict[str, list[FailureClass]] = defaultdict(list)
self.observation_mismatches: set[str] = set()

def describe(self) -> ProviderDescriptor:
return ProviderDescriptor(
id=self.provider_id,
version="1.0",
binding_id=f"{self.provider_id}:memory",
available=self.available,
healthy=self.healthy,
capabilities=[CapabilityDeclaration(id="resource.manage", dry_run=self.dry_run)],
)

def inject_failure(self, operation_id: str, failure: FailureClass) -> None:
self.failures[operation_id].append(failure)

async def validate(self, operation: Any, context: ProviderContext) -> ValidationResult:
self.call_history.append(("validate", operation.id))
if context.dry_run and not self.dry_run:
return ValidationResult(
valid=False,
failure=FailureClass.CAPABILITY_UNAVAILABLE,
message="dry run is unsupported",
)
if (
self.failures[operation.id]
and self.failures[operation.id][0] == FailureClass.VALIDATION
):
self.failures[operation.id].pop(0)
return ValidationResult(
valid=False, failure=FailureClass.VALIDATION, message="injected"
)
return ValidationResult(
valid=True,
predicted_action=f"{operation.operation_type}:{operation.target}",
evidence={"validated": True},
)

async def apply(self, operation: Any, context: ProviderContext) -> ProviderResult:
self.call_history.append(("apply", operation.id))
if self.failures[operation.id]:
failure = self.failures[operation.id].pop(0)
return ProviderResult(
success=False,
failure=failure,
retryable=failure == FailureClass.TRANSIENT,
message="injected",
)
resource_id = "fake-" + sha256(operation.target.encode()).hexdigest()[:12]
if operation.operation_type in {"remove", "delete"}:
self.state.pop(operation.target, None)
else:
self.state[operation.target] = operation.expected
return ProviderResult(
success=True,
provider_resource_id=resource_id,
output={"action": operation.operation_type},
evidence={"mutated": True},
)

async def observe(self, operation: Any, context: ProviderContext) -> ObservationResult:
self.call_history.append(("observe", operation.id))
actual = self.state.get(operation.target)
expected = operation.expected
matches = actual == expected and operation.id not in self.observation_mismatches
return ObservationResult(
observed=actual is not None,
matches_expected=matches,
state={"value": actual},
evidence={"matches": matches},
)

async def compensate(self, operation: Any, context: ProviderContext) -> ProviderResult:
self.call_history.append(("compensate", operation.id))
if (
self.failures[operation.id]
and self.failures[operation.id][0] == FailureClass.COMPENSATION
):
self.failures[operation.id].pop(0)
return ProviderResult(
success=False, failure=FailureClass.COMPENSATION, message="injected"
)
self.state.pop(operation.target, None)
return ProviderResult(success=True, evidence={"compensated": True})

async def delete(self, operation: Any, context: ProviderContext) -> ProviderResult:
return await self.compensate(operation, context)
Loading
Loading