Repository navigation
Added
-
Cooperative mid-execution interrupt (
RuntimeMonitor) — ACP can now ask a host
executor to stop an action that is already running when session risk spikes, closing
the gap between "cleared the kill switch" and "EventStore records the outcome". Hosts
opt in by implementing theCancellableExecutionprotocol (async cancel(reason)+
is_running); ACP never reaches across the abstraction boundary to force a stop. The
RuntimeMonitorpollsSessionRiskAccumulator.peek()(a new non-mutating assessment)
while an execution is in flight and, on escalation at/above a configurable threshold
(defaultHIGH, 250 ms poll), records a state-bearingRUNTIME_INTERRUPT_REQUESTED
event and callsexecution.cancel()— exactly once.McpGatewaywires this in as an
optional path viaMcpGatewayConfig.execution_factory; whether the executor actually
stops remains the executor's responsibility. The monitor never intercepts the
execution result or swallows executor errors. -
Optional session-risk routing integration —
ProposalRouternow accepts a
SessionRiskAccumulator. When provided, routed decisions use the accumulator's
effective risk for auto-approval and tier classification, and expose
RoutingDecision.risk_escalatedplusRoutingDecision.risk_escalation. -
Pre-execution preconditions —
ActionProposal.preconditionscan now carry
optional resource-state checks that run immediately before execution, after
kill-switch checks.PreconditionVerifierrecords state-bearing
PRECONDITION_FAILEDevents with expected/actual divergence details and uses the
distinctGovernanceOutcome.PRECONDITION_FAILEDtelemetry outcome. Built-in
providers cover SHA-256 file hashes and environment variables; hosts can supply
PreconditionStateProviderimplementations for other resource types. ACP reserves
the internalmetadata_jsonkey__acp_preconditionsfor migration-free proposal
persistence. -
write_html_report(results, path)— stdlib-only HTML reporter for benchmark runs.
Takes alist[BenchmarkRunResult]and emits a self-contained static file with a
summary table and per-result drill-down (metrics, fitness breakdown, notes, run ID).
Exported fromagent_control_planealongside the existing benchmark API. -
BenchmarkRunResultshape test —test_benchmark_run_result_shapeasserts every
field on the DTO (types, UTC-awareness,config_hashlength, timestamp ordering) to
serve as a stable contract for downstream consumers. -
McpGateway.simulate_action(proposal)— synchronous, side-effect-free policy
preview: classifies a proposal through the fullPolicyEnginepath and returns a
RoutingDecisionwithout storing the proposal, creating approval tickets, or
emitting events. Risk accumulator is excluded (simulated tier may differ from the
live tier if session risk has accumulated); agent revocation is not checked. Useful
for pre-flight tier inspection before committing an action. -
Approval ticket revocation —
ApprovalGateway.revoke_ticket()(sync and async,
including the resilient wrapper) revokes an approved ticket, resets the associated
proposal toPENDING, and appends a state-bearingAPPROVAL_REVOKEDevent. Callers
are responsible for re-issuing a ticket viacreate_ticket()when manual re-approval
is needed.ApprovalStatus.REVOKEDandEventKind.APPROVAL_REVOKEDare new enum
values.ApprovalTicketgainsrevoked_by,revocation_reason, andrevoked_at
fields (all nullable). The underlyingget_ticket_for_update()lock method is added
toApprovalRepositoryandAsyncApprovalRepositoryprotocols. -
SyncControlPlane.route_proposal()andControlPlaneFacade.route_proposal()—
new method on both types exposes the full routing path (policy classification + optional
session-risk escalation) to sync callers without manual engine construction. Both accept
(proposal, policy_snapshot)and return aRoutingDecision. Passing a
risk_accumulatorat construction time wires it into everyroute_proposal()call —
the same escalation path asProposalRouterin the async stack. A non-state-bearing
SESSION_RISK_ESCALATEDevent is written to the audit log when escalation fires.ControlPlaneFacade.__init__()andControlPlaneFacade.from_database_url()now accept
risk_accumulator: SessionRiskAccumulator | None = None(defaultNone; no breaking
change).ControlPlaneSetup.build()auto-constructs aSessionRiskAccumulatorfrom
GovernanceConfig.risk_patternswhen patterns are present, so the standard setup path
now wires session-risk routing without host-app engine construction.
Fixed
-
Sync ticket reads now hold a row lock —
SyncSqlAlchemyUnitOfWork.get_pending_ticket_for_update()
andget_ticket_for_update()were missing.with_for_update(). On PostgreSQL,
concurrent sync callers (e.g.revoke_ticket+approve_ticketon the same ticket)
could both read the same row without a lock, both pass the application-level status
guard, and both write — last write wins silently. The async backend already locked the
row; the sync backend now does too. -
revoke_ticket()rejects revocation of already-executed proposals — both the sync
and async facades calledupdate_status(proposal_id, PENDING)unconditionally. A
proposal already inEXECUTEDorFAILEDstate would be silently reset toPENDING,
making it eligible for re-approval and re-execution of an action that had already run.
Both facades now fetch the current proposal status before writing and raiseValueError
when the proposal is in a terminal state. -
Removed orphaned
ApprovalGate.revoke_approval()engine method — the method was
never called; both facades implement revoke logic inline. The engine version also
mutated the DTO in-place rather than usingmodel_copy, diverging from the facade
pattern. Callers usingApprovalGatedirectly should use the facade's
revoke_ticket()instead. -
ControlPlaneFacade.run()now enforces preconditions automatically —run()
acceptspreconditions,proposal_id,action_id, andprecondition_providers
kwargs. When supplied, preconditions are verified after the kill-switch check and
before the body executes. A failing check appends a state-bearing
PRECONDITION_FAILEDevent, aborts the session, and raises
RuntimeError("precondition_failed")without entering the body. Previously
non-MCP callers usingrun()had no automatic precondition enforcement — the
feature failed open by omission unless callers manually invoked
verify_preconditions(). -
McpGatewayprecondition failure now raisesPreconditionFailedErrorand emits
TOOL_CALL_BLOCKED— previously the only denial path that returnedok=False
instead of raising, breaking callers written against theMcpGovernanceError
exception contract. TheTOOL_CALL_ALLOWEDevent emitted before the precondition
check was also left without a corresponding outcome event, leaving an unclosed
governance cycle in the MCP stream. Both are corrected:TOOL_CALL_BLOCKED(with
reason="precondition_failed"andoutcome=GovernanceOutcome.PRECONDITION_FAILED)
is emitted before the raise.PreconditionFailedErroris a newMcpGovernanceError
subclass exported fromagent_control_plane.