use polyc_llm::ToolSpec;
use serde_json::json;
pub const ROUTINE_LIST: &str = "routine_list";
pub const ROUTINE_CREATE: &str = "routine_create";
pub const ROUTINE_PAUSE: &str = "routine_pause";
pub const ROUTINE_RESUME: &str = "routine_resume";
pub const ROUTINE_DELETE: &str = "routine_delete";
pub const ROUTINE_FIRE: &str = "routine_fire";
pub const ROUTINE_DUPLICATE: &str = "routine_duplicate";
pub const ROUTINE_SET_SCOPE: &str = "routine_set_scope";
pub const ROUTINE_ALLOW_DENIAL: &str = "routine_allow_denial";
pub const ROUTINE_REVOKE_GRANT: &str = "routine_revoke_grant";
pub const ROUTINE_REFIRE_ATTENDED: &str = "routine_refire_attended";
pub const ALL: &[&str] = &[
ROUTINE_CREATE,
ROUTINE_LIST,
ROUTINE_PAUSE,
ROUTINE_RESUME,
ROUTINE_DELETE,
ROUTINE_FIRE,
ROUTINE_DUPLICATE,
ROUTINE_SET_SCOPE,
ROUTINE_ALLOW_DENIAL,
ROUTINE_REVOKE_GRANT,
ROUTINE_REFIRE_ATTENDED,
];
pub const ARG_ID: &str = "id";
pub const ARG_OBSERVATION_HANDLE: &str = "observation_handle";
pub const ARG_OBSERVATION_HANDLE_EXPIRES_AT: &str = "observation_handle_expires_at";
pub const ARG_REASON: &str = "reason";
pub const ARG_SCOPE: &str = "scope";
pub const ARG_TOOL: &str = "tool";
#[must_use]
pub fn all_specs() -> Vec<ToolSpec> {
vec![
create_spec(),
list_spec(),
pause_spec(),
resume_spec(),
delete_spec(),
fire_spec(),
duplicate_spec(),
set_scope_spec(),
allow_denial_spec(),
revoke_grant_spec(),
refire_attended_spec(),
]
}
fn observation_handle_properties() -> serde_json::Value {
json!({
"id": {
"type": "string",
"description": "The routine's id, exactly as `routine_list` returned it for this \
routine — never invented or guessed."
},
"observation_handle": {
"type": "string",
"description": "The `observation_handle` value `routine_list` returned for this \
routine — relay it exactly as given."
},
"observation_handle_expires_at": {
"type": "string",
"description": "The `observation_handle_expires_at` value `routine_list` returned \
alongside the handle — relay it exactly as given."
}
})
}
fn observation_handle_required() -> Vec<serde_json::Value> {
vec![
json!("id"),
json!("observation_handle"),
json!("observation_handle_expires_at"),
]
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn create_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_CREATE,
"For an admin only: create a new routine (a scheduled, unattended automation) from a \
plain-language description. Use it when an admin asks to schedule something recurring \
or one-off — for example \"remind the channel every weekday at 9am to post standup\" or \
\"send this reminder once tomorrow at 3pm\". Work out the schedule yourself: a repeating \
request becomes a cron expression (with an IANA time zone when the admin named one), a \
single-instant request becomes a one-shot RFC3339 instant — never ask the admin to name \
a schedule \"kind\" or type. The prompt is the exact text the routine runs when it \
fires; write it as a complete, standalone instruction, since the routine has no memory \
of this conversation when it later runs. A routine is created visible only to the admin \
who made it unless they say other members should be able to see and copy it — only set \
`scope` to \"public\" when the admin actually said something like that; otherwise omit \
it. Creating a routine always pauses for the admin's confirmation, which shows the exact \
schedule and prompt before anything is created — nothing is scheduled until they \
approve. If the person asking isn't an admin, it returns a refusal and creates nothing.",
json!({
"type": "object",
"properties": {
"scope": {
"type": "string",
"enum": ["public", "private"],
"description": "Whether other members of this instance may view and \
duplicate this routine's definition. Omit for the default, \"private\" \
— set to \"public\" only when the admin said other members should be \
able to see and copy it."
},
"schedule": {
"oneOf": [
{
"type": "object",
"properties": {
"cron": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A standard five-field cron \
expression (minute hour day-of-month month \
day-of-week)."
},
"timezone": {
"type": "string",
"description": "IANA time zone name (e.g. \
\"America/New_York\"); omit for UTC."
}
},
"required": ["expression"],
"additionalProperties": false
}
},
"required": ["cron"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"once": {
"type": "object",
"properties": {
"at": {
"type": "string",
"description": "The RFC3339 instant this routine \
fires at."
}
},
"required": ["at"],
"additionalProperties": false
}
},
"required": ["once"],
"additionalProperties": false
}
],
"description": "The compiled schedule: a repeating `cron` cadence or a \
single `once` instant."
},
"prompt": {
"type": "string",
"description": "The exact, standalone text the routine runs when it fires."
},
"approval_mode": approval_mode_property("the routine", "")
},
"required": ["schedule", "prompt"],
"additionalProperties": false
}),
)
.titled("Create a routine (admin)")
.approval_required()
}
#[must_use]
pub fn list_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_LIST,
"List routines (scheduled, unattended automations) the person asking can see: the \
ones they created, or every routine this deployment has published if they're an \
admin. Use it when someone asks to see, list, or check on their routines, \
scheduled tasks, or automations. For each routine, shows its id, who created it \
and from which conversation, its schedule's upcoming fire times, when it last \
fired and whether that fire actually ran, and whether it's paused. This is the \
ONLY way to learn a routine's exact id — never guess or invent one; a later \
request to pause, resume, delete, or manually fire a routine only works against \
one you listed with this tool first. Someone who isn't an admin and created no \
routine gets a refusal rather than any routine's details, and never sees a \
routine created by anyone else. Takes no arguments.",
json!({
"type": "object",
"properties": {},
"additionalProperties": false
}),
)
.titled("List the routines you can see")
.read_only()
.cacheable_approval()
}
#[must_use]
pub fn pause_spec() -> ToolSpec {
let mut properties = observation_handle_properties();
properties["reason"] = json!({
"type": "string",
"description": "Why the routine is being paused, if the admin gave one. Omit if they \
didn't say."
});
ToolSpec::new(
ROUTINE_PAUSE,
"For an admin only: immediately pause a routine (scheduled, unattended automation) so \
it stops firing. Takes effect right away, with no confirmation — pausing only reduces \
what already runs, never adds anything. Use it when an admin asks to pause, stop, or \
hold off a routine or scheduled task. Requires the exact `id`, `observation_handle`, \
and `observation_handle_expires_at` a prior `routine_list` call returned for this \
routine — call `routine_list` first if you don't already have them from this \
conversation. If the person asking isn't an admin, or the handle is missing, expired, \
or for a different routine, it returns a refusal and pauses nothing.",
json!({
"type": "object",
"properties": properties,
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Pause a routine (admin)")
}
#[must_use]
pub fn resume_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_RESUME,
"For an admin only: resume a paused routine (scheduled, unattended automation) so it \
starts firing again on its schedule. Always pauses for the admin's confirmation \
first — nothing resumes until they approve. Resuming never replays ticks that were \
missed while paused — the next fire is the next one on the schedule from now. Use it \
when an admin asks to resume, restart, or turn a paused routine back on. Requires the \
exact `id`, `observation_handle`, and `observation_handle_expires_at` a prior \
`routine_list` call returned for this routine — call `routine_list` first if you \
don't already have them from this conversation. If the person asking isn't an admin, \
or the handle is missing, expired, or for a different routine, it returns a refusal \
and resumes nothing.",
json!({
"type": "object",
"properties": observation_handle_properties(),
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.destructive()
.approval_required()
.titled("Resume a routine (admin)")
}
#[must_use]
pub fn delete_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_DELETE,
"For an admin only: permanently delete a routine (scheduled, unattended automation). \
Always pauses for the admin's confirmation first — nothing is deleted until they \
approve, and once approved it cannot be undone. Use it when an admin asks to delete, \
remove, or cancel a routine or scheduled task for good (for pausing it instead, use \
`routine_pause`). Requires the exact `id`, `observation_handle`, and \
`observation_handle_expires_at` a prior `routine_list` call returned for this routine — \
call `routine_list` first if you don't already have them from this conversation. If the \
person asking isn't an admin, or the handle is missing, expired, or for a different \
routine, it returns a refusal and deletes nothing.",
json!({
"type": "object",
"properties": observation_handle_properties(),
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Delete a routine (admin)")
.approval_required()
}
#[must_use]
pub fn fire_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_FIRE,
"For an admin only: test-fire a routine (scheduled, unattended automation) right now, \
exactly as its own schedule would fire it. Always pauses for the admin's confirmation \
first — nothing runs until they approve. Use it when an admin wants to check that a \
routine actually posts where and what they expect, without waiting for its schedule. \
Firing the same routine twice within the same minute is a no-op the second time — it \
never runs the routine's prompt twice for one occurrence. Requires the exact `id`, \
`observation_handle`, and `observation_handle_expires_at` a prior `routine_list` call \
returned for this routine — call `routine_list` first if you don't already have them \
from this conversation. If the person asking isn't an admin, or the handle is missing, \
expired, or for a different routine, it returns a refusal and fires nothing.",
json!({
"type": "object",
"properties": observation_handle_properties(),
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Test-fire a routine (admin)")
.approval_required()
}
#[must_use]
pub fn duplicate_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_DUPLICATE,
"Make your own copy of an existing routine (scheduled, unattended automation), so it \
runs on your authority instead of whoever set it up. Use it when someone asks to copy, \
duplicate, or take over a routine — for example a public routine another member \
published, or one whose creator has left. The copy keeps the source routine's schedule \
and prompt exactly, but always starts visible only to you, however the source was \
shared. The source itself is never changed. Always pauses for confirmation first, \
showing the exact schedule and prompt the copy will run — nothing is created until \
it's approved. Requires the exact `id`, `observation_handle`, and \
`observation_handle_expires_at` a prior `routine_list` call returned for the source \
routine — call `routine_list` first if you don't already have them from this \
conversation. A private source can only be copied by whoever created it or an admin; a \
public source needs an admin today. If the handle is missing, expired, or for a \
different routine, or the caller isn't allowed to copy this one, it returns a refusal \
and creates nothing.",
json!({
"type": "object",
"properties": duplicate_properties(),
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Duplicate a routine")
.approval_required()
}
fn duplicate_properties() -> serde_json::Value {
let mut props = observation_handle_properties();
props["approval_mode"] = approval_mode_property(
"the copy",
", for the copy you're creating now — independent of whatever mode the source routine \
runs under",
);
props
}
fn approval_mode_property(subject: &str, note: &str) -> serde_json::Value {
json!({
"type": "string",
"enum": ["individual", "auto", "approve-all-dangerous"],
"description": format!(
"How much of {subject}'s future unattended tool use you authorize up front{note}. \
\"individual\" (the default; omit to choose it) — you approve each tool the first \
time {subject} wants to use it. \"auto\" — you pre-approve {subject}'s ordinary \
tools; a high-risk tool still pauses. \"approve-all-dangerous\" — you pre-approve \
every tool, including high-risk ones; only set this when the person asking \
explicitly said they want it to run without ever asking. Only set to \"auto\" or \
\"approve-all-dangerous\" when they actually said something like that; otherwise \
omit it."
)
})
}
#[must_use]
pub fn set_scope_spec() -> ToolSpec {
let mut properties = observation_handle_properties();
properties["scope"] = json!({
"type": "string",
"enum": ["public", "private"],
"description": "The sharing value to set: \"public\" so other members of this \
instance may view and duplicate this routine's definition, or \"private\" so \
only its owner can."
});
let mut required = observation_handle_required();
required.push(json!("scope"));
ToolSpec::new(
ROUTINE_SET_SCOPE,
"For the person who created a routine (scheduled, unattended automation) only: \
change whether other members of this instance may view and copy its definition. \
Set `scope` to \"public\" to let other members see and duplicate it, or \"private\" \
to keep it visible only to its creator. Always pauses for the creator's confirmation \
first — nothing changes until they approve. Use it when the routine's creator asks \
to share, publish, unshare, or make a routine private again. Requires the exact `id`, \
`observation_handle`, and `observation_handle_expires_at` a prior `routine_list` call \
returned for this routine — call `routine_list` first if you don't already have them \
from this conversation. If the person asking isn't the routine's creator — including \
an admin who didn't create it — it returns a refusal and changes nothing.",
json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
}),
)
.titled("Change who can see a routine (owner)")
.approval_required()
}
#[must_use]
pub fn allow_denial_spec() -> ToolSpec {
let mut properties = observation_handle_properties();
properties["tool"] = json!({
"type": "string",
"description": "The tool name the routine's last fire denied — exactly as it appeared \
in that denial. Never invented or guessed."
});
let mut required = observation_handle_required();
required.push(json!("tool"));
ToolSpec::new(
ROUTINE_ALLOW_DENIAL,
"For the routine's owner only: allow a tool that this routine's own unattended fire \
just refused to use, so its NEXT fire can use it. Use it right after a fire aborted \
because it needed a tool the owner hadn't allowed yet. Always pauses for the owner's \
confirmation first — nothing is allowed until they approve. Requires the exact `id`, \
`observation_handle`, and `observation_handle_expires_at` a prior `routine_list` call \
returned for this routine, plus the `tool` name the denial named. If the caller isn't \
this routine's owner, or the tool was never denied, it returns a refusal and allows \
nothing.",
json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false
}),
)
.titled("Allow a denied tool (owner)")
.approval_required()
}
#[must_use]
pub fn revoke_grant_spec() -> ToolSpec {
let mut properties = observation_handle_properties();
properties["tool"] = json!({
"type": "string",
"description": "The tool whose standing grant to end. Omit this to end the routine's \
blanket grant instead (auto/approve-all-dangerous mode), which drops it back to \
individual, per-tool approval."
});
ToolSpec::new(
ROUTINE_REVOKE_GRANT,
"For the routine's owner only: immediately end a standing tool grant this routine's \
unattended fires have been using, or (when no `tool` is given) end the routine's \
blanket grant and drop it back to individual, per-tool approval. Takes effect right \
away, with no confirmation — revoking only reduces what already runs. The routine's \
NEXT fire re-checks the tool (or every tool, for a blanket revoke) as if it had never \
been allowed. Requires the exact `id`, `observation_handle`, and \
`observation_handle_expires_at` a prior `routine_list` call returned for this routine. \
If the caller isn't this routine's owner, it returns a refusal and revokes nothing.",
json!({
"type": "object",
"properties": properties,
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Revoke a routine grant (owner)")
}
#[must_use]
pub fn refire_attended_spec() -> ToolSpec {
ToolSpec::new(
ROUTINE_REFIRE_ATTENDED,
"For the routine's owner only: run a routine (scheduled, unattended automation) right \
now WITH YOU WATCHING, so any tool it needs but hasn't been allowed yet pauses for your \
approval instead of the fire aborting. Use it to watch a routine run interactively, or \
to close approval gaps one at a time. Always pauses for the owner's confirmation \
first — nothing runs until they approve. Unlike `routine_fire`, this never counts as a \
scheduled tick: it can be run as many times as needed. Requires the exact `id`, \
`observation_handle`, and `observation_handle_expires_at` a prior `routine_list` call \
returned for this routine. If the caller isn't this routine's owner, it returns a \
refusal and runs nothing.",
json!({
"type": "object",
"properties": observation_handle_properties(),
"required": observation_handle_required(),
"additionalProperties": false
}),
)
.titled("Run a routine, attended (owner)")
.approval_required()
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
#[test]
fn list_spec_is_read_only_and_owner_scoped_in_its_own_description() {
let spec = list_spec();
assert_eq!(spec.name, ROUTINE_LIST);
assert!(spec.read_only, "routine_list must be read-only");
assert!(!spec.destructive);
assert!(!spec.open_world);
assert!(
spec.description.contains("the ones they created"),
"the spec tells the model a non-admin still sees their own routines: {}",
spec.description
);
assert!(
spec.description
.contains("never sees a routine created by anyone else"),
"the spec tells the model the listing is owner-scoped for a non-admin: {}",
spec.description
);
let title = spec.title.clone().expect("routine_list carries a title");
assert!(
!title.contains("admin"),
"#1872: the title no longer claims the tool is admin-only: {title}"
);
}
#[test]
fn all_specs_spans_exactly_all() {
let specs = all_specs();
let names: Vec<&str> = specs.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, ALL);
}
#[test]
fn create_spec_is_admin_scoped_mutating_and_always_approval_gated() {
let spec = create_spec();
assert_eq!(spec.name, ROUTINE_CREATE);
assert!(!spec.read_only, "routine_create writes a new CR");
assert!(
spec.needs_approval,
"routine_create must always require human approval"
);
assert!(
spec.description.contains("admin only"),
"the spec tells the model this is admin-gated: {}",
spec.description
);
assert!(
spec.schema_json["required"]
.as_array()
.is_some_and(
|r| r.iter().any(|v| v == "schedule") && r.iter().any(|v| v == "prompt")
),
"schedule and prompt are both required: {}",
spec.schema_json
);
}
#[test]
fn create_spec_carries_an_optional_public_private_scope() {
let spec = create_spec();
assert_eq!(
spec.schema_json["properties"]["scope"]["enum"],
json!(["public", "private"])
);
assert!(
!spec.schema_json["required"]
.as_array()
.unwrap()
.iter()
.any(|v| v == "scope"),
"scope must stay optional: {}",
spec.schema_json
);
}
#[test]
fn create_spec_carries_an_optional_approval_mode() {
let spec = create_spec();
assert_eq!(
spec.schema_json["properties"]["approval_mode"]["enum"],
json!(["individual", "auto", "approve-all-dangerous"])
);
assert!(
!spec.schema_json["required"]
.as_array()
.unwrap()
.iter()
.any(|v| v == "approval_mode"),
"approval_mode must stay optional: {}",
spec.schema_json
);
}
#[test]
fn duplicate_spec_carries_an_optional_approval_mode() {
let spec = duplicate_spec();
assert_eq!(
spec.schema_json["properties"]["approval_mode"]["enum"],
json!(["individual", "auto", "approve-all-dangerous"])
);
assert!(
!spec.schema_json["required"]
.as_array()
.unwrap()
.iter()
.any(|v| v == "approval_mode"),
"approval_mode must stay optional: {}",
spec.schema_json
);
}
fn assert_requires_observation_handle_triple(spec: &ToolSpec) {
let required = spec.schema_json["required"]
.as_array()
.unwrap_or_else(|| panic!("{} has no required array: {}", spec.name, spec.schema_json));
for field in [
ARG_ID,
ARG_OBSERVATION_HANDLE,
ARG_OBSERVATION_HANDLE_EXPIRES_AT,
] {
assert!(
required.iter().any(|v| v == field),
"{} must require {field}: {}",
spec.name,
spec.schema_json
);
}
}
#[test]
fn pause_spec_is_mutating_admin_scoped_and_never_approval_gated() {
let spec = pause_spec();
assert!(!spec.read_only, "{} writes spec.suspend", spec.name);
assert!(
!spec.needs_approval,
"{} must never require approval — it's a kill switch (INV-RL2 does not apply)",
spec.name
);
assert!(
spec.description.contains("admin only"),
"{} tells the model this is admin-gated: {}",
spec.name,
spec.description
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn resume_spec_is_mutating_and_admin_scoped() {
let spec = resume_spec();
assert!(!spec.read_only, "{} writes spec.suspend", spec.name);
assert!(
spec.description.contains("admin only"),
"{} tells the model this is admin-gated: {}",
spec.name,
spec.description
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn resume_spec_is_gated_by_intent_not_just_caller_identity() {
let spec = resume_spec();
assert!(
spec.destructive,
"re-arms unattended automation for every future scheduled fire"
);
assert!(
spec.needs_approval,
"resuming must always pause for a human check, regardless of the caller's \
admin status — resume RE-ESTABLISHES capability, unlike pause"
);
}
#[test]
fn pause_spec_accepts_an_optional_reason() {
let spec = pause_spec();
assert!(spec.schema_json["properties"]["reason"].is_object());
assert!(
!spec.schema_json["required"]
.as_array()
.unwrap()
.iter()
.any(|v| v == "reason"),
"reason must stay optional: {}",
spec.schema_json
);
}
#[test]
fn delete_spec_is_admin_scoped_mutating_and_always_approval_gated() {
let spec = delete_spec();
assert_eq!(spec.name, ROUTINE_DELETE);
assert!(!spec.read_only, "routine_delete removes a CR");
assert!(
spec.needs_approval,
"routine_delete must always require human approval"
);
assert!(
spec.description.contains("admin only"),
"the spec tells the model this is admin-gated: {}",
spec.description
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn fire_spec_is_admin_scoped_mutating_and_always_approval_gated() {
let spec = fire_spec();
assert_eq!(spec.name, ROUTINE_FIRE);
assert!(!spec.read_only, "routine_fire dispatches a real turn");
assert!(
spec.needs_approval,
"routine_fire must always require human approval"
);
assert!(
spec.description.contains("admin only"),
"the spec tells the model this is admin-gated: {}",
spec.description
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn duplicate_spec_is_mutating_and_always_approval_gated() {
let spec = duplicate_spec();
assert_eq!(spec.name, ROUTINE_DUPLICATE);
assert!(!spec.read_only, "routine_duplicate creates a new CR");
assert!(
spec.needs_approval,
"routine_duplicate must always require human approval"
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn set_scope_spec_is_owner_scoped_mutating_and_always_approval_gated() {
let spec = set_scope_spec();
assert_eq!(spec.name, ROUTINE_SET_SCOPE);
assert!(!spec.read_only, "routine_set_scope writes spec.scope");
assert!(
spec.needs_approval,
"routine_set_scope must always require human confirmation"
);
assert!(
spec.description.contains("creator"),
"the spec tells the model this is owner-gated, not admin-gated: {}",
spec.description
);
assert_requires_observation_handle_triple(&spec);
}
#[test]
fn duplicate_spec_takes_no_arguments_beyond_the_observation_handle_triple_and_approval_mode() {
let spec = duplicate_spec();
let props = spec.schema_json["properties"]
.as_object()
.expect("object schema");
assert_eq!(
props
.keys()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>(),
[
ARG_ID,
ARG_OBSERVATION_HANDLE,
ARG_OBSERVATION_HANDLE_EXPIRES_AT,
"approval_mode",
]
.into_iter()
.collect::<std::collections::BTreeSet<_>>(),
"{}",
spec.schema_json
);
}
#[test]
fn set_scope_spec_requires_a_public_or_private_scope() {
let spec = set_scope_spec();
assert_eq!(
spec.schema_json["properties"]["scope"]["enum"],
json!(["public", "private"])
);
assert!(
spec.schema_json["required"]
.as_array()
.unwrap()
.iter()
.any(|v| v == "scope"),
"scope must be required: {}",
spec.schema_json
);
}
}