Skip to content

Releases: ryanwi/agent-control-plane

v0.25.0

Choose a tag to compare

@ryanwi ryanwi released this 06 Jun 06:22

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 the CancellableExecution protocol (async cancel(reason) +
    is_running); ACP never reaches across the abstraction boundary to force a stop. The
    RuntimeMonitor polls SessionRiskAccumulator.peek() (a new non-mutating assessment)
    while an execution is in flight and, on escalation at/above a configurable threshold
    (default HIGH, 250 ms poll), records a state-bearing RUNTIME_INTERRUPT_REQUESTED
    event and calls execution.cancel() — exactly once. McpGateway wires this in as an
    optional path via McpGatewayConfig.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 — ProposalRouter now accepts a
    SessionRiskAccumulator. When provided, routed decisions use the accumulator's
    effective risk for auto-approval and tier classification, and expose
    RoutingDecision.risk_escalated plus RoutingDecision.risk_escalation.

  • Pre-execution preconditions — ActionProposal.preconditions can now carry
    optional resource-state checks that run immediately before execution, after
    kill-switch checks. PreconditionVerifier records state-bearing
    PRECONDITION_FAILED events with expected/actual divergence details and uses the
    distinct GovernanceOutcome.PRECONDITION_FAILED telemetry outcome. Built-in
    providers cover SHA-256 file hashes and environment variables; hosts can supply
    PreconditionStateProvider implementations for other resource types. ACP reserves
    the internal metadata_json key __acp_preconditions for migration-free proposal
    persistence.

  • write_html_report(results, path) — stdlib-only HTML reporter for benchmark runs.
    Takes a list[BenchmarkRunResult] and emits a self-contained static file with a
    summary table and per-result drill-down (metrics, fitness breakdown, notes, run ID).
    Exported from agent_control_plane alongside the existing benchmark API.

  • BenchmarkRunResult shape test — test_benchmark_run_result_shape asserts every
    field on the DTO (types, UTC-awareness, config_hash length, 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 full PolicyEngine path and returns a
    RoutingDecision without 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 to PENDING, and appends a state-bearing APPROVAL_REVOKED event. Callers
    are responsible for re-issuing a ticket via create_ticket() when manual re-approval
    is needed. ApprovalStatus.REVOKED and EventKind.APPROVAL_REVOKED are new enum
    values. ApprovalTicket gains revoked_by, revocation_reason, and revoked_at
    fields (all nullable). The underlying get_ticket_for_update() lock method is added
    to ApprovalRepository and AsyncApprovalRepository protocols.

  • SyncControlPlane.route_proposal() and ControlPlaneFacade.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 a RoutingDecision. Passing a
    risk_accumulator at construction time wires it into every route_proposal() call —
    the same escalation path as ProposalRouter in the async stack. A non-state-bearing
    SESSION_RISK_ESCALATED event is written to the audit log when escalation fires.

    ControlPlaneFacade.__init__() and ControlPlaneFacade.from_database_url() now accept
    risk_accumulator: SessionRiskAccumulator | None = None (default None; no breaking
    change). ControlPlaneSetup.build() auto-constructs a SessionRiskAccumulator from
    GovernanceConfig.risk_patterns when 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()
    and get_ticket_for_update() were missing .with_for_update(). On PostgreSQL,
    concurrent sync callers (e.g. revoke_ticket + approve_ticket on 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 called update_status(proposal_id, PENDING) unconditionally. A
    proposal already in EXECUTED or FAILED state would be silently reset to PENDING,
    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 raise ValueError
    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 using model_copy, diverging from the facade
    pattern. Callers using ApprovalGate directly should use the facade's
    revoke_ticket() instead.

  • ControlPlaneFacade.run() now enforces preconditions automatically — run()
    accepts preconditions, proposal_id, action_id, and precondition_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_FAILED event, aborts the session, and raises
    RuntimeError("precondition_failed") without entering the body. Previously
    non-MCP callers using run() had no automatic precondition enforcement — the
    feature failed open by omission unless callers manually invoked
    verify_preconditions().

  • McpGateway precondition failure now raises PreconditionFailedError and emits
    TOOL_CALL_BLOCKED
    — previously the only denial path that returned ok=False
    instead of raising, breaking callers written against the McpGovernanceError
    exception contract. The TOOL_CALL_ALLOWED event 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" and outcome=GovernanceOutcome.PRECONDITION_FAILED)
    is emitted before the raise. PreconditionFailedError is a new McpGovernanceError
    subclass exported from agent_control_plane.

v0.24.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:53

Fixed (security)

  • Killed sessions can no longer be reactivated — activate_session() and resume_session() raise when killed_at is 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_at are new fields (existing rows default to NULL).

  • Event sequence monotonicity enforced on replay — EventStore.replay() raises SessionStateIntegrityError if 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)

  • ActionName stripped to UNKNOWN sentinel — 35 hardcoded domain action names removed. Use plain strings: decision="wire_transfer" instead of decision=ActionName.WIRE_TRANSFER. register_action_names() no longer required before use.

  • parse_action_name() no longer coerces unknown strings to UNKNOWN — any non-empty, non-"unknown" string passes through as-is. Unlisted actions reach the PolicyEngine normally instead of being blocked.

See docs/compatibility.md for migration details.

v0.23.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:16

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, GoalStatus

SessionCheckpoint, RollbackResult, and ControlPlaneScorecard remain stable.

See docs/compatibility.md for the full migration guide.

0.22.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:07

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 a RunHandle for 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

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:00

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 — BUDGET in MIXED mode previously returned True on any DB exception. Budget operations now fail closed (raise). Opt out via category_overrides={OperationCategory.BUDGET: ResilienceMode.FAIL_OPEN}.

  • McpGatewayConfig.auto_create_sessions defaults to False — previously True, silently creating sessions with max_cost=10000/max_action_count=100. Callers must now supply session_id explicitly.

  • increment_budget (sync backend) is now a single atomic UPDATE — eliminates the check/increment TOCTOU race via UPDATE … WHERE used_cost + cost ≤ max_cost ….

High — fixed:

  • EventConfig.unknown_event_policy defaults to RAISE — ControlPlaneSetup previously passed IGNORE, silently dropping unmapped app events.

  • UnknownAppEventError is never swallowed by resilient wrappers — re-raised before the TELEMETRY fail-open handler in both sync and async.

  • TokenUsage.estimated_cost_usd rejects negative values — @model_validator raises ValidationError for negative costs.

Medium — fixed:

  • Agent-abort kill switch events are now state-bearing — KillSwitch._abort_agent now emits KILL_SWITCH_TRIGGERED with state_bearing=True.

  • Evaluator plugins cannot override built-in names — EvaluatorRegistry._discover warns and skips any plugin whose name collides with an already-registered evaluator.

Breaking (pre-1.0)

  • McpGatewayConfig.auto_create_sessions: True → False. Supply session_id explicitly or pass McpGatewayConfig(auto_create_sessions=True) to restore.
  • EventConfig.unknown_event_policy: IGNORE → RAISE. Add all event names to event_map, or pass EventConfig(unknown_event_policy=UnknownAppEventPolicy.IGNORE) to restore.
  • MIXED resilience mode BUDGET: FAIL_OPEN → FAIL_CLOSED. Pass category_overrides={OperationCategory.BUDGET: ResilienceMode.FAIL_OPEN} to restore.

v0.20.1

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:00

Documented

  • Compatibility & Upgrading guide — docs/compatibility.md now consolidates the compatibility policy with a task-oriented cross-version upgrade guide covering all breaking migrations from 0.17.0–0.20.0.

v0.20.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 05:00

Added

  • Per-session agent revocation enforced and facade-exposed — completes the revocation feature from 0.19.0.
    • McpGateway.handle_tool_call blocks a revoked agent before tool resolution (raises PolicyDeniedError).
    • ProposalRouter.route() short-circuits to BLOCKED for a revoked agent regardless of capabilities.
    • revoke / reinstate / is_revoked surfaced on new .agents gateway on both sync and async facades.

Fixed

  • Example API correctness — 14 examples corrected: missing await on ProposalRouter.route(), phantom sub-object accesses on engine/repo classes, and flat attribution kwargs on EventStore.append() instead of EventMetadata. quickstart.py now runs end-to-end without error.

v0.19.1

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 04:59

Changed

  • Internal: finalize_scorecard extracted to _scorecard.py — scorecard derived-field computation deduplicated from sync and async observer gateways. No behaviour change.
  • is_agent_revoked uses EXISTS — storage query now uses SQL EXISTS instead of fetching a row. No behaviour change.

v0.19.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 04:59

Added

  • Approval grant-rate telemetry — operational scorecard now tracks approvals_granted, approvals_denied, and derived approval_grant_rate (approval-fatigue signal). export_scorecard() emits cp.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 by agent_session_revocations table, emitting state-bearing AGENT_REVOKED / AGENT_REINSTATED events.

Fixed

  • Gateway classes exposed on facades since 0.18.0 are now exported from agent_control_plane.

v0.18.0

Choose a tag to compare

@ryanwi ryanwi released this 02 Jun 04:57

Changed

  • Focused facade gateways — ControlPlaneFacade and AsyncControlPlaneFacade split into focused gateway sub-objects: .sessions, .lifecycle, .approvals, .budget, .agentic, .observer, .maintenance. E.g. facade.open_session(...) → facade.sessions.open_session(...).
  • EventMetadata for event append — EventStore.append and event-repository append now take a single EventMetadata frozen dataclass instead of individual attribution kwargs.

Breaking (pre-1.0)

  • Facade operations reorganized onto gateway sub-objects (see Changed).
  • EventStore.append / event-repository append signature: individual attribution kwargs → EventMetadata dataclass.