Repository navigation
Releases: ryanwi/agent-control-plane
Release list
v0.25.0
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.
v0.24.0
Fixed (security)
-
Killed sessions can no longer be reactivated —
activate_session()andresume_session()raise whenkilled_atis set. Previously a session paused by the kill switch could be silently resumed.unkill_session()added for explicit operator recovery.SessionState.killed_at/SessionSummary.killed_atare new fields (existing rows default toNULL). -
Event sequence monotonicity enforced on replay —
EventStore.replay()raisesSessionStateIntegrityErrorif the sequence contains gaps or duplicates.
Added
SessionState.started_at— stamped when a session first becomes ACTIVE; preserved through pause/resume. Enables run-duration computation.ControlPlaneObserver.list_sessions()— now available on the sync facade, matching the async counterpart.
Changed (breaking)
-
ActionNamestripped toUNKNOWNsentinel — 35 hardcoded domain action names removed. Use plain strings:decision="wire_transfer"instead ofdecision=ActionName.WIRE_TRANSFER.register_action_names()no longer required before use. -
parse_action_name()no longer coerces unknown strings toUNKNOWN— any non-empty, non-"unknown"string passes through as-is. Unlisted actions reach thePolicyEnginenormally instead of being blocked.
See docs/compatibility.md for migration details.
v0.23.0
Breaking changes
ID type aliases are now plain str
AgentId, OrgId, UserId, TeamId, ModelId, ResourceId, and IdempotencyKey were NewType wrappers with no added behavior. They are now TypeAlias = str. Drop any OrgId(x) constructor calls or cast(AgentId, x) patterns — plain strings assign directly.
Agentic types moved to agent_control_plane.experimental
Goal, GoalStatus, Plan, PlanStep, PlanStepStatus, PlanProgress, EvaluationResult, EvaluationDecision, GuardrailDecision, GuardrailPhase, and HandoffResult are no longer exported from the top-level package. Update imports:
# before
from agent_control_plane import Goal, Plan, GoalStatus
# after
from agent_control_plane.experimental import Goal, Plan, GoalStatusSessionCheckpoint, RollbackResult, and ControlPlaneScorecard remain stable.
See docs/compatibility.md for the full migration guide.
0.22.0
Breaking changes
Four SQL table names renamed — Python class names unchanged. Existing deployments need ALTER TABLE before upgrading; see docs/compatibility.md.
| Old | New |
|---|---|
control_sessions |
agent_runs |
control_events |
audit_events |
action_proposals |
proposals |
command_ledger |
idempotency_ledger |
Added
cp.run()context manager — opens/activates/closes a session automatically, yields aRunHandlefor tagging. Aborts with error repr on exception. Available on all four facade/resilient classes (async + sync).RunHandle— exported from top-level package.handle.tag(**metadata)accumulates into the session close payload.examples/audit_trail.py— runnable Tier 2 demo.- README adoption tiers — Tier 1 (cost tracking), Tier 2 (audit trail), Tier 3 (full governance).
v0.21.0
Security
Seven security hardening fixes across three severity tiers, guided by a formal security review of the 0.20.x codebase.
Critical — fixed:
-
Resilient budget gate is now fail-closed —
BUDGETin MIXED mode previously returnedTrueon any DB exception. Budget operations now fail closed (raise). Opt out viacategory_overrides={OperationCategory.BUDGET: ResilienceMode.FAIL_OPEN}. -
McpGatewayConfig.auto_create_sessionsdefaults toFalse— previouslyTrue, silently creating sessions withmax_cost=10000/max_action_count=100. Callers must now supplysession_idexplicitly. -
increment_budget(sync backend) is now a single atomic UPDATE — eliminates the check/increment TOCTOU race viaUPDATE … WHERE used_cost + cost ≤ max_cost ….
High — fixed:
-
EventConfig.unknown_event_policydefaults toRAISE—ControlPlaneSetuppreviously passedIGNORE, silently dropping unmapped app events. -
UnknownAppEventErroris never swallowed by resilient wrappers — re-raised before the TELEMETRY fail-open handler in both sync and async. -
TokenUsage.estimated_cost_usdrejects negative values —@model_validatorraisesValidationErrorfor negative costs.
Medium — fixed:
-
Agent-abort kill switch events are now state-bearing —
KillSwitch._abort_agentnow emitsKILL_SWITCH_TRIGGEREDwithstate_bearing=True. -
Evaluator plugins cannot override built-in names —
EvaluatorRegistry._discoverwarns and skips any plugin whose name collides with an already-registered evaluator.
Breaking (pre-1.0)
McpGatewayConfig.auto_create_sessions:True→False. Supplysession_idexplicitly or passMcpGatewayConfig(auto_create_sessions=True)to restore.EventConfig.unknown_event_policy:IGNORE→RAISE. Add all event names toevent_map, or passEventConfig(unknown_event_policy=UnknownAppEventPolicy.IGNORE)to restore.- MIXED resilience mode
BUDGET:FAIL_OPEN→FAIL_CLOSED. Passcategory_overrides={OperationCategory.BUDGET: ResilienceMode.FAIL_OPEN}to restore.
v0.20.1
v0.20.0
Added
- Per-session agent revocation enforced and facade-exposed — completes the revocation feature from 0.19.0.
McpGateway.handle_tool_callblocks a revoked agent before tool resolution (raisesPolicyDeniedError).ProposalRouter.route()short-circuits toBLOCKEDfor a revoked agent regardless of capabilities.revoke/reinstate/is_revokedsurfaced on new.agentsgateway on both sync and async facades.
Fixed
- Example API correctness — 14 examples corrected: missing
awaitonProposalRouter.route(), phantom sub-object accesses on engine/repo classes, and flat attribution kwargs onEventStore.append()instead ofEventMetadata.quickstart.pynow runs end-to-end without error.
v0.19.1
Changed
- Internal:
finalize_scorecardextracted to_scorecard.py— scorecard derived-field computation deduplicated from sync and async observer gateways. No behaviour change. is_agent_revokedusesEXISTS— storage query now uses SQLEXISTSinstead of fetching a row. No behaviour change.
v0.19.0
Added
- Approval grant-rate telemetry — operational scorecard now tracks
approvals_granted,approvals_denied, and derivedapproval_grant_rate(approval-fatigue signal).export_scorecard()emitscp.approvals_granted,cp.approvals_denied,cp.approval_grant_rate. AgentMetadata.is_capable(action)— single source of truth for an agent's effective authority. Establishes and documents the "delegation does not elevate trust" invariant.- Per-session agent revocation primitive — new
AgentSessionGuard(revoke/reinstate/is_revoked) backed byagent_session_revocationstable, emitting state-bearingAGENT_REVOKED/AGENT_REINSTATEDevents.
Fixed
- Gateway classes exposed on facades since 0.18.0 are now exported from
agent_control_plane.
v0.18.0
Changed
- Focused facade gateways —
ControlPlaneFacadeandAsyncControlPlaneFacadesplit into focused gateway sub-objects:.sessions,.lifecycle,.approvals,.budget,.agentic,.observer,.maintenance. E.g.facade.open_session(...)→facade.sessions.open_session(...). EventMetadatafor event append —EventStore.appendand event-repositoryappendnow take a singleEventMetadatafrozen dataclass instead of individual attribution kwargs.
Breaking (pre-1.0)
- Facade operations reorganized onto gateway sub-objects (see Changed).
EventStore.append/ event-repositoryappendsignature: individual attribution kwargs →EventMetadatadataclass.