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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
//! Specs for the agent-evaluable routine tools: `routine_list` (`#1494`),
//! `routine_create` (`#1497`), and the mutation trio
//! `routine_pause`/`routine_resume`/`routine_delete` (`#1495`).
//!
//! `routine_list` is the read tool every routine lifecycle mutation depends
//! on: it is the ONLY way an agent turn learns what routines the asking
//! person can act on — every routine on the deployment for an admin, and the
//! ones they created for anyone else (`#1872`) — and for each one it mints an
//! observation handle (`polyc_crypto::routine_observation`) that a later
//! `routine_pause`/`resume`/`delete`/`fire` call must present back
//! (INV-RL4) — a hallucinated or guessed routine name never authorizes a
//! mutation.
//!
//! `routine_create` is the tracer bullet that makes the feature real: the
//! model compiles an admin's plain-language request into an explicit
//! schedule + prompt, plus an optional sharing intent — whether other
//! members of this instance may view and duplicate the routine (`scope`,
//! `"public"`/`"private"`, default private) — never a run-as principal, since
//! a routine always runs as its owner by rule (see
//! `polyc_controller::routine_intent`). It always requires human approval
//! ([`ToolSpec::approval_required`]) — creating a routine establishes durable
//! unattended automation (INV-RL2), so every call pauses for the enriched
//! confirmation card regardless of any granted capability.
//!
//! `routine_pause` only ever REDUCES capability, so — unlike
//! `routine_create`/`routine_delete` — it carries no intrinsic approval
//! flag: the kill switch takes effect immediately, no confirmation
//! ceremony. `routine_resume` used to share that posture, but (#1705) an
//! admin-only check is not a substitute for gating on what the call does:
//! resuming RE-ARMS a routine's entire future schedule — establishing
//! capability, not reducing it — so it now mirrors `routine_create`'s
//! approval posture instead. `routine_delete` mirrors it too: deleting a
//! routine is exactly as durable and irreversible a change as creating one,
//! so it always pauses for a human. `routine_fire` (`#1498`) joins them as a
//! sixth verb: an admin's one-shot
//! test fire of an existing routine, gated and confirmed exactly like
//! `routine_delete` — its write is a DISPATCH (the routine's prompt, run as
//! one unattended turn) rather than a CR mutation, reusing the SAME fire
//! path a scheduled tick uses, so a test fire posts exactly what the
//! schedule would.
//!
//! `routine_pause`/`routine_resume`/`routine_delete`/`routine_fire` all take
//! the SAME three fields `routine_list` returned for the target routine
//! (`id`, `observation_handle`, `observation_handle_expires_at`) — relayed
//! back verbatim, never invented — which the control plane re-verifies
//! before touching anything (INV-RL4/RL5).
//!
//! `routine_duplicate` (`#1805`) joins them as a seventh verb: it takes the
//! SAME observation-handle triple, loads the source routine under the
//! handle's guarantees, and mints a copy carrying the caller's OWN
//! provenance and the source's schedule and prompt — never the source's
//! scope, which the copy always starts `private` regardless of it. Nothing
//! about the source transfers: not its scope, not its owner's authority, not
//! its pause state. It carries the SAME intrinsic approval flag
//! `routine_create` does — the new owner approves the literal spec they are
//! adopting before anything durable is written, through the exact same
//! confirmation machinery. Duplicating a private source requires being its
//! owner or an admin; duplicating a public source requires an admin today
//! (member routine access is a later track). An orphaned source — one whose
//! owner has left the instance — is always duplicable: duplication is the
//! only succession path a departed owner's routine has.
//!
//! Like the wallet and history families none of these seven tools has an
//! in-process implementation: the conversation sandbox can't reach the
//! cluster's `Routine` CRs. The harness advertises them via the
//! control-plane proxy; the control plane runs them (every one of them gated
//! on the resolved caller — a caller who is neither an admin nor the
//! routine's owner gets a refusal, never a mutation) and, for
//! `routine_list`, mints the handles trusted-side.

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

/// The `routine_list` tool name.
pub const ROUTINE_LIST: &str = "routine_list";

/// The `routine_create` tool name.
pub const ROUTINE_CREATE: &str = "routine_create";

/// The `routine_pause` tool name (`#1495`).
pub const ROUTINE_PAUSE: &str = "routine_pause";

/// The `routine_resume` tool name (`#1495`).
pub const ROUTINE_RESUME: &str = "routine_resume";

/// The `routine_delete` tool name (`#1495`).
pub const ROUTINE_DELETE: &str = "routine_delete";

/// The `routine_fire` tool name (`#1498`).
pub const ROUTINE_FIRE: &str = "routine_fire";

/// The `routine_duplicate` tool name (`#1805`).
pub const ROUTINE_DUPLICATE: &str = "routine_duplicate";

/// The `routine_set_scope` tool name (`#1806`).
pub const ROUTINE_SET_SCOPE: &str = "routine_set_scope";

/// The `routine_allow_denial` tool name (POLY-32).
pub const ROUTINE_ALLOW_DENIAL: &str = "routine_allow_denial";

/// The `routine_revoke_grant` tool name (POLY-32).
pub const ROUTINE_REVOKE_GRANT: &str = "routine_revoke_grant";

/// The `routine_refire_attended` tool name (POLY-32).
pub const ROUTINE_REFIRE_ATTENDED: &str = "routine_refire_attended";

/// Every routine tool name, for allowlist checks and dispatch.
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,
];

/// The tool argument name carrying the target routine's id — exactly as
/// `routine_list` returned it. Shared by all three mutation verbs so the
/// field name can't drift between them.
pub const ARG_ID: &str = "id";

/// The tool argument name carrying the observation handle `routine_list`
/// minted for this routine (INV-RL4). Shared by all three mutation verbs.
pub const ARG_OBSERVATION_HANDLE: &str = "observation_handle";

/// The tool argument name carrying the observation handle's expiry, exactly
/// as `routine_list` returned it alongside the handle. Shared by all three
/// mutation verbs.
pub const ARG_OBSERVATION_HANDLE_EXPIRES_AT: &str = "observation_handle_expires_at";

/// The tool argument name carrying an optional human-readable pause reason.
pub const ARG_REASON: &str = "reason";

/// The tool argument name carrying `routine_set_scope`'s target sharing
/// value (`#1806`).
pub const ARG_SCOPE: &str = "scope";

/// The tool argument name naming the tool `routine_allow_denial`/
/// `routine_revoke_grant` acts on (POLY-32).
///
/// Omitted or empty on `routine_revoke_grant` means "the routine's blanket
/// grant" rather than one tool's.
pub const ARG_TOOL: &str = "tool";

/// Every routine tool spec.
#[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(),
    ]
}

/// The JSON-Schema `properties` every mutation verb's schema shares: the
/// exact `(id, observation_handle, observation_handle_expires_at)` triple
/// `routine_list` returned for the target routine (INV-RL4). One definition
/// so the three verbs' schemas cannot drift on field name or description.
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."
        }
    })
}

/// The JSON-Schema `required` array every mutation verb's schema shares.
fn observation_handle_required() -> Vec<serde_json::Value> {
    vec![
        json!("id"),
        json!("observation_handle"),
        json!("observation_handle_expires_at"),
    ]
}

/// `routine_create` spec — an admin schedules a new unattended automation by
/// describing it in plain language.
///
/// Admin-only: the control plane refuses a non-admin caller and writes
/// nothing. Not read-only (it schedules a new unattended prompt) and always
/// approval-gated: every call pauses on the enriched confirmation card (the
/// compiled schedule's next fire times plus the exact prompt text) before
/// anything durable is written (INV-RL2).
///
/// The model never supplies a run-as principal — a routine always runs as
/// its owner, by rule — and instead supplies:
/// - `schedule`: `{"cron": {"expression": "...", "timezone": "..."}}` for a
///   repeating cadence, or `{"once": {"at": "..."}}` for a single instant —
///   derived from whether the request names a repeating cadence or a single
///   moment.
/// - `prompt`: the exact text to run when the routine fires.
/// - `scope` (optional): `"public"` if the admin said other members should
///   be able to see and copy this routine, `"private"` (or omitted) if they
///   didn't — a routine is created private by default.
#[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()
}

/// `routine_list` spec — an inspection of the routines the asking person is
/// entitled to see: their own, or every routine on the deployment for an
/// admin (`#1872`).
///
/// Read-only and NOT egress: nothing leaves the sandbox, and it never
/// enumerates the deployment's automation map for someone who isn't an admin —
/// they see the routines they created and nothing else. Each listed routine
/// carries its id, who created it and from where, its compiled schedule's
/// upcoming fire times, its last fire time and whether it fired, and its
/// pause state — plus an observation handle a later
/// pause/resume/delete/manual-fire request must present back.
#[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()
}

/// `routine_pause` spec — an ADMIN-ONLY, IMMEDIATE kill switch for one
/// routine (`#1495`).
///
/// Admin-only, and gated on a valid observation handle from `routine_list`
/// (INV-RL1/RL4). Deliberately carries NO approval flag: pausing only
/// reduces capability, so it takes effect the instant it is called — the
/// kill switch is never argued with (INV-RL2 does not apply to pause; #1705
/// gates its `resume` sibling instead, since resuming RE-ESTABLISHES
/// capability rather than reducing it). Not read-only (it changes
/// `spec.suspend`).
#[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)")
}

/// `routine_resume` spec — the un-pause sibling of [`pause_spec`] (`#1495`).
///
/// Admin-only and observation-handle gated, same as `routine_pause` — but
/// (#1705) NOT ungated: `routine_pause` only ever REDUCES what's running (a
/// kill switch INV-RL2 deliberately exempts), while `routine_resume`
/// RE-ARMS a routine's entire future schedule, the same kind of
/// re-establishing-automation action `routine_create`/`routine_fire`
/// already require approval for. An admin-only check is not a substitute
/// for gating on what the call does — both live #1635 incidents were
/// triggered by the account's own 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)")
}

/// `routine_delete` spec — an ADMIN-ONLY, APPROVAL-GATED permanent removal of
/// one routine (`#1495`).
///
/// Admin-only, and gated on a valid observation handle from `routine_list`
/// (INV-RL1/RL4), like pause/resume — but unlike pause, deleting is exactly
/// as durable and irreversible a change as creating a routine, so this tool
/// carries the SAME intrinsic approval flag `routine_create` (and, since
/// #1705, `routine_resume`) does ([`ToolSpec::approval_required`], INV-RL2):
/// every call pauses for the admin's explicit confirmation before the
/// routine is actually removed.
#[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()
}

/// `routine_fire` spec — an ADMIN-ONLY, APPROVAL-GATED one-shot test fire of
/// an existing routine (`#1498`).
///
/// Admin-only, and gated on a valid observation handle from `routine_list`
/// (INV-RL1/RL4), exactly like the mutation trio — but its write is a
/// DISPATCH, not a CR mutation: on approval it runs the routine's exact
/// prompt as one unattended turn, the SAME way the routine's own schedule
/// would fire it, so an admin can confirm a routine actually posts where and
/// what they expect before trusting the schedule. Carries the SAME
/// intrinsic approval flag `routine_create`/`routine_delete` do
/// ([`ToolSpec::approval_required`]): firing a routine is exactly as
/// consequential as its schedule doing so on its own, so every call pauses
/// for the admin's explicit confirmation first.
#[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()
}

/// `routine_duplicate` spec — mint a caller-owned copy of an existing
/// routine's schedule and prompt (`#1805`).
///
/// Gated on a valid observation handle from `routine_list` (INV-RL4/RL5),
/// exactly like the mutation trio and test-fire. The copy always carries the
/// CALLER's own provenance and starts private, regardless of the source's
/// scope or owner — duplication never transfers the source's authority,
/// sharing setting, or pause state. Duplicating a private source requires
/// being its owner or an admin; a public source requires an admin today
/// (member routine access lands in a later track). An orphaned source — its
/// owner having left the instance — stays duplicable: duplication is that
/// routine's only path back to a live owner.
///
/// Carries the SAME intrinsic approval flag `routine_create` does
/// ([`ToolSpec::approval_required`]): every call pauses for the new owner's
/// confirmation, showing the exact schedule and prompt they are adopting,
/// before anything is created.
#[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()
}

/// `routine_duplicate`'s own schema `properties`: the shared observation-handle
/// triple plus its own `approval_mode` choice — the duplicating member sets
/// their own grant mode for the copy they're creating; it is never inherited
/// from the source routine.
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
}

/// The `approval_mode` schema property shared by `routine_create`'s
/// top-level schema and [`duplicate_properties`]. `subject` names what the
/// mode governs ("the routine" / "the copy"); `note` is an optional clause
/// appended right after "up front" (empty for `routine_create`).
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."
        )
    })
}

/// `routine_set_scope` spec — flips a routine's sharing between `public` and
/// `private` after creation (`#1806`).
///
/// Gated on a valid observation handle like the mutation trio, but its
/// authorization check is narrower: only the routine's owner may call it,
/// admin status never substitutes (mirrors `routine_fire`'s INV-RL11
/// posture) — whether to open a routine's definition to other members is the
/// owner's decision, not an administrative one. Not read-only (it writes
/// `spec.scope`) and always approval-gated, the same durable-change posture
/// as `routine_create`/`routine_delete`/`routine_fire`: nothing shares or
/// hides a routine's definition until the owner explicitly confirms 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()
}

/// `routine_allow_denial` spec — mint a standing grant for a tool a
/// routine's own fire dispatch just denied fail-closed (POLY-32).
///
/// Owner-only, exactly like `routine_fire` (INV-RL11 class — admin status
/// never substitutes): a grant this mints authorizes what runs under the
/// owner's own name on every FUTURE unattended fire, so admin status alone
/// is never enough. Capability-EXPANDING, like `routine_resume` (`#1705`),
/// so it always pauses for the owner's confirmation first.
#[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()
}

/// `routine_revoke_grant` spec — end one standing tool grant, or the
/// routine's blanket grant, immediately (POLY-32).
///
/// Owner-only, same authorization class as `routine_allow_denial`. Unlike
/// it, this only ever REDUCES what the routine may run unattended — the
/// same kill-switch posture `routine_pause` has (`#1495`) — so it carries no
/// approval gate and takes effect the instant it is called.
#[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)")
}

/// `routine_refire_attended` spec — the setup rehearsal's occurrence-less
/// dispatch, exposed as a repeatable "run now, attended" operation (POLY-32).
///
/// Owner-only and approval-gated, exactly like `routine_fire` — this
/// dispatches a real turn under the owner's authority, so it carries the
/// SAME confirmation posture. Unlike `routine_fire`, it never claims a
/// scheduled occurrence and never mints a `routine_fired` marker: any gated
/// call it hits pauses for the owner to resolve, exactly like the setup
/// rehearsal, instead of denying fail-closed.
#[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);
    }

    /// #1497 INV-RL2: `routine_create` always pauses for a human, regardless
    /// of any capability granted — it is not read-only, and it carries the
    /// intrinsic approval flag rather than relying solely on capability
    /// escalation.
    #[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
        );
    }

    /// #1802: `scope` is an optional sharing intent, never a run-as
    /// principal — the model may name `"public"`/`"private"`, but must never
    /// be required to.
    #[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
        );
    }

    /// `approval_mode` is an optional, three-valued grant-mode choice on
    /// `routine_create` — never required, so `individual` stays reachable by
    /// omitting it.
    #[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
        );
    }

    /// `routine_duplicate` carries the same optional `approval_mode` choice
    /// — the duplicating member's own, independent of the source.
    #[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
        );
    }

    /// Every mutation verb's schema requires the exact three observation-handle
    /// fields — the shared property/required definitions can't drift per verb.
    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
            );
        }
    }

    /// #1495 INV-RL2: `routine_pause` is deliberately UNGATED — the kill
    /// switch takes effect immediately, never argued with. (`routine_resume`
    /// used to share this posture; #1705 gates it instead — see
    /// `resume_spec_is_gated_by_intent_not_just_caller_identity` below.)
    #[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);
    }

    /// `routine_resume` is mutating and admin-scoped, same as `routine_pause`
    /// — but (#1705) NOT ungated; see the dedicated gating test below.
    #[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);
    }

    /// #1705: an admin-only check is not a substitute for gating on what the
    /// call does — both live #1635 incidents were triggered by the account's
    /// own admin. Unlike `routine_pause` (which only ever REDUCES what's
    /// running), `routine_resume` re-arms a routine's entire future schedule
    /// — the same kind of re-establishing-automation action `routine_create`
    /// and `routine_fire` already require approval for.
    #[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"
        );
    }

    /// `routine_pause` additionally accepts an optional `reason`.
    #[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
        );
    }

    /// #1495 INV-RL2: `routine_delete` always pauses for a human, mirroring
    /// `routine_create` — deleting is exactly as durable a change.
    #[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);
    }

    /// #1498 INV-RL2: `routine_fire` always pauses for a human, mirroring
    /// `routine_create`/`routine_delete` — a test fire is exactly as
    /// consequential as the schedule itself firing.
    #[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);
    }

    /// #1805 INV-RL2: `routine_duplicate` always pauses for a human, mirroring
    /// `routine_create` — a duplicate is a brand-new durable routine, exactly
    /// as consequential to create as the original was.
    #[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);
    }

    /// `#1806`: `routine_set_scope` always pauses for a human, requires the
    /// observation-handle triple like every other mutation verb, and its own
    /// description names it as owner-only rather than admin-only — the ONE
    /// verb in the family that reads that way.
    #[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);
    }

    /// #1805: `routine_duplicate` takes the SAME observation-handle triple as
    /// the mutation verbs — no bespoke argument shape.
    #[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
        );
    }

    /// `#1806`: `scope` is a required enum, `"public"` or `"private"` —
    /// unlike `routine_create`'s optional sharing hint, the flip verb always
    /// names an explicit target.
    #[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
        );
    }
}