apsimo-hostworker 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,754 @@
1
+ """Single source of truth for the governed Colony tool catalog.
2
+
3
+ Each governed tool carries exactly five things: its wire name, its model-visible
4
+ argument schema, its strict argument validator, its owner-facing approval-display
5
+ metadata, and whether a standing bounded grant may ever authorize it
6
+ (``non_grantable``).
7
+
8
+ ``colony_autonomy_enable`` and ``colony_autonomy_disable`` are marked
9
+ ``non_grantable`` by owner decision: autonomy posture must always be a
10
+ per-message owner approval and can never ride on a standing grant, however the
11
+ grant was issued. The gate core (:mod:`apsimo_hostworker.gate`) fails closed
12
+ on the grant path for any non-grantable — or unknown — tool regardless of host
13
+ configuration.
14
+
15
+ The validators here are behavior-identical ports of the two existing
16
+ independent validators (ColonyAI's endpoint ``_validate_args`` and the private
17
+ worker's intent ``_validate_args``); the repo-internal agreement test pins
18
+ that equivalence against ColonyAI's endpoint, which deliberately keeps its own
19
+ copy (see the design rule in :mod:`apsimo_hostworker.contract`).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import copy
25
+ import json
26
+ from dataclasses import dataclass
27
+ from typing import Any, Callable, Mapping
28
+
29
+ from .contract import (
30
+ BOUNDED_JSON_INTEGER_MAX,
31
+ BOUNDED_JSON_KEY_MAX_CHARS,
32
+ BOUNDED_JSON_MAX_DEPTH,
33
+ BOUNDED_JSON_MAX_NODES,
34
+ BOUNDED_JSON_STRING_MAX_CHARS,
35
+ GovernedContractError,
36
+ IDENTIFIER_MAX_CHARS,
37
+ IDENTIFIER_RE,
38
+ RESEARCH_TOPIC_MAX_CHARS,
39
+ bounded_integer,
40
+ bounded_json_value,
41
+ bounded_number,
42
+ bounded_text,
43
+ enum_text,
44
+ exact_mapping,
45
+ )
46
+
47
+
48
+ COMMITMENT_PRIORITY_DEFAULT = 60
49
+ COMMITMENT_DESCRIPTION_MAX_CHARS = 8000
50
+ COMMITMENT_DUE_AT_MAX_CHARS = 256
51
+ INSIGHT_CONTENT_MAX_CHARS = 16000
52
+ FREEFORM_REASON_MAX_CHARS = 8000
53
+
54
+ # ``bounded_text`` rejects NUL and lone UTF-16 surrogates for every text field,
55
+ # and rejects blank text unless ``allow_empty=True``. These zero-width,
56
+ # ECMAScript-compatible patterns project the same rules into JSON Schema while
57
+ # still permitting ordinary newlines. The explicit class is Python's
58
+ # ``str.strip`` whitespace set; ECMAScript's shorter ``\s`` set differs.
59
+ _MODEL_TEXT_PATTERN = r"^(?![\s\S]*[\u0000\uD800-\uDFFF])"
60
+ _MODEL_NONBLANK_TEXT_PATTERN = (
61
+ _MODEL_TEXT_PATTERN
62
+ + r"(?=[\s\S]*[^\u0009-\u000D\u001C-\u0020\u0085\u00A0"
63
+ r"\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000])"
64
+ )
65
+
66
+
67
+ class ToolCatalogError(GovernedContractError):
68
+ """The tool is not part of the governed catalog."""
69
+
70
+
71
+ def _preview(value: Any, maximum: int = 300) -> str:
72
+ text = " ".join(str(value).split())
73
+ if len(text) > maximum:
74
+ text = text[: maximum - 1] + "…"
75
+ return json.dumps(text, ensure_ascii=True)
76
+
77
+
78
+ def _parameters(
79
+ properties: Mapping[str, Any],
80
+ required=(),
81
+ *,
82
+ definitions: Mapping[str, Any] | None = None,
83
+ ) -> dict[str, Any]:
84
+ schema = {
85
+ "type": "object",
86
+ "properties": dict(properties),
87
+ "required": list(required),
88
+ "additionalProperties": False,
89
+ }
90
+ if definitions:
91
+ schema["$defs"] = dict(definitions)
92
+ return schema
93
+
94
+
95
+ def _text_model_schema(
96
+ maximum: int, *, allow_empty: bool = False,
97
+ ) -> dict[str, Any]:
98
+ schema = {
99
+ "type": "string",
100
+ "maxLength": maximum,
101
+ "pattern": (
102
+ _MODEL_TEXT_PATTERN if allow_empty else _MODEL_NONBLANK_TEXT_PATTERN
103
+ ),
104
+ }
105
+ if not allow_empty:
106
+ schema["minLength"] = 1
107
+ return schema
108
+
109
+
110
+ def identifier_model_schema() -> dict[str, Any]:
111
+ """Return the exact model-visible governed identifier grammar."""
112
+
113
+ return {
114
+ "type": "string",
115
+ "description": (
116
+ "Canonical identifier: starts with an ASCII letter or digit; "
117
+ "remaining characters are ASCII letters, digits, '.', '_', ':', "
118
+ "'/', or '-'."
119
+ ),
120
+ "maxLength": IDENTIFIER_MAX_CHARS,
121
+ "pattern": IDENTIFIER_RE.pattern,
122
+ }
123
+
124
+
125
+ def _details_definitions() -> dict[str, Any]:
126
+ """Build a finite JSON Schema projection of the bounded details tree.
127
+
128
+ Standard JSON Schema has no aggregate descendant-node counter. The exact
129
+ whole-tree budget is therefore stated in the portable field description
130
+ and enforced by both strict runtime validators. Per-container limits here
131
+ enforce every locally possible overflow without rejecting valid branched
132
+ values merely to approximate that aggregate budget.
133
+ """
134
+
135
+ definitions: dict[str, Any] = {}
136
+ key_schema = {
137
+ "type": "string",
138
+ "maxLength": BOUNDED_JSON_KEY_MAX_CHARS,
139
+ "pattern": IDENTIFIER_RE.pattern,
140
+ }
141
+ scalar = [
142
+ {"type": "null"},
143
+ {"type": "boolean"},
144
+ # JSON Schema's mathematical number model cannot distinguish a Python
145
+ # integer from an integral float. Bounding the numeric branch is the
146
+ # portable safe subset: every advertised number passes both validators.
147
+ {
148
+ "type": "number",
149
+ "minimum": -BOUNDED_JSON_INTEGER_MAX,
150
+ "maximum": BOUNDED_JSON_INTEGER_MAX,
151
+ },
152
+ _text_model_schema(
153
+ BOUNDED_JSON_STRING_MAX_CHARS, allow_empty=True,
154
+ ),
155
+ ]
156
+ for depth in range(BOUNDED_JSON_MAX_DEPTH, 0, -1):
157
+ if depth == BOUNDED_JSON_MAX_DEPTH:
158
+ containers = [
159
+ {"type": "array", "maxItems": 0},
160
+ {
161
+ "type": "object",
162
+ "propertyNames": key_schema,
163
+ "maxProperties": 0,
164
+ "additionalProperties": False,
165
+ },
166
+ ]
167
+ else:
168
+ child = {"$ref": f"#/$defs/detailsValue{depth + 1}"}
169
+ # The root and this container's ancestors have already consumed
170
+ # ``depth + 1`` nodes. This is the largest local collection that
171
+ # can still fit the runtime's whole-tree budget.
172
+ container_maximum = BOUNDED_JSON_MAX_NODES - depth - 1
173
+ containers = [
174
+ {
175
+ "type": "array",
176
+ "maxItems": container_maximum,
177
+ "items": child,
178
+ },
179
+ {
180
+ "type": "object",
181
+ "propertyNames": key_schema,
182
+ "maxProperties": container_maximum,
183
+ "additionalProperties": child,
184
+ },
185
+ ]
186
+ definitions[f"detailsValue{depth}"] = {"anyOf": scalar + containers}
187
+ return definitions
188
+
189
+
190
+ _DETAILS_DEFINITIONS = _details_definitions()
191
+ _DETAILS_DESCRIPTION = (
192
+ f"Bounded JSON object: at most {BOUNDED_JSON_MAX_NODES} total values "
193
+ f"including this object; maximum nesting depth {BOUNDED_JSON_MAX_DEPTH}; "
194
+ f"strings at most {BOUNDED_JSON_STRING_MAX_CHARS} characters; object keys "
195
+ f"at most {BOUNDED_JSON_KEY_MAX_CHARS} characters, starting with an ASCII "
196
+ "letter or digit and then using only ASCII letters, digits, '.', '_', ':', "
197
+ "'/', or '-'. Numeric values must be finite and within the signed 63-bit "
198
+ "range."
199
+ )
200
+
201
+
202
+ def details_model_schema() -> dict[str, Any]:
203
+ """Return the portable advertised bounded-details root schema."""
204
+
205
+ return {
206
+ "type": "object",
207
+ "description": _DETAILS_DESCRIPTION,
208
+ "propertyNames": {
209
+ "type": "string",
210
+ "maxLength": BOUNDED_JSON_KEY_MAX_CHARS,
211
+ "pattern": IDENTIFIER_RE.pattern,
212
+ },
213
+ "maxProperties": BOUNDED_JSON_MAX_NODES - 1,
214
+ "additionalProperties": {"$ref": "#/$defs/detailsValue1"},
215
+ }
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Argument validators (one per tool, exact bounded contracts)
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ def _args_autonomy(raw: Any) -> dict[str, Any]:
224
+ return exact_mapping(raw, "args", allowed=frozenset())
225
+
226
+
227
+ def _args_create_commitment(raw: Any) -> dict[str, Any]:
228
+ args = exact_mapping(
229
+ raw,
230
+ "args",
231
+ allowed={"description", "due_at", "priority"},
232
+ required={"description"},
233
+ )
234
+ bounded_text(
235
+ args["description"], "description", COMMITMENT_DESCRIPTION_MAX_CHARS,
236
+ )
237
+ if "due_at" in args:
238
+ bounded_text(
239
+ args["due_at"], "due_at", COMMITMENT_DUE_AT_MAX_CHARS,
240
+ allow_empty=True,
241
+ )
242
+ if "priority" in args:
243
+ bounded_integer(args["priority"], "priority", 0, 100)
244
+ return args
245
+
246
+
247
+ def _args_initiative_feedback(raw: Any) -> dict[str, Any]:
248
+ args = exact_mapping(
249
+ raw,
250
+ "args",
251
+ allowed={"initiative_id", "action", "details"},
252
+ required={"initiative_id", "action"},
253
+ )
254
+ bounded_text(
255
+ args["initiative_id"], "initiative_id", IDENTIFIER_MAX_CHARS,
256
+ identifier=True,
257
+ )
258
+ enum_text(
259
+ args["action"],
260
+ "action",
261
+ frozenset({"acknowledged", "actioned", "dismissed", "snoozed"}),
262
+ )
263
+ if "details" in args:
264
+ if not isinstance(args["details"], Mapping):
265
+ raise GovernedContractError("details must be an object")
266
+ args["details"] = bounded_json_value(args["details"], "details")
267
+ return args
268
+
269
+
270
+ def _args_record_insight(raw: Any) -> dict[str, Any]:
271
+ args = exact_mapping(
272
+ raw,
273
+ "args",
274
+ allowed={"confidence", "content", "insight_type"},
275
+ required={"content", "insight_type"},
276
+ )
277
+ bounded_text(args["content"], "content", INSIGHT_CONTENT_MAX_CHARS)
278
+ enum_text(
279
+ args["insight_type"],
280
+ "insight_type",
281
+ frozenset(
282
+ {
283
+ "preference",
284
+ "connection",
285
+ "fact",
286
+ "goal_hint",
287
+ "relationship_update",
288
+ }
289
+ ),
290
+ )
291
+ if "confidence" in args:
292
+ bounded_number(args["confidence"], "confidence", 0.0, 1.0)
293
+ return args
294
+
295
+
296
+ def _args_research(raw: Any) -> dict[str, Any]:
297
+ args = exact_mapping(
298
+ raw, "args", allowed={"depth", "topic"}, required={"topic"},
299
+ )
300
+ bounded_text(args["topic"], "topic", RESEARCH_TOPIC_MAX_CHARS)
301
+ if "depth" in args:
302
+ enum_text(args["depth"], "depth", frozenset({"quick", "standard", "deep"}))
303
+ return args
304
+
305
+
306
+ def _args_resolve_commitment(raw: Any) -> dict[str, Any]:
307
+ args = exact_mapping(
308
+ raw,
309
+ "args",
310
+ allowed={"commitment_id", "outcome", "reason"},
311
+ required={"commitment_id"},
312
+ )
313
+ bounded_text(
314
+ args["commitment_id"], "commitment_id", IDENTIFIER_MAX_CHARS,
315
+ identifier=True,
316
+ )
317
+ if "outcome" in args:
318
+ enum_text(
319
+ args["outcome"],
320
+ "outcome",
321
+ frozenset({"done", "invalid", "duplicate", "wont_do", "obsolete"}),
322
+ )
323
+ if "reason" in args:
324
+ bounded_text(
325
+ args["reason"], "reason", FREEFORM_REASON_MAX_CHARS,
326
+ allow_empty=True,
327
+ )
328
+ return args
329
+
330
+
331
+ def _args_task_complete(raw: Any) -> dict[str, Any]:
332
+ args = exact_mapping(raw, "args", allowed={"task_id"}, required={"task_id"})
333
+ bounded_text(
334
+ args["task_id"], "task_id", IDENTIFIER_MAX_CHARS, identifier=True,
335
+ )
336
+ return args
337
+
338
+
339
+ def _args_task_dismiss(raw: Any) -> dict[str, Any]:
340
+ args = exact_mapping(
341
+ raw, "args", allowed={"reason", "task_id"}, required={"task_id"},
342
+ )
343
+ bounded_text(
344
+ args["task_id"], "task_id", IDENTIFIER_MAX_CHARS, identifier=True,
345
+ )
346
+ if "reason" in args:
347
+ enum_text(
348
+ args["reason"],
349
+ "reason",
350
+ frozenset({"stale", "completed", "abandoned", "not_applicable"}),
351
+ )
352
+ return args
353
+
354
+
355
+ def _args_task_snooze(raw: Any) -> dict[str, Any]:
356
+ args = exact_mapping(
357
+ raw,
358
+ "args",
359
+ allowed={"hours", "reason", "task_id"},
360
+ required={"task_id"},
361
+ )
362
+ bounded_text(
363
+ args["task_id"], "task_id", IDENTIFIER_MAX_CHARS, identifier=True,
364
+ )
365
+ if "hours" in args:
366
+ bounded_integer(args["hours"], "hours", 1, 168)
367
+ if "reason" in args:
368
+ bounded_text(
369
+ args["reason"], "reason", FREEFORM_REASON_MAX_CHARS,
370
+ allow_empty=True,
371
+ )
372
+ return args
373
+
374
+
375
+ # ---------------------------------------------------------------------------
376
+ # Approval-display metadata (owner-facing summary/target/risk)
377
+ # ---------------------------------------------------------------------------
378
+
379
+
380
+ def _display_autonomy_disable(_args: Mapping[str, Any]) -> dict[str, str]:
381
+ return {
382
+ "summary": "Disable Colony autonomous scheduling",
383
+ "target": "Colony autonomy scheduler",
384
+ "risk": "Autonomous work will stop until it is explicitly enabled again",
385
+ }
386
+
387
+
388
+ def _display_autonomy_enable(_args: Mapping[str, Any]) -> dict[str, str]:
389
+ return {
390
+ "summary": "Enable Colony autonomous scheduling",
391
+ "target": "Colony autonomy scheduler",
392
+ "risk": (
393
+ "Colony may begin bounded autonomous work under its configured policies"
394
+ ),
395
+ }
396
+
397
+
398
+ def _display_create_commitment(args: Mapping[str, Any]) -> dict[str, str]:
399
+ return {
400
+ "summary": "Create Colony commitment %s" % _preview(args["description"]),
401
+ "target": "Private Colony commitment ledger",
402
+ "risk": (
403
+ "Adds one durable personal commitment; "
404
+ "no external communication is sent"
405
+ ),
406
+ }
407
+
408
+
409
+ def _display_initiative_feedback(args: Mapping[str, Any]) -> dict[str, str]:
410
+ return {
411
+ "summary": "Mark initiative %s as %s"
412
+ % (_preview(args["initiative_id"]), args["action"]),
413
+ "target": "Private Colony initiative ledger",
414
+ "risk": "Changes Colony's internal initiative state and learning evidence",
415
+ }
416
+
417
+
418
+ def _display_record_insight(args: Mapping[str, Any]) -> dict[str, str]:
419
+ return {
420
+ "summary": "Record %s insight %s"
421
+ % (args["insight_type"], _preview(args["content"])),
422
+ "target": "Private Colony memory and learning store",
423
+ "risk": (
424
+ "Persists model-derived personal context that can influence "
425
+ "future reasoning"
426
+ ),
427
+ }
428
+
429
+
430
+ def _display_research(args: Mapping[str, Any]) -> dict[str, str]:
431
+ return {
432
+ "summary": "Start %s Colony research on %s"
433
+ % (args.get("depth", "quick"), _preview(args["topic"])),
434
+ "target": "Colony research queue",
435
+ "risk": (
436
+ "May consume configured research resources; "
437
+ "external publishing is not authorized"
438
+ ),
439
+ }
440
+
441
+
442
+ def _display_resolve_commitment(args: Mapping[str, Any]) -> dict[str, str]:
443
+ return {
444
+ "summary": "Resolve commitment %s as %s"
445
+ % (_preview(args["commitment_id"]), args.get("outcome", "done")),
446
+ "target": "Private Colony commitment ledger",
447
+ "risk": "Changes one durable commitment's terminal state",
448
+ }
449
+
450
+
451
+ def _display_task_complete(args: Mapping[str, Any]) -> dict[str, str]:
452
+ return {
453
+ "summary": "Complete Colony task %s" % _preview(args["task_id"]),
454
+ "target": "Private Colony task or initiative ledger",
455
+ "risk": "Marks one internal task or initiative complete",
456
+ }
457
+
458
+
459
+ def _display_task_dismiss(args: Mapping[str, Any]) -> dict[str, str]:
460
+ return {
461
+ "summary": "Dismiss Colony task %s as %s"
462
+ % (_preview(args["task_id"]), args.get("reason", "stale")),
463
+ "target": "Private Colony task or initiative ledger",
464
+ "risk": "Dismisses one internal task or initiative from active work",
465
+ }
466
+
467
+
468
+ def _display_task_snooze(args: Mapping[str, Any]) -> dict[str, str]:
469
+ return {
470
+ "summary": "Snooze Colony task %s for %d hours"
471
+ % (_preview(args["task_id"]), args.get("hours", 24)),
472
+ "target": "Private Colony task or initiative ledger",
473
+ "risk": (
474
+ "Defers one internal task or initiative until the bounded "
475
+ "snooze expires"
476
+ ),
477
+ }
478
+
479
+
480
+ # ---------------------------------------------------------------------------
481
+ # The catalog
482
+ # ---------------------------------------------------------------------------
483
+
484
+
485
+ @dataclass(frozen=True, slots=True)
486
+ class ToolSpec:
487
+ """One governed tool and its complete model/validation contract."""
488
+
489
+ name: str
490
+ model_description: str
491
+ model_parameters: Mapping[str, Any]
492
+ validate_args: Callable[[Any], dict[str, Any]]
493
+ approval_display: Callable[[Mapping[str, Any]], dict[str, str]]
494
+ # True → this tool must always require a per-message owner approval and
495
+ # can never be authorized by a standing bounded grant (owner decision).
496
+ non_grantable: bool = False
497
+
498
+ @property
499
+ def model_schema(self) -> dict[str, Any]:
500
+ """Return an isolated copy safe for a model-facing plugin catalog."""
501
+
502
+ return {
503
+ "name": self.name,
504
+ "description": self.model_description,
505
+ "parameters": copy.deepcopy(self.model_parameters),
506
+ }
507
+
508
+
509
+ TOOL_CATALOG: dict[str, ToolSpec] = {
510
+ spec.name: spec
511
+ for spec in (
512
+ ToolSpec(
513
+ name="colony_autonomy_disable",
514
+ model_description=(
515
+ "Submit a governed intent to disable autonomous work scheduling."
516
+ ),
517
+ model_parameters=_parameters({}),
518
+ validate_args=_args_autonomy,
519
+ approval_display=_display_autonomy_disable,
520
+ non_grantable=True,
521
+ ),
522
+ ToolSpec(
523
+ name="colony_autonomy_enable",
524
+ model_description=(
525
+ "Submit a governed intent to enable autonomous work scheduling."
526
+ ),
527
+ model_parameters=_parameters({}),
528
+ validate_args=_args_autonomy,
529
+ approval_display=_display_autonomy_enable,
530
+ non_grantable=True,
531
+ ),
532
+ ToolSpec(
533
+ name="colony_create_commitment",
534
+ model_description=(
535
+ "Submit a governed intent to create a commitment for this "
536
+ "participant."
537
+ ),
538
+ model_parameters=_parameters(
539
+ {
540
+ "description": _text_model_schema(
541
+ COMMITMENT_DESCRIPTION_MAX_CHARS,
542
+ ),
543
+ "due_at": _text_model_schema(
544
+ COMMITMENT_DUE_AT_MAX_CHARS, allow_empty=True,
545
+ ),
546
+ "priority": {
547
+ "type": "integer",
548
+ "minimum": 0,
549
+ "maximum": 100,
550
+ "default": COMMITMENT_PRIORITY_DEFAULT,
551
+ },
552
+ },
553
+ ("description",),
554
+ ),
555
+ validate_args=_args_create_commitment,
556
+ approval_display=_display_create_commitment,
557
+ ),
558
+ ToolSpec(
559
+ name="colony_initiative_feedback",
560
+ model_description=(
561
+ "Submit a governed intent describing an initiative outcome."
562
+ ),
563
+ model_parameters=_parameters(
564
+ {
565
+ "action": {
566
+ "type": "string",
567
+ "enum": [
568
+ "acknowledged", "actioned", "dismissed", "snoozed",
569
+ ],
570
+ },
571
+ "details": details_model_schema(),
572
+ "initiative_id": identifier_model_schema(),
573
+ },
574
+ ("initiative_id", "action"),
575
+ definitions=_DETAILS_DEFINITIONS,
576
+ ),
577
+ validate_args=_args_initiative_feedback,
578
+ approval_display=_display_initiative_feedback,
579
+ ),
580
+ ToolSpec(
581
+ name="colony_record_insight",
582
+ model_description=(
583
+ "Submit a governed intent to record a conversational insight."
584
+ ),
585
+ model_parameters=_parameters(
586
+ {
587
+ "confidence": {
588
+ "type": "number",
589
+ "minimum": 0,
590
+ "maximum": 1,
591
+ "default": 0.7,
592
+ },
593
+ "content": _text_model_schema(INSIGHT_CONTENT_MAX_CHARS),
594
+ "insight_type": {
595
+ "type": "string",
596
+ "enum": [
597
+ "preference", "connection", "fact", "goal_hint",
598
+ "relationship_update",
599
+ ],
600
+ },
601
+ },
602
+ ("insight_type", "content"),
603
+ ),
604
+ validate_args=_args_record_insight,
605
+ approval_display=_display_record_insight,
606
+ ),
607
+ ToolSpec(
608
+ name="colony_research",
609
+ model_description=(
610
+ "Submit a governed intent to queue durable Colony research."
611
+ ),
612
+ model_parameters=_parameters(
613
+ {
614
+ "depth": {
615
+ "type": "string",
616
+ "enum": ["quick", "standard", "deep"],
617
+ "default": "quick",
618
+ },
619
+ "topic": _text_model_schema(RESEARCH_TOPIC_MAX_CHARS),
620
+ },
621
+ ("topic",),
622
+ ),
623
+ validate_args=_args_research,
624
+ approval_display=_display_research,
625
+ ),
626
+ ToolSpec(
627
+ name="colony_resolve_commitment",
628
+ model_description=(
629
+ "Submit a governed intent to resolve a commitment."
630
+ ),
631
+ model_parameters=_parameters(
632
+ {
633
+ "commitment_id": identifier_model_schema(),
634
+ "outcome": {
635
+ "type": "string",
636
+ "enum": [
637
+ "done", "invalid", "duplicate", "wont_do", "obsolete",
638
+ ],
639
+ "default": "done",
640
+ },
641
+ "reason": _text_model_schema(
642
+ FREEFORM_REASON_MAX_CHARS, allow_empty=True,
643
+ ),
644
+ },
645
+ ("commitment_id",),
646
+ ),
647
+ validate_args=_args_resolve_commitment,
648
+ approval_display=_display_resolve_commitment,
649
+ ),
650
+ ToolSpec(
651
+ name="colony_task_complete",
652
+ model_description=(
653
+ "Submit a governed intent to complete a task or initiative."
654
+ ),
655
+ model_parameters=_parameters(
656
+ {"task_id": identifier_model_schema()}, ("task_id",),
657
+ ),
658
+ validate_args=_args_task_complete,
659
+ approval_display=_display_task_complete,
660
+ ),
661
+ ToolSpec(
662
+ name="colony_task_dismiss",
663
+ model_description=(
664
+ "Submit a governed intent to dismiss a task or initiative."
665
+ ),
666
+ model_parameters=_parameters(
667
+ {
668
+ "reason": {
669
+ "type": "string",
670
+ "enum": [
671
+ "stale", "completed", "abandoned", "not_applicable",
672
+ ],
673
+ "default": "stale",
674
+ },
675
+ "task_id": identifier_model_schema(),
676
+ },
677
+ ("task_id",),
678
+ ),
679
+ validate_args=_args_task_dismiss,
680
+ approval_display=_display_task_dismiss,
681
+ ),
682
+ ToolSpec(
683
+ name="colony_task_snooze",
684
+ model_description=(
685
+ "Submit a governed intent to snooze a task or initiative."
686
+ ),
687
+ model_parameters=_parameters(
688
+ {
689
+ "hours": {
690
+ "type": "integer",
691
+ "minimum": 1,
692
+ "maximum": 168,
693
+ "default": 24,
694
+ },
695
+ "reason": {
696
+ **_text_model_schema(
697
+ FREEFORM_REASON_MAX_CHARS, allow_empty=True,
698
+ ),
699
+ "default": "",
700
+ },
701
+ "task_id": identifier_model_schema(),
702
+ },
703
+ ("task_id",),
704
+ ),
705
+ validate_args=_args_task_snooze,
706
+ approval_display=_display_task_snooze,
707
+ ),
708
+ )
709
+ }
710
+
711
+ ACTION_TOOL_NAMES = frozenset(TOOL_CATALOG)
712
+ NON_GRANTABLE_TOOL_NAMES = frozenset(
713
+ name for name, spec in TOOL_CATALOG.items() if spec.non_grantable
714
+ )
715
+ GRANT_AUTHORIZABLE_TOOL_NAMES = ACTION_TOOL_NAMES - NON_GRANTABLE_TOOL_NAMES
716
+ ACTION_MODEL_TOOL_SCHEMAS = tuple(
717
+ TOOL_CATALOG[name].model_schema for name in sorted(ACTION_TOOL_NAMES)
718
+ )
719
+
720
+
721
+ def validate_tool_args(tool_name: Any, raw: Any) -> dict[str, Any]:
722
+ """Validate one tool call against the catalog's exact bounded contract."""
723
+
724
+ spec = TOOL_CATALOG.get(tool_name) if isinstance(tool_name, str) else None
725
+ if spec is None:
726
+ raise ToolCatalogError("tool is not a governed Colony action")
727
+ return spec.validate_args(raw)
728
+
729
+
730
+ __all__ = (
731
+ "ACTION_MODEL_TOOL_SCHEMAS",
732
+ "ACTION_TOOL_NAMES",
733
+ "BOUNDED_JSON_INTEGER_MAX",
734
+ "BOUNDED_JSON_KEY_MAX_CHARS",
735
+ "BOUNDED_JSON_MAX_DEPTH",
736
+ "BOUNDED_JSON_MAX_NODES",
737
+ "BOUNDED_JSON_STRING_MAX_CHARS",
738
+ "COMMITMENT_DESCRIPTION_MAX_CHARS",
739
+ "COMMITMENT_DUE_AT_MAX_CHARS",
740
+ "COMMITMENT_PRIORITY_DEFAULT",
741
+ "FREEFORM_REASON_MAX_CHARS",
742
+ "GRANT_AUTHORIZABLE_TOOL_NAMES",
743
+ "IDENTIFIER_MAX_CHARS",
744
+ "IDENTIFIER_RE",
745
+ "INSIGHT_CONTENT_MAX_CHARS",
746
+ "NON_GRANTABLE_TOOL_NAMES",
747
+ "RESEARCH_TOPIC_MAX_CHARS",
748
+ "TOOL_CATALOG",
749
+ "ToolCatalogError",
750
+ "ToolSpec",
751
+ "details_model_schema",
752
+ "identifier_model_schema",
753
+ "validate_tool_args",
754
+ )