Skip to content

v0.25.0

Latest

Choose a tag to compare

@ryanwi ryanwi released this 06 Jun 06:22
· 10 commits to main since this release

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.