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
This commit is contained in:
parent
8da4c5bf7a
commit
04e9573b5a
42 changed files with 1533 additions and 20 deletions
1
lab/__init__.py
Normal file
1
lab/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""The test-driver lab — the system under test."""
|
||||
BIN
lab/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
lab/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
lab/__pycache__/minimal.cpython-312.pyc
Normal file
BIN
lab/__pycache__/minimal.cpython-312.pyc
Normal file
Binary file not shown.
191
lab/minimal.py
Normal file
191
lab/minimal.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue