build(agent): c3po#b883b4 iteration

This commit is contained in:
agent-b883b4bc188823a2 2026-04-26 22:27:36 +02:00
parent 80e4f867da
commit 2fa2ccd87c
6 changed files with 369 additions and 8 deletions

View File

@ -8,6 +8,7 @@ This repository contains a Python reference implementation for the Autopoiesis s
- `genome.py`: genome schema, canonical hashing, Ed25519 signing, signed artifact verification. - `genome.py`: genome schema, canonical hashing, Ed25519 signing, signed artifact verification.
- `policy.py`: policy templates, deterministic smoke-test evaluation, safety attestation. - `policy.py`: policy templates, deterministic smoke-test evaluation, safety attestation.
- `admission.py`: signed capability tokens and deterministic admission decisions.
- `ledger.py`: append-only provenance ledger, Merkle root computation, replay trace models. - `ledger.py`: append-only provenance ledger, Merkle root computation, replay trace models.
- `replicator.py`: deterministic replication planner and lightweight ReplicaWeave intent model. - `replicator.py`: deterministic replication planner and lightweight ReplicaWeave intent model.

View File

@ -7,6 +7,7 @@ It focuses on the trust substrate needed for safe self-replication across edge a
- `ServiceGenome` models immutable service blueprints with resources, capabilities, invariants, mutation operators, and test contracts. - `ServiceGenome` models immutable service blueprints with resources, capabilities, invariants, mutation operators, and test contracts.
- `GenomeSigner` produces and verifies Ed25519 signatures for genome artifacts. - `GenomeSigner` produces and verifies Ed25519 signatures for genome artifacts.
- `PolicySandbox` checks a genome against operational budgets and deterministic smoke-test contracts. - `PolicySandbox` checks a genome against operational budgets and deterministic smoke-test contracts.
- `AdmissionTokenIssuer` and `AdmissionGate` sign and verify lightweight capability tokens for constrained edges.
- `ProvenanceLedger` records append-only replication events with a Merkle root and chain verification for tamper evidence. - `ProvenanceLedger` records append-only replication events with a Merkle root and chain verification for tamper evidence.
- `ReplicatorPlanner` turns an approved genome into a deterministic rollout trace. - `ReplicatorPlanner` turns an approved genome into a deterministic rollout trace.
- `ReplayHarness` replays and verifies a rollout trace against the genome and attestation that produced it. - `ReplayHarness` replays and verifies a rollout trace against the genome and attestation that produced it.
@ -20,6 +21,7 @@ This repository ships a production-shaped core for the autonomy fabric:
- content addressing via canonical JSON hashing - content addressing via canonical JSON hashing
- signed artifact creation and verification - signed artifact creation and verification
- deterministic smoke-test evaluation - deterministic smoke-test evaluation
- signed admission tokens for budgeted edge installs
- provenance logging with Merkle aggregation - provenance logging with Merkle aggregation
- replayable replication traces - replayable replication traces
- a small test suite and buildable Python package - a small test suite and buildable Python package

View File

@ -1,3 +1,4 @@
from .admission import AdmissionDecision, AdmissionGate, AdmissionTokenClaims, AdmissionTokenIssuer, SignedAdmissionToken
from .genome import ( from .genome import (
CapabilityVector, CapabilityVector,
GenomeSigner, GenomeSigner,
@ -13,6 +14,10 @@ from .policy import PolicySandbox, PolicyTemplate, SafetyAttestation, SmokeTestR
from .replicator import ReplicaWeaveIntent, ReplayHarness, ReplicatorPlanner, ReplicationPlan, RolloutStage from .replicator import ReplicaWeaveIntent, ReplayHarness, ReplicatorPlanner, ReplicationPlan, RolloutStage
__all__ = [ __all__ = [
"AdmissionDecision",
"AdmissionGate",
"AdmissionTokenClaims",
"AdmissionTokenIssuer",
"CapabilityVector", "CapabilityVector",
"GenomeSigner", "GenomeSigner",
"LedgerEntry", "LedgerEntry",
@ -27,6 +32,7 @@ __all__ = [
"ResourceEnvelope", "ResourceEnvelope",
"ReplayTrace", "ReplayTrace",
"RolloutStage", "RolloutStage",
"SignedAdmissionToken",
"SafetyAttestation", "SafetyAttestation",
"SafetyInvariants", "SafetyInvariants",
"ServiceGenome", "ServiceGenome",

View File

@ -0,0 +1,176 @@
from __future__ import annotations
import base64
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from pydantic import BaseModel, ConfigDict, Field, model_validator
from .genome import ResourceEnvelope, ServiceGenome
def _canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
class AdmissionTokenClaims(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
issuer_did: str
subject_did: str
audience: str
genome_hash: str
allowed_intents: list[str] = Field(default_factory=list)
safety_labels: list[str] = Field(default_factory=list)
resource_budget: ResourceEnvelope
issued_at: datetime
not_before: datetime
expires_at: datetime
nonce: str
@model_validator(mode="after")
def _validate_timestamps(self) -> "AdmissionTokenClaims":
if not self.issuer_did.strip():
raise ValueError("issuer_did cannot be empty")
if not self.subject_did.strip():
raise ValueError("subject_did cannot be empty")
if not self.audience.strip():
raise ValueError("audience cannot be empty")
if self.not_before > self.expires_at:
raise ValueError("not_before must be before expires_at")
if self.issued_at > self.expires_at:
raise ValueError("issued_at must be before expires_at")
return self
@dataclass(frozen=True)
class SignedAdmissionToken:
claims: AdmissionTokenClaims
signature_b64: str
public_key_b64: str
def canonical_payload(self) -> bytes:
return _canonical_json(self.claims.model_dump(mode="json", exclude_none=True))
def digest(self) -> str:
payload = {
"claims": self.claims.model_dump(mode="json", exclude_none=True),
"public_key_b64": self.public_key_b64,
"signature_b64": self.signature_b64,
}
return hashlib.sha256(_canonical_json(payload)).hexdigest()
@dataclass(frozen=True)
class AdmissionDecision:
approved: bool
reasons: tuple[str, ...]
token_digest: str
def digest(self) -> str:
return hashlib.sha256(
_canonical_json({"approved": self.approved, "reasons": list(self.reasons), "token_digest": self.token_digest})
).hexdigest()
class AdmissionTokenIssuer:
def __init__(self, private_key: Ed25519PrivateKey) -> None:
self._private_key = private_key
@classmethod
def generate(cls) -> "AdmissionTokenIssuer":
return cls(Ed25519PrivateKey.generate())
@property
def public_key(self) -> Ed25519PublicKey:
return self._private_key.public_key()
def issue(self, claims: AdmissionTokenClaims) -> SignedAdmissionToken:
signature = self._private_key.sign(_canonical_json(claims.model_dump(mode="json", exclude_none=True)))
return SignedAdmissionToken(
claims=claims,
signature_b64=base64.b64encode(signature).decode("ascii"),
public_key_b64=base64.b64encode(
self.public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
).decode("ascii"),
)
class AdmissionGate:
def authorize(
self,
genome: ServiceGenome,
token: SignedAdmissionToken,
*,
intent: Any | None = None,
at: datetime | None = None,
) -> AdmissionDecision:
reasons: list[str] = []
now = at or datetime.now(timezone.utc)
try:
self.verify_signature(token)
except ValueError as exc:
reasons.append(str(exc))
return AdmissionDecision(approved=False, reasons=tuple(reasons), token_digest=token.digest())
claims = token.claims
token_digest = token.digest()
if claims.genome_hash != genome.content_hash():
reasons.append("genome hash does not match admission token")
if now < claims.not_before:
reasons.append("admission token is not yet valid")
if now > claims.expires_at:
reasons.append("admission token has expired")
envelope = genome.resource_envelope
budget = claims.resource_budget
if envelope.cpu_millicores > budget.cpu_millicores:
reasons.append("cpu budget exceeds admission token")
if envelope.memory_mib > budget.memory_mib:
reasons.append("memory budget exceeds admission token")
if envelope.bandwidth_mbps > budget.bandwidth_mbps:
reasons.append("bandwidth budget exceeds admission token")
if envelope.cost_usd_per_hour > budget.cost_usd_per_hour:
reasons.append("cost budget exceeds admission token")
if envelope.max_replicas > budget.max_replicas:
reasons.append("replica budget exceeds admission token")
if intent is not None:
intent_kind = getattr(intent, "intent_kind", None)
safety_labels = list(getattr(intent, "safety_labels", []) or [])
budget_hint = getattr(intent, "budget_hint", {}) or {}
if claims.allowed_intents and intent_kind not in claims.allowed_intents:
reasons.append(f"intent kind not allowed: {intent_kind}")
missing_labels = sorted(set(safety_labels) - set(claims.safety_labels))
if missing_labels:
reasons.append(f"missing safety label(s): {', '.join(missing_labels)}")
budget_checks = (
("cpu_millicores", "cpu budget hint exceeds admission token"),
("memory_mib", "memory budget hint exceeds admission token"),
("bandwidth_mbps", "bandwidth budget hint exceeds admission token"),
("cost_usd_per_hour", "cost budget hint exceeds admission token"),
("max_replicas", "replica budget hint exceeds admission token"),
)
for field_name, reason in budget_checks:
hinted = budget_hint.get(field_name)
if hinted is not None and hinted > getattr(budget, field_name):
reasons.append(reason)
return AdmissionDecision(approved=not reasons, reasons=tuple(reasons), token_digest=token_digest)
@staticmethod
def verify_signature(token: SignedAdmissionToken) -> bool:
public_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(token.public_key_b64))
signature = base64.b64decode(token.signature_b64)
public_key.verify(signature, token.canonical_payload())
return True

View File

@ -7,6 +7,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
from .admission import AdmissionDecision, AdmissionGate, SignedAdmissionToken
from .genome import ServiceGenome from .genome import ServiceGenome
from .ledger import ReplayTrace, TraceDelta from .ledger import ReplayTrace, TraceDelta
from .policy import SafetyAttestation from .policy import SafetyAttestation
@ -44,6 +45,7 @@ class ReplicationPlan(BaseModel):
stages: list[RolloutStage] = Field(default_factory=list) stages: list[RolloutStage] = Field(default_factory=list)
trace: ReplayTrace trace: ReplayTrace
rationale: list[str] = Field(default_factory=list) rationale: list[str] = Field(default_factory=list)
admission_decision: AdmissionDecision | None = None
def digest(self) -> str: def digest(self) -> str:
payload = self.model_dump(mode="json", exclude_none=True) payload = self.model_dump(mode="json", exclude_none=True)
@ -62,25 +64,89 @@ def _rollout_stages(max_replicas: int, score: float) -> list[RolloutStage]:
class ReplicatorPlanner: class ReplicatorPlanner:
def plan(self, genome: ServiceGenome, attestation: SafetyAttestation) -> ReplicationPlan: def plan(
self,
genome: ServiceGenome,
attestation: SafetyAttestation,
*,
intent: ReplicaWeaveIntent | None = None,
admission_token: SignedAdmissionToken | None = None,
) -> ReplicationPlan:
genome_hash = genome.content_hash() genome_hash = genome.content_hash()
trace_seed = int(genome_hash[:16], 16) trace_seed = int(genome_hash[:16], 16)
rationale = list(attestation.reasons) rationale = list(attestation.reasons)
admission_decision = None
if admission_token is not None:
admission_decision = AdmissionGate().authorize(genome, admission_token, intent=intent)
if not admission_decision.approved:
rationale.extend(admission_decision.reasons)
trace = ReplayTrace(
seed=trace_seed,
deltas=[
TraceDelta(
stage="blocked",
operation="deny",
payload={
"policy": attestation.policy_name,
"admission_token": admission_decision.token_digest,
"reasons": list(admission_decision.reasons),
},
)
],
)
return ReplicationPlan(
genome_hash=genome_hash,
approved=False,
stages=[],
trace=trace,
rationale=rationale,
admission_decision=admission_decision,
)
if not attestation.approved: if not attestation.approved:
trace = ReplayTrace(seed=trace_seed, deltas=[TraceDelta(stage="blocked", operation="deny", payload={"policy": attestation.policy_name})]) trace = ReplayTrace(
return ReplicationPlan(genome_hash=genome_hash, approved=False, stages=[], trace=trace, rationale=rationale) seed=trace_seed,
deltas=[TraceDelta(stage="blocked", operation="deny", payload={"policy": attestation.policy_name})],
)
return ReplicationPlan(
genome_hash=genome_hash,
approved=False,
stages=[],
trace=trace,
rationale=rationale,
admission_decision=admission_decision,
)
stages = _rollout_stages(genome.resource_envelope.max_replicas, attestation.score) stages = _rollout_stages(genome.resource_envelope.max_replicas, attestation.score)
deltas = [ deltas = [
TraceDelta(stage=stage.name, operation="rollout", payload={"cohort_size": stage.cohort_size, "percentage": stage.percentage}) TraceDelta(stage=stage.name, operation="rollout", payload={"cohort_size": stage.cohort_size, "percentage": stage.percentage})
for stage in stages for stage in stages
] ]
trace = ReplayTrace(seed=trace_seed, deltas=deltas, signatures=[genome_hash, attestation.digest()]) signatures = [genome_hash, attestation.digest()]
return ReplicationPlan(genome_hash=genome_hash, approved=True, stages=stages, trace=trace, rationale=rationale) if admission_decision is not None:
signatures.append(admission_decision.digest())
trace = ReplayTrace(seed=trace_seed, deltas=deltas, signatures=signatures)
return ReplicationPlan(
genome_hash=genome_hash,
approved=True,
stages=stages,
trace=trace,
rationale=rationale,
admission_decision=admission_decision,
)
class ReplayHarness: class ReplayHarness:
def verify(self, genome: ServiceGenome, attestation: SafetyAttestation, plan: ReplicationPlan) -> ReplayTrace: def verify(
self,
genome: ServiceGenome,
attestation: SafetyAttestation,
plan: ReplicationPlan,
*,
intent: ReplicaWeaveIntent | None = None,
admission_token: SignedAdmissionToken | None = None,
) -> ReplayTrace:
genome_hash = genome.content_hash() genome_hash = genome.content_hash()
if plan.genome_hash != genome_hash: if plan.genome_hash != genome_hash:
raise ValueError("plan genome hash does not match genome content") raise ValueError("plan genome hash does not match genome content")
@ -91,8 +157,20 @@ class ReplayHarness:
if plan.approved != attestation.approved: if plan.approved != attestation.approved:
raise ValueError("plan approval does not match attestation") raise ValueError("plan approval does not match attestation")
expected_admission = None
if admission_token is not None:
expected_admission = AdmissionGate().authorize(genome, admission_token, intent=intent)
if plan.admission_decision != expected_admission:
raise ValueError("plan admission decision is not reproducible from the token and intent")
if expected_admission.approved and expected_admission.digest() not in plan.trace.signatures:
raise ValueError("trace signatures do not include the admission decision")
if not plan.approved: if not plan.approved:
if plan.trace.deltas != [TraceDelta(stage="blocked", operation="deny", payload={"policy": attestation.policy_name})]: expected_payload: dict[str, object] = {"policy": attestation.policy_name}
if expected_admission is not None and not expected_admission.approved:
expected_payload["admission_token"] = expected_admission.token_digest
expected_payload["reasons"] = list(expected_admission.reasons)
if plan.trace.deltas != [TraceDelta(stage="blocked", operation="deny", payload=expected_payload)]:
raise ValueError("blocked trace does not match attestation") raise ValueError("blocked trace does not match attestation")
return plan.trace return plan.trace
@ -106,6 +184,9 @@ class ReplayHarness:
raise ValueError("plan stages are not reproducible from the genome and attestation") raise ValueError("plan stages are not reproducible from the genome and attestation")
if plan.trace.deltas != expected_deltas: if plan.trace.deltas != expected_deltas:
raise ValueError("trace deltas are not reproducible from the rollout stages") raise ValueError("trace deltas are not reproducible from the rollout stages")
if plan.trace.signatures != [genome_hash, attestation.digest()]: expected_signatures = [genome_hash, attestation.digest()]
if expected_admission is not None:
expected_signatures.append(expected_admission.digest())
if plan.trace.signatures != expected_signatures:
raise ValueError("trace signatures do not match the expected audit chain") raise ValueError("trace signatures do not match the expected audit chain")
return plan.trace return plan.trace

View File

@ -3,6 +3,9 @@ from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from idea187_autopoiesis_verifiable_service import ( from idea187_autopoiesis_verifiable_service import (
AdmissionGate,
AdmissionTokenClaims,
AdmissionTokenIssuer,
GenomeSigner, GenomeSigner,
CapabilityVector, CapabilityVector,
PolicySandbox, PolicySandbox,
@ -12,6 +15,7 @@ from idea187_autopoiesis_verifiable_service import (
ReplicatorPlanner, ReplicatorPlanner,
MutationOperator, MutationOperator,
ResourceEnvelope, ResourceEnvelope,
ReplicaWeaveIntent,
SafetyInvariants, SafetyInvariants,
ServiceGenome, ServiceGenome,
TestContract, TestContract,
@ -154,3 +158,94 @@ def test_audit_verification_rejects_tampering() -> None:
assert "seed" in str(exc) assert "seed" in str(exc)
else: else:
raise AssertionError("expected trace verification to fail") raise AssertionError("expected trace verification to fail")
def test_admission_token_binds_genome_budget_and_intent() -> None:
genome = build_genome()
attestation = PolicySandbox().evaluate(
genome,
PolicyTemplate(
name="default-edge",
max_cpu_millicores=500,
max_memory_mib=512,
max_bandwidth_mbps=100,
max_cost_usd_per_hour=1.0,
allow_network_egress=True,
allowed_egress_endpoints=["https://internal.example"],
allowed_mutation_operators=["safe-fork"],
),
)
issuer = AdmissionTokenIssuer.generate()
claims = AdmissionTokenClaims(
issuer_did="did:key:zissuer",
subject_did="did:key:zsubject",
audience="edge-fleet-a",
genome_hash=genome.content_hash(),
allowed_intents=["fork"],
safety_labels=["read-only", "deterministic"],
resource_budget=ResourceEnvelope(
cpu_millicores=500,
memory_mib=512,
bandwidth_mbps=100,
cost_usd_per_hour=1.0,
max_replicas=3,
),
issued_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
not_before=datetime(2024, 1, 1, tzinfo=timezone.utc),
expires_at=datetime(2030, 1, 2, tzinfo=timezone.utc),
nonce="nonce-1",
)
token = issuer.issue(claims)
intent = ReplicaWeaveIntent(
source="catopt",
target="edge-01",
intent_kind="fork",
budget_hint={"cpu_millicores": 250, "max_replicas": 2},
safety_labels=["read-only"],
)
decision = AdmissionGate().authorize(genome, token, intent=intent, at=datetime(2024, 1, 1, 12, tzinfo=timezone.utc))
assert decision.approved is True
plan = ReplicatorPlanner().plan(genome, attestation, intent=intent, admission_token=token)
assert plan.approved is True
assert plan.admission_decision == decision
assert decision.digest() in plan.trace.signatures
assert ReplayHarness().verify(genome, attestation, plan, intent=intent, admission_token=token) == plan.trace
def test_admission_token_rejects_label_mismatch() -> None:
genome = build_genome()
issuer = AdmissionTokenIssuer.generate()
token = issuer.issue(
AdmissionTokenClaims(
issuer_did="did:key:zissuer",
subject_did="did:key:zsubject",
audience="edge-fleet-a",
genome_hash=genome.content_hash(),
allowed_intents=["fork"],
safety_labels=["read-only"],
resource_budget=ResourceEnvelope(
cpu_millicores=500,
memory_mib=512,
bandwidth_mbps=100,
cost_usd_per_hour=1.0,
max_replicas=3,
),
issued_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
not_before=datetime(2024, 1, 1, tzinfo=timezone.utc),
expires_at=datetime(2030, 1, 2, tzinfo=timezone.utc),
nonce="nonce-2",
)
)
intent = ReplicaWeaveIntent(
source="catopt",
target="edge-01",
intent_kind="fork",
budget_hint={"cpu_millicores": 250},
safety_labels=["read-only", "requires-attestation"],
)
decision = AdmissionGate().authorize(genome, token, intent=intent, at=datetime(2024, 1, 1, 12, tzinfo=timezone.utc))
assert decision.approved is False
assert any("missing safety label" in reason for reason in decision.reasons)