polyc-tools 2026.8.3

The in-process tool core for polychrome agents: local executors (coding, web fetch, wallet, ...), the tool registry, and MCP composition. The networked connectors live in polyc-connectors.
//! Spec for the agent-evaluable admin `list_admins` tool.
//!
//! The read-only companion to the `invite`/`revoke`/`demote` set: an admin
//! asking in natural language — "who are the admins", "who can approve
//! this", "who else has admin here" — should be answered from the durable
//! admin roster, not guessed at by the model.
//!
//! Like the rest of the roster-admin family it has no in-process
//! implementation: the conversation sandbox can't reach the persona store.
//! The harness advertises it via the same control-plane proxy; the control
//! plane runs it (admin-gated) against
//! `polyc_persona::PersonaHost::admin_roster` — named, not linked: this crate
//! does not depend on `polyc-persona`, so an intra-doc link there cannot
//! resolve and fails `doc-strict`.
//!
//! Read-only, so — unlike `invite`/`revoke`/`demote`, which each require a
//! never-granted capability marker and therefore always escalate to a human
//! — an admin can ask this as freely as any other report. It is the same
//! posture `list_provisional_personas` carries.
//!
//! The result names every admin the deployment has, whichever surface each of
//! them holds admin on — membership is governance information and the caller
//! is already admin-gated. What is scoped is the ACCOUNT ID: an admin's
//! provider-native user id is rendered just for the surface the asking
//! conversation is happening on, so the answer never hands over the account id
//! of an admin it can't reach. A display NAME is not covered by that, because
//! on a surface where the address is the name the two are one string — the
//! email edge sets both to the sender address
//! (`crates/email/src/edge.rs:156-170`). See `list_admins_nav` for the whole
//! boundary, the residuals it accepts, and why it reverses the surface-scoped
//! roster this tool first shipped with.

use polyc_llm::ToolSpec;
use serde_json::json;

/// The `list_admins` tool name.
pub const TOOL_NAME: &str = "list_admins";

/// Every list-admins tool name, for allowlist checks and dispatch (one, today).
pub const ALL: &[&str] = &[TOOL_NAME];

/// Every list-admins tool spec.
#[must_use]
pub fn all_specs() -> Vec<ToolSpec> {
    vec![list_admins_spec()]
}

/// `list_admins` spec — an admin reads the current admin roster.
///
/// Admin-only: the control plane refuses a non-admin caller and returns no
/// membership at all. Read-only — nothing is ever changed by this tool — and
/// not egress.
#[must_use]
pub fn list_admins_spec() -> ToolSpec {
    ToolSpec::new(
        TOOL_NAME,
        "For an admin only: list who currently has the admin role. Use it when an admin asks \
         who the admins are, who else can approve something, or who can invite and remove \
         people — for example \"who are the admins here\" or \"who else has admin\". Takes no \
         arguments. The list is complete: every admin on this Polychrome instance, named, \
         wherever each of them works with it — so you can answer with all of them. Each entry \
         carries reachable_here. When that is false the entry has no user_id: there is no \
         account here to mention, so name that person in plain text. It never means they are \
         unknown or that anything went wrong, so name them like anyone else. An entry with no \
         name at all came through without one — say that entry has no name rather than guessing \
         at who it is. If the person asking isn't an admin, it returns a refusal instead of a \
         list.",
        json!({
            "type": "object",
            "properties": {},
            "additionalProperties": false
        }),
    )
    .titled("List the admins (admin)")
    .read_only()
}