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
10 changes: 10 additions & 0 deletions conformance/fixtures/runtime_governance_consistency.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "runtime-governance-consistency",
"version": "0.1",
"assertions": [
"runtime lifecycle evaluation uses the governance engine attached to the relationship",
"a PAUSE lifecycle outcome records invocation status PAUSED",
"a BLOCK lifecycle outcome records invocation status BLOCKED",
"runtime governance decisions remain auditable relational events"
]
}
5 changes: 4 additions & 1 deletion conformance/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"explicit_authorization_conditions",
"pure_governance_evaluation",
"explicit_governance_audit_recording",
"runtime_uses_relationship_governance",
"runtime_pause_and_block_are_distinct",
"cross_boundary_disclosure_admission_derivation_separation",
"consent_permission_intersection",
"raw_execution_metadata_minimized",
Expand Down Expand Up @@ -50,6 +52,7 @@
"fixtures/lifecycle_authority.json",
"fixtures/purpose_bound_governance.json",
"fixtures/time_conditions.json",
"fixtures/pure_governance_evaluation.json"
"fixtures/pure_governance_evaluation.json",
"fixtures/runtime_governance_consistency.json"
]
}
15 changes: 15 additions & 0 deletions docs/runtime-governance-consistency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Runtime governance consistency

TRIA Runtime must evaluate execution against the governance engine attached to the `Relationship` being executed.

A relationship may be constructed with caller-supplied governance behavior. Runtime must not silently replace that behavior with a fresh default `GovernanceEngine`, because doing so would create two governance realities for the same relationship.

Lifecycle outcomes also retain their meaning in the invocation audit trail:

- `ALLOW` may proceed to the remaining authorization checks.
- `PAUSE` records `InvocationResolved.status = "PAUSED"`.
- other non-allow lifecycle outcomes record `InvocationResolved.status = "BLOCKED"`.

`PAUSE` and `BLOCK` are intentionally distinct. A paused relationship is not equivalent to a prohibited or dissolved relationship; it represents an operational state in which execution should wait without erasing the possibility of continuation.

This build changes no event schema, projection version, or bundle format. It aligns Runtime with governance semantics already present in Core.
18 changes: 15 additions & 3 deletions src/tria/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from uuid import uuid4

from .core import Relationship
from .governance import GovernanceEngine
from .types import Capability, GovernanceDecision, GovernanceOutcome


Expand Down Expand Up @@ -71,11 +70,19 @@ class InvocationResult:
class Runtime:
"""Model-agnostic boundary between governed relationship state and execution."""

@staticmethod
def _resolution_status(outcome: GovernanceOutcome) -> str:
if outcome is GovernanceOutcome.PAUSE:
return "PAUSED"
return "BLOCKED"

def prepare(self, relationship: Relationship, request: InvocationRequest) -> InvocationPlan:
relationship.record_invocation_proposed(request)
decisions: list[GovernanceDecision] = []

lifecycle_decision = GovernanceEngine().require_runtime_execution(relationship.state)
# Runtime must use the governance engine attached to the relationship.
# Creating a new engine here would bypass caller-supplied governance behavior.
lifecycle_decision = relationship._governance.require_runtime_execution(relationship.state)
decisions.append(lifecycle_decision)
relationship.record_governance_decision(
lifecycle_decision,
Expand All @@ -85,7 +92,12 @@ def prepare(self, relationship: Relationship, request: InvocationRequest) -> Inv
check="lifecycle",
)
if lifecycle_decision.outcome is not GovernanceOutcome.ALLOW:
relationship.record_invocation_resolution(request.requested_by, request.request_id, "BLOCKED", reason=lifecycle_decision.reason)
relationship.record_invocation_resolution(
request.requested_by,
request.request_id,
self._resolution_status(lifecycle_decision.outcome),
reason=lifecycle_decision.reason,
)
return InvocationPlan(request=request, outcome=lifecycle_decision.outcome, decisions=tuple(decisions), reason=lifecycle_decision.reason)

for requirement in request.consent_requirements:
Expand Down
68 changes: 68 additions & 0 deletions tests/test_runtime_governance_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from tria import (
GovernanceDecision,
GovernanceEngine,
GovernanceOutcome,
InMemoryEventStore,
InvocationRequest,
LifecycleState,
Relationship,
Runtime,
Tria,
)


class BlockingRuntimeGovernance(GovernanceEngine):
def require_runtime_execution(self, state):
return GovernanceDecision(
GovernanceOutcome.BLOCK,
"test.runtime.injected",
"1",
"Injected governance blocks runtime execution.",
)


def test_runtime_uses_relationship_governance_engine():
rel = Relationship("relationship:custom", InMemoryEventStore(), governance=BlockingRuntimeGovernance())
rel._commit("RelationshipCreated", "tria:system", {"participants": ["human:a", "agent:b"]})

plan = Runtime().prepare(
rel,
InvocationRequest(requested_by="agent:b", action="continue", target="executor:any"),
)

assert plan.outcome is GovernanceOutcome.BLOCK
assert plan.decisions[0].policy_id == "test.runtime.injected"
resolutions = [event for event in rel.events if event.event_type == "InvocationResolved"]
assert resolutions[-1].payload["status"] == "BLOCKED"


def test_resting_runtime_records_paused_not_blocked():
rel = Tria().create_relationship(["human:a", "agent:b"])
rel.grant_lifecycle_authority("tria:system", "human:a")
rel.transition("human:a", LifecycleState.ACTIVE)
rel.transition("human:a", LifecycleState.RESTING)

plan = Runtime().prepare(
rel,
InvocationRequest(requested_by="agent:b", action="continue", target="executor:any"),
)

assert plan.outcome is GovernanceOutcome.PAUSE
resolutions = [event for event in rel.events if event.event_type == "InvocationResolved"]
assert resolutions[-1].payload["status"] == "PAUSED"


def test_dissolved_runtime_records_blocked():
rel = Tria().create_relationship(["human:a", "agent:b"])
rel.grant_lifecycle_authority("tria:system", "human:a")
rel.transition("human:a", LifecycleState.DISSOLVING)
rel.transition("human:a", LifecycleState.DISSOLVED)

plan = Runtime().prepare(
rel,
InvocationRequest(requested_by="agent:b", action="continue", target="executor:any"),
)

assert plan.outcome is GovernanceOutcome.BLOCK
resolutions = [event for event in rel.events if event.event_type == "InvocationResolved"]
assert resolutions[-1].payload["status"] == "BLOCKED"
Loading