polyc-tools 2026.9.0

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.
//! Specs for the agent-evaluable admin `list_provisional_personas` and
//! `delete_provisional_persona` tools (`#1599`).
//!
//! Provisional personas are minted with zero validation on first contact
//! from any unrecognized identity (`polyc_persona::PersonaStore::new_provisional`)
//! — a test message, a demo script, a one-off mention, all leave a stray
//! persona behind. These two tools give an admin a way to find and clean
//! those up without touching `revoke` (which only works on already-linked
//! personas and would refuse a provisional target's non-admin-relevant
//! state anyway).
//!
//! `delete_provisional_persona` is the narrow, always-human-approved sibling
//! of `revoke`: it tombstones a target ONLY if its status is currently
//! provisional, refusing outright otherwise — so it can never be pointed at
//! a real, linked persona by mistake. `list_provisional_personas` is
//! read-only enumeration and does NOT carry that same escalate-every-call
//! bar: an admin can freely ask "what provisional personas are there" the
//! same way they'd ask for any other read-only report.
//!
//! Like `revoke`/`demote`/`unlink_identity` these have no in-process
//! implementation: the conversation sandbox can't reach the persona store.
//! The harness advertises them via the same control-plane proxy; the control
//! plane runs them (admin-gated).

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

/// The `delete_provisional_persona` tool name.
pub const DELETE_PROVISIONAL_PERSONA: &str = "delete_provisional_persona";

/// The `list_provisional_personas` tool name.
pub const LIST_PROVISIONAL_PERSONAS: &str = "list_provisional_personas";

/// Every provisional-persona tool name, for allowlist checks and dispatch.
pub const ALL: &[&str] = &[DELETE_PROVISIONAL_PERSONA, LIST_PROVISIONAL_PERSONAS];

/// The required argument for `delete_provisional_persona`: the target's
/// provider-native user id.
///
/// Taken from the mention markup already in the agent's input — same field
/// name as `invite`/`revoke`/`demote`/`unlink_identity`, the shared
/// extraction helper on every edge relies on this.
pub const ARG_TARGET_USER_ID: &str = "target_user_id";

/// The optional argument for `list_provisional_personas`: a continuation
/// token from a prior call's result, to see more when a first page didn't
/// cover everything.
pub const ARG_PAGE_TOKEN: &str = "page_token";

/// Every provisional-persona tool spec.
#[must_use]
pub fn all_specs() -> Vec<ToolSpec> {
    vec![
        delete_provisional_persona_spec(),
        list_provisional_personas_spec(),
    ]
}

/// `delete_provisional_persona` spec — an admin tombstones a persona that is
/// STILL provisional (never linked, never granted anything).
///
/// Admin-only: the control plane refuses a non-admin caller and removes
/// nothing. Refuses any target whose status is not provisional. Not
/// read-only and not egress. No secret is ever produced — the tool result is
/// a plain, codeless confirmation.
#[must_use]
pub fn delete_provisional_persona_spec() -> ToolSpec {
    ToolSpec::new(
        DELETE_PROVISIONAL_PERSONA,
        "For an admin only: permanently remove a STILL-PROVISIONAL persona — one that was \
         auto-created by a first message but never went through a link ceremony or was ever \
         granted anything. Use it to clean up stray test/demo identities, for example \"delete \
         the U_TESTER persona\" or \"remove that leftover demo account\". Pass the target's \
         user id EXACTLY as it appears in the mention markup in the message (the id inside \
         `<@...>`), never a typed-out name. This refuses outright if the target is not \
         currently provisional (already linked, merged, or an admin/operator) — that persona \
         must go through revoke instead, since removing a real linked persona is a different, \
         higher-stakes action. If the person asking isn't an admin, it returns a refusal \
         rather than removing anyone.",
        json!({
            "type": "object",
            "properties": {
                ARG_TARGET_USER_ID: {
                    "type": "string",
                    "description": "The target's provider-native user id, taken \
                        verbatim from the mention markup (`<@U...>`) in the \
                        message — not a display name or handle."
                }
            },
            "required": [ARG_TARGET_USER_ID],
            "additionalProperties": false
        }),
    )
    .titled("Delete a still-provisional persona (admin)")
}

/// `list_provisional_personas` spec — an admin's read-only report of every
/// persona that is still provisional, for spotting cleanup candidates.
///
/// Admin-only: the control plane refuses a non-admin caller. Read-only —
/// nothing is ever changed by this tool.
#[must_use]
pub fn list_provisional_personas_spec() -> ToolSpec {
    ToolSpec::new(
        LIST_PROVISIONAL_PERSONAS,
        "For an admin only: list personas that are still PROVISIONAL — auto-created by a \
         first message but never linked or granted anything, the usual shape of a stray \
         test/demo identity. Use it when an admin asks what provisional or unlinked test \
         personas exist, e.g. \"what test personas are lying around\" or \"show me the \
         provisional ones\". Takes no argument on a first call; if the result says there are \
         more, call again passing the returned continuation token to see the rest. If the \
         person asking isn't an admin, it returns a refusal instead of a list.",
        json!({
            "type": "object",
            "properties": {
                ARG_PAGE_TOKEN: {
                    "type": "string",
                    "description": "Continuation token from a prior call's result, to \
                        fetch the next page. Omit on the first call."
                }
            },
            "additionalProperties": false
        }),
    )
    .titled("List still-provisional personas (admin)")
    .read_only()
}