test-driver/lab/minimal.py
tegwick 04e9573b5a T04: deterministic semantic kernel
Alice/Bob/Carol runs end to end, deterministically, replayable from seed.
16 tests pass, no third-party dependencies.

- src/testdriver: intent, provenance, world, actions, drivers, observers,
  oracles, evidence, energy, scenario, runner
- lab/minimal.py: the SUT, exposing the independent observation channel
  required by D-07
- evidence is stratified S1/S2/S3; Runner refuses to attribute S2/S3 to an
  actor; claims are frozen and provenance-checked at construction
- missing evidence yields INCONCLUSIVE, which outranks PASS in the run verdict
- EnergyEvents captured, no scoring (H-005 dormant)

The observation channel records both stored state and an out-of-band
enforcement probe; their disagreement is an invariant and is what detects an
authorization defect that leaves the audit trail intact. A seeded
RevokeIsCosmetic lab fails the run via both the claim and that invariant.

Also closes TD-WP-0001-T02 (stack and commands now exist).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1629012@bnt-lap001
Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
2026-08-22 23:21:07 +02:00

191 lines
7.2 KiB
Python

"""Minimal lab: the seed of the system under test.
Deliberately small. Users own resources, share them with read or write
permission, and revoke that sharing. Every state change is audited.
Two access paths exist, and the distinction is the whole point:
* the **enforcement path** (`request`) is what an actor uses. It checks
authorization and can therefore be *wrong* — that is where a seeded
authorization defect lives.
* the **observation channel** (`ObservationChannel`) reads stored state directly,
without authorization. It is the independent channel required by decision
D-07.
An oracle that consulted only stored state would verify the framework's own
reimplementation of the rules rather than the system's enforcement of them. An
oracle that consulted only the enforcement path would have no way to notice that
enforcement and record disagree. test-driver observes both, and treats
disagreement between them as meaningful in its own right — that disagreement is
the precise signature of the M05 authorization defect.
TD-WP-0002-T05 grows this into the full lab with an HTTP API, a browser UI and
the labelled mutation catalogue. It is kept in-process here so that T04 can prove
the kernel without dragging in a web stack.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
Permission = Literal["READ", "WRITE"]
class Denied(Exception):
"""The enforcement path refused the request."""
@dataclass(slots=True)
class AuditRecord:
sequence: int
event: str
actor_id: str
resource_id: str | None = None
subject_id: str | None = None
permission: str | None = None
@dataclass(slots=True)
class MinimalLab:
"""In-process resource-sharing service."""
version: str = "lab-0.1.0-baseline"
users: dict[str, str] = field(default_factory=dict) # user_id -> token
_tokens: dict[str, str] = field(default_factory=dict) # token -> user_id
resources: dict[str, dict[str, Any]] = field(default_factory=dict)
grants: dict[tuple[str, str], Permission] = field(default_factory=dict)
audit: list[AuditRecord] = field(default_factory=list)
_seq: int = 0
# -- setup -----------------------------------------------------------
def add_user(self, user_id: str) -> str:
token = f"tok-{user_id}"
self.users[user_id] = token
self._tokens[token] = user_id
return token
def _audit(self, event: str, actor_id: str, **kw: Any) -> None:
self._seq += 1
self.audit.append(AuditRecord(self._seq, event, actor_id, **kw))
def _whoami(self, token: str) -> str:
if token not in self._tokens:
raise Denied("unknown token")
return self._tokens[token]
# -- enforcement path (what actors use) ------------------------------
def request(self, token: str, op: str, **args: Any) -> Any:
"""The single entry point actors go through. Authorization is enforced here."""
user_id = self._whoami(token)
handler = getattr(self, f"_op_{op}", None)
if handler is None:
raise Denied(f"unknown operation {op!r}")
return handler(user_id, **args)
def _op_create_resource(self, user_id: str, resource_id: str, content: str) -> dict:
self.resources[resource_id] = {"owner": user_id, "content": content}
self._audit("create", user_id, resource_id=resource_id)
return {"resource_id": resource_id}
def _op_read_resource(self, user_id: str, resource_id: str) -> dict:
resource = self.resources.get(resource_id)
if resource is None:
raise Denied("no such resource")
if not self._may_read(user_id, resource_id):
raise Denied("not authorized to read")
return {"resource_id": resource_id, "content": resource["content"]}
def _op_grant(
self, user_id: str, resource_id: str, subject_id: str, permission: Permission
) -> dict:
resource = self.resources.get(resource_id)
if resource is None or resource["owner"] != user_id:
raise Denied("only the owner may grant")
self.grants[(resource_id, subject_id)] = permission
self._audit(
"grant", user_id, resource_id=resource_id,
subject_id=subject_id, permission=permission,
)
return {"granted": permission}
def _op_revoke(self, user_id: str, resource_id: str, subject_id: str) -> dict:
resource = self.resources.get(resource_id)
if resource is None or resource["owner"] != user_id:
raise Denied("only the owner may revoke")
self.grants.pop((resource_id, subject_id), None)
self._audit("revoke", user_id, resource_id=resource_id, subject_id=subject_id)
return {"revoked": True}
def _may_read(self, user_id: str, resource_id: str) -> bool:
"""The authorization rule as the system actually enforces it."""
resource = self.resources.get(resource_id)
if resource is None:
return False
if resource["owner"] == user_id:
return True
return (resource_id, user_id) in self.grants
class ObservationChannel:
"""Independent read access to lab state — decision D-07.
Bypasses authorization deliberately. This is the channel test-driver requires
of any system under test, and the main integration burden the framework
imposes on an adopter.
"""
def __init__(self, lab: MinimalLab) -> None:
self._lab = lab
@property
def version(self) -> str:
return self._lab.version
def state_permission(self, user_id: str, resource_id: str) -> str | None:
"""What the stored record says, independent of any enforcement decision."""
resource = self._lab.resources.get(resource_id)
if resource is None:
return None
if resource["owner"] == user_id:
return "OWNER"
return self._lab.grants.get((resource_id, user_id))
def probe_read(self, user_id: str, resource_id: str) -> bool:
"""Exercise the enforcement path out-of-band and report what it did.
This uses the subject's own credentials, which can look like a violation
of actor isolation but is not: independence means the *actor's report* is
never the evidence. The observer issues its own request and records the
raw outcome. No actor is ever asked whether it succeeded.
"""
token = self._lab.users.get(user_id)
if token is None:
return False
try:
self._lab.request(token, "read_resource", resource_id=resource_id)
except Denied:
return False
return True
def audit_events(self, resource_id: str) -> list[dict[str, Any]]:
return [
{
"sequence": r.sequence,
"event": r.event,
"actor_id": r.actor_id,
"subject_id": r.subject_id,
"permission": r.permission,
}
for r in self._lab.audit
if r.resource_id == resource_id
]
def build_baseline() -> tuple[MinimalLab, dict[str, str]]:
"""Known initial state, so that runs replay from the same starting point."""
lab = MinimalLab()
tokens = {user: lab.add_user(user) for user in ("alice", "bob", "carol")}
return lab, tokens