polyc-agent 2026.8.3

The agent turn loop: provider + tool-call routing, shared by the control plane and harness.
//! Durable routine-fire tool grants.
//!
//! A grant is a signed `approval_response` an owner minted during a
//! routine's attended setup rehearsal, honored on later UNATTENDED firings
//! of the same routine — the mechanism that lets a scheduled fire use a
//! tool the capability gate would otherwise escalate to a person who is not
//! present. [`RoutineGrantSet`] is what the harness verifies off the wire
//! (`polyc_turn_runner::verify_routine_tool_grants`) and hands to
//! [`crate::RunTurnOptions::routine_tool_grants`]; the gate that consults it
//! (`crate::routine_grant_approves`) is armed only on a routine's fire
//! dispatch (`crate::RunTurnOptions::fire_dispatch`), scheduled or the
//! attended setup rehearsal alike.

use std::collections::HashMap;

/// Verified routine-fire tool grants for one turn.
///
/// Empty on every ordinary conversation — a grant is inert unless
/// [`crate::RunTurnOptions::fire_dispatch`] is `true`.
#[derive(Debug, Clone, Default)]
pub struct RoutineGrantSet {
    /// Per-tool grants, keyed by tool name — the latest verified grant for
    /// each tool the owner scoped individually (`grant_scope: "tool"`).
    pub per_tool: HashMap<String, PerToolGrant>,
    /// The routine's blanket grant, if the owner minted one
    /// (`grant_scope: "blanket_below_high"` or `"blanket_all"`). At most one
    /// applies per turn — the latest-wins fold keeps only the newest.
    pub blanket: Option<BlanketGrant>,
}

/// One tool-scoped grant: the capability shortfall it covers and
/// the exact tool shape it was minted against.
#[derive(Debug, Clone)]
pub struct PerToolGrant {
    /// The capability set the signed grant covered at minting time. A call
    /// is admitted only when this includes everything the call is
    /// currently missing — the same `#595` scope rule a session grant
    /// follows.
    pub covered: polyc_capability::CapabilitySet,
    /// The granted tool's descriptor hash
    /// ([`polyc_llm::ToolSpec::descriptor_hash`]) at minting time. Admission
    /// is governed by [`Self::covered`] and the grant's other signed
    /// bindings, not by this hash: this is a drift-detection and audit
    /// signal — a call whose CURRENT descriptor hash no longer matches
    /// still executes, but is recorded as drifted so a later surface can
    /// tell the routine's owner its tool's definition changed since they
    /// approved it. A MISSING current descriptor (no advertised spec at
    /// all) is a separate case: it makes no grant applicable, because
    /// there is nothing to classify the call's risk against.
    pub descriptor_hash: String,
}

/// A routine's blanket grant: admits every tool below a risk
/// ceiling, without per-tool scoping.
#[derive(Debug, Clone, Copy)]
pub struct BlanketGrant {
    /// Whether the blanket admits High-risk (destructive) tools too
    /// (`grant_scope: "blanket_all"`). `false` is `"blanket_below_high"` —
    /// every tool EXCEPT a destructive one.
    pub include_high: bool,
}