hermes-agent 0.10.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.
Files changed (243) hide show
  1. acp_adapter/__init__.py +1 -0
  2. acp_adapter/__main__.py +5 -0
  3. acp_adapter/auth.py +24 -0
  4. acp_adapter/entry.py +85 -0
  5. acp_adapter/events.py +175 -0
  6. acp_adapter/permissions.py +77 -0
  7. acp_adapter/server.py +728 -0
  8. acp_adapter/session.py +475 -0
  9. acp_adapter/tools.py +214 -0
  10. agent/__init__.py +6 -0
  11. agent/anthropic_adapter.py +1520 -0
  12. agent/auxiliary_client.py +2716 -0
  13. agent/bedrock_adapter.py +1098 -0
  14. agent/context_compressor.py +1163 -0
  15. agent/context_engine.py +184 -0
  16. agent/context_references.py +520 -0
  17. agent/copilot_acp_client.py +586 -0
  18. agent/credential_pool.py +1418 -0
  19. agent/display.py +996 -0
  20. agent/error_classifier.py +829 -0
  21. agent/gemini_cloudcode_adapter.py +764 -0
  22. agent/google_code_assist.py +417 -0
  23. agent/google_oauth.py +1048 -0
  24. agent/insights.py +768 -0
  25. agent/manual_compression_feedback.py +49 -0
  26. agent/memory_manager.py +373 -0
  27. agent/memory_provider.py +231 -0
  28. agent/model_metadata.py +1116 -0
  29. agent/models_dev.py +586 -0
  30. agent/nous_rate_guard.py +182 -0
  31. agent/prompt_builder.py +1045 -0
  32. agent/prompt_caching.py +72 -0
  33. agent/rate_limit_tracker.py +246 -0
  34. agent/redact.py +198 -0
  35. agent/retry_utils.py +57 -0
  36. agent/skill_commands.py +377 -0
  37. agent/skill_utils.py +465 -0
  38. agent/smart_model_routing.py +195 -0
  39. agent/subdirectory_hints.py +224 -0
  40. agent/title_generator.py +125 -0
  41. agent/trajectory.py +56 -0
  42. agent/usage_pricing.py +687 -0
  43. batch_runner.py +1290 -0
  44. cli.py +10275 -0
  45. cron/__init__.py +42 -0
  46. cron/jobs.py +768 -0
  47. cron/scheduler.py +1009 -0
  48. gateway/__init__.py +35 -0
  49. gateway/builtin_hooks/__init__.py +1 -0
  50. gateway/builtin_hooks/boot_md.py +85 -0
  51. gateway/channel_directory.py +276 -0
  52. gateway/config.py +1178 -0
  53. gateway/delivery.py +256 -0
  54. gateway/display_config.py +194 -0
  55. gateway/hooks.py +170 -0
  56. gateway/mirror.py +132 -0
  57. gateway/pairing.py +309 -0
  58. gateway/platforms/__init__.py +19 -0
  59. gateway/platforms/api_server.py +2436 -0
  60. gateway/platforms/base.py +2164 -0
  61. gateway/platforms/bluebubbles.py +918 -0
  62. gateway/platforms/dingtalk.py +347 -0
  63. gateway/platforms/discord.py +3191 -0
  64. gateway/platforms/email.py +625 -0
  65. gateway/platforms/feishu.py +4131 -0
  66. gateway/platforms/helpers.py +264 -0
  67. gateway/platforms/homeassistant.py +449 -0
  68. gateway/platforms/matrix.py +2216 -0
  69. gateway/platforms/mattermost.py +740 -0
  70. gateway/platforms/qqbot.py +1960 -0
  71. gateway/platforms/signal.py +825 -0
  72. gateway/platforms/slack.py +1695 -0
  73. gateway/platforms/sms.py +373 -0
  74. gateway/platforms/telegram.py +2914 -0
  75. gateway/platforms/telegram_network.py +246 -0
  76. gateway/platforms/webhook.py +672 -0
  77. gateway/platforms/wecom.py +1430 -0
  78. gateway/platforms/wecom_callback.py +401 -0
  79. gateway/platforms/wecom_crypto.py +142 -0
  80. gateway/platforms/weixin.py +1829 -0
  81. gateway/platforms/whatsapp.py +989 -0
  82. gateway/restart.py +20 -0
  83. gateway/run.py +9892 -0
  84. gateway/session.py +1090 -0
  85. gateway/session_context.py +145 -0
  86. gateway/status.py +455 -0
  87. gateway/sticker_cache.py +111 -0
  88. gateway/stream_consumer.py +761 -0
  89. hermes_agent-0.10.0.dist-info/METADATA +295 -0
  90. hermes_agent-0.10.0.dist-info/RECORD +243 -0
  91. hermes_agent-0.10.0.dist-info/WHEEL +5 -0
  92. hermes_agent-0.10.0.dist-info/entry_points.txt +4 -0
  93. hermes_agent-0.10.0.dist-info/licenses/LICENSE +21 -0
  94. hermes_agent-0.10.0.dist-info/top_level.txt +20 -0
  95. hermes_cli/__init__.py +15 -0
  96. hermes_cli/auth.py +3429 -0
  97. hermes_cli/auth_commands.py +587 -0
  98. hermes_cli/backup.py +655 -0
  99. hermes_cli/banner.py +535 -0
  100. hermes_cli/callbacks.py +242 -0
  101. hermes_cli/claw.py +734 -0
  102. hermes_cli/cli_output.py +78 -0
  103. hermes_cli/clipboard.py +432 -0
  104. hermes_cli/codex_models.py +176 -0
  105. hermes_cli/colors.py +38 -0
  106. hermes_cli/commands.py +1234 -0
  107. hermes_cli/completion.py +315 -0
  108. hermes_cli/config.py +3619 -0
  109. hermes_cli/copilot_auth.py +299 -0
  110. hermes_cli/cron.py +290 -0
  111. hermes_cli/curses_ui.py +466 -0
  112. hermes_cli/debug.py +477 -0
  113. hermes_cli/default_soul.py +11 -0
  114. hermes_cli/doctor.py +1149 -0
  115. hermes_cli/dump.py +345 -0
  116. hermes_cli/env_loader.py +123 -0
  117. hermes_cli/gateway.py +3161 -0
  118. hermes_cli/logs.py +390 -0
  119. hermes_cli/main.py +6552 -0
  120. hermes_cli/mcp_config.py +777 -0
  121. hermes_cli/memory_setup.py +457 -0
  122. hermes_cli/model_normalize.py +407 -0
  123. hermes_cli/model_switch.py +1125 -0
  124. hermes_cli/models.py +2173 -0
  125. hermes_cli/nous_subscription.py +778 -0
  126. hermes_cli/pairing.py +97 -0
  127. hermes_cli/platforms.py +47 -0
  128. hermes_cli/plugins.py +843 -0
  129. hermes_cli/plugins_cmd.py +1128 -0
  130. hermes_cli/profiles.py +1094 -0
  131. hermes_cli/providers.py +583 -0
  132. hermes_cli/runtime_provider.py +996 -0
  133. hermes_cli/setup.py +3235 -0
  134. hermes_cli/skills_config.py +177 -0
  135. hermes_cli/skills_hub.py +1300 -0
  136. hermes_cli/skin_engine.py +818 -0
  137. hermes_cli/status.py +488 -0
  138. hermes_cli/tips.py +349 -0
  139. hermes_cli/tools_config.py +1868 -0
  140. hermes_cli/uninstall.py +326 -0
  141. hermes_cli/web_server.py +2350 -0
  142. hermes_cli/webhook.py +259 -0
  143. hermes_constants.py +294 -0
  144. hermes_logging.py +390 -0
  145. hermes_state.py +1238 -0
  146. hermes_time.py +104 -0
  147. model_tools.py +562 -0
  148. plugins/__init__.py +1 -0
  149. plugins/context_engine/__init__.py +219 -0
  150. plugins/example-dashboard/dashboard/plugin_api.py +14 -0
  151. plugins/memory/__init__.py +406 -0
  152. plugins/memory/byterover/__init__.py +383 -0
  153. plugins/memory/hindsight/__init__.py +883 -0
  154. plugins/memory/holographic/__init__.py +407 -0
  155. plugins/memory/holographic/holographic.py +203 -0
  156. plugins/memory/holographic/retrieval.py +593 -0
  157. plugins/memory/holographic/store.py +574 -0
  158. plugins/memory/honcho/__init__.py +1054 -0
  159. plugins/memory/honcho/cli.py +1398 -0
  160. plugins/memory/honcho/client.py +676 -0
  161. plugins/memory/honcho/session.py +1255 -0
  162. plugins/memory/mem0/__init__.py +373 -0
  163. plugins/memory/openviking/__init__.py +674 -0
  164. plugins/memory/retaindb/__init__.py +766 -0
  165. plugins/memory/supermemory/__init__.py +791 -0
  166. rl_cli.py +446 -0
  167. run_agent.py +11603 -0
  168. tools/__init__.py +25 -0
  169. tools/ansi_strip.py +44 -0
  170. tools/approval.py +957 -0
  171. tools/binary_extensions.py +42 -0
  172. tools/browser_camofox.py +600 -0
  173. tools/browser_camofox_state.py +47 -0
  174. tools/browser_providers/__init__.py +10 -0
  175. tools/browser_providers/base.py +59 -0
  176. tools/browser_providers/browser_use.py +215 -0
  177. tools/browser_providers/browserbase.py +217 -0
  178. tools/browser_providers/firecrawl.py +107 -0
  179. tools/browser_tool.py +2418 -0
  180. tools/budget_config.py +52 -0
  181. tools/checkpoint_manager.py +654 -0
  182. tools/clarify_tool.py +141 -0
  183. tools/code_execution_tool.py +1416 -0
  184. tools/credential_files.py +407 -0
  185. tools/cronjob_tools.py +510 -0
  186. tools/debug_helpers.py +105 -0
  187. tools/delegate_tool.py +1143 -0
  188. tools/env_passthrough.py +101 -0
  189. tools/environments/__init__.py +13 -0
  190. tools/environments/base.py +599 -0
  191. tools/environments/daytona.py +259 -0
  192. tools/environments/docker.py +578 -0
  193. tools/environments/file_sync.py +393 -0
  194. tools/environments/local.py +314 -0
  195. tools/environments/managed_modal.py +282 -0
  196. tools/environments/modal.py +460 -0
  197. tools/environments/modal_utils.py +199 -0
  198. tools/environments/singularity.py +262 -0
  199. tools/environments/ssh.py +275 -0
  200. tools/file_operations.py +1216 -0
  201. tools/file_tools.py +799 -0
  202. tools/fuzzy_match.py +566 -0
  203. tools/homeassistant_tool.py +513 -0
  204. tools/image_generation_tool.py +837 -0
  205. tools/interrupt.py +76 -0
  206. tools/managed_tool_gateway.py +167 -0
  207. tools/mcp_oauth.py +526 -0
  208. tools/mcp_oauth_manager.py +413 -0
  209. tools/mcp_tool.py +2599 -0
  210. tools/memory_tool.py +584 -0
  211. tools/mixture_of_agents_tool.py +539 -0
  212. tools/neutts_synth.py +104 -0
  213. tools/openrouter_client.py +33 -0
  214. tools/osv_check.py +155 -0
  215. tools/patch_parser.py +580 -0
  216. tools/path_security.py +43 -0
  217. tools/process_registry.py +1184 -0
  218. tools/registry.py +482 -0
  219. tools/rl_training_tool.py +1396 -0
  220. tools/send_message_tool.py +1304 -0
  221. tools/session_search_tool.py +562 -0
  222. tools/skill_manager_tool.py +789 -0
  223. tools/skills_guard.py +928 -0
  224. tools/skills_hub.py +3053 -0
  225. tools/skills_sync.py +414 -0
  226. tools/skills_tool.py +1420 -0
  227. tools/terminal_tool.py +1756 -0
  228. tools/tirith_security.py +684 -0
  229. tools/todo_tool.py +277 -0
  230. tools/tool_backend_helpers.py +121 -0
  231. tools/tool_result_storage.py +226 -0
  232. tools/transcription_tools.py +677 -0
  233. tools/tts_tool.py +1334 -0
  234. tools/url_safety.py +97 -0
  235. tools/vision_tools.py +789 -0
  236. tools/voice_mode.py +1017 -0
  237. tools/web_tools.py +2101 -0
  238. tools/website_policy.py +282 -0
  239. tools/xai_http.py +12 -0
  240. toolset_distributions.py +364 -0
  241. toolsets.py +702 -0
  242. trajectory_compressor.py +1462 -0
  243. utils.py +196 -0
@@ -0,0 +1 @@
1
+ """ACP (Agent Communication Protocol) adapter for hermes-agent."""
@@ -0,0 +1,5 @@
1
+ """Allow running the ACP adapter as ``python -m acp_adapter``."""
2
+
3
+ from .entry import main
4
+
5
+ main()
acp_adapter/auth.py ADDED
@@ -0,0 +1,24 @@
1
+ """ACP auth helpers — detect the currently configured Hermes provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ def detect_provider() -> Optional[str]:
9
+ """Resolve the active Hermes runtime provider, or None if unavailable."""
10
+ try:
11
+ from hermes_cli.runtime_provider import resolve_runtime_provider
12
+ runtime = resolve_runtime_provider()
13
+ api_key = runtime.get("api_key")
14
+ provider = runtime.get("provider")
15
+ if isinstance(api_key, str) and api_key.strip() and isinstance(provider, str) and provider.strip():
16
+ return provider.strip().lower()
17
+ except Exception:
18
+ return None
19
+ return None
20
+
21
+
22
+ def has_provider() -> bool:
23
+ """Return True if Hermes can resolve any runtime provider credentials."""
24
+ return detect_provider() is not None
acp_adapter/entry.py ADDED
@@ -0,0 +1,85 @@
1
+ """CLI entry point for the hermes-agent ACP adapter.
2
+
3
+ Loads environment variables from ``~/.hermes/.env``, configures logging
4
+ to write to stderr (so stdout is reserved for ACP JSON-RPC transport),
5
+ and starts the ACP agent server.
6
+
7
+ Usage::
8
+
9
+ python -m acp_adapter.entry
10
+ # or
11
+ hermes acp
12
+ # or
13
+ hermes-acp
14
+ """
15
+
16
+ import asyncio
17
+ import logging
18
+ import sys
19
+ from pathlib import Path
20
+ from hermes_constants import get_hermes_home
21
+
22
+
23
+ def _setup_logging() -> None:
24
+ """Route all logging to stderr so stdout stays clean for ACP stdio."""
25
+ handler = logging.StreamHandler(sys.stderr)
26
+ handler.setFormatter(
27
+ logging.Formatter(
28
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
29
+ datefmt="%Y-%m-%d %H:%M:%S",
30
+ )
31
+ )
32
+ root = logging.getLogger()
33
+ root.handlers.clear()
34
+ root.addHandler(handler)
35
+ root.setLevel(logging.INFO)
36
+
37
+ # Quiet down noisy libraries
38
+ logging.getLogger("httpx").setLevel(logging.WARNING)
39
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
40
+ logging.getLogger("openai").setLevel(logging.WARNING)
41
+
42
+
43
+ def _load_env() -> None:
44
+ """Load .env from HERMES_HOME (default ``~/.hermes``)."""
45
+ from hermes_cli.env_loader import load_hermes_dotenv
46
+
47
+ hermes_home = get_hermes_home()
48
+ loaded = load_hermes_dotenv(hermes_home=hermes_home)
49
+ if loaded:
50
+ for env_file in loaded:
51
+ logging.getLogger(__name__).info("Loaded env from %s", env_file)
52
+ else:
53
+ logging.getLogger(__name__).info(
54
+ "No .env found at %s, using system env", hermes_home / ".env"
55
+ )
56
+
57
+
58
+ def main() -> None:
59
+ """Entry point: load env, configure logging, run the ACP agent."""
60
+ _setup_logging()
61
+ _load_env()
62
+
63
+ logger = logging.getLogger(__name__)
64
+ logger.info("Starting hermes-agent ACP adapter")
65
+
66
+ # Ensure the project root is on sys.path so ``from run_agent import AIAgent`` works
67
+ project_root = str(Path(__file__).resolve().parent.parent)
68
+ if project_root not in sys.path:
69
+ sys.path.insert(0, project_root)
70
+
71
+ import acp
72
+ from .server import HermesACPAgent
73
+
74
+ agent = HermesACPAgent()
75
+ try:
76
+ asyncio.run(acp.run_agent(agent, use_unstable_protocol=True))
77
+ except KeyboardInterrupt:
78
+ logger.info("Shutting down (KeyboardInterrupt)")
79
+ except Exception:
80
+ logger.exception("ACP agent crashed")
81
+ sys.exit(1)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
acp_adapter/events.py ADDED
@@ -0,0 +1,175 @@
1
+ """Callback factories for bridging AIAgent events to ACP notifications.
2
+
3
+ Each factory returns a callable with the signature that AIAgent expects
4
+ for its callbacks. Internally, the callbacks push ACP session updates
5
+ to the client via ``conn.session_update()`` using
6
+ ``asyncio.run_coroutine_threadsafe()`` (since AIAgent runs in a worker
7
+ thread while the event loop lives on the main thread).
8
+ """
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ from collections import deque
14
+ from typing import Any, Callable, Deque, Dict
15
+
16
+ import acp
17
+
18
+ from .tools import (
19
+ build_tool_complete,
20
+ build_tool_start,
21
+ make_tool_call_id,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _send_update(
28
+ conn: acp.Client,
29
+ session_id: str,
30
+ loop: asyncio.AbstractEventLoop,
31
+ update: Any,
32
+ ) -> None:
33
+ """Fire-and-forget an ACP session update from a worker thread."""
34
+ try:
35
+ future = asyncio.run_coroutine_threadsafe(
36
+ conn.session_update(session_id, update), loop
37
+ )
38
+ future.result(timeout=5)
39
+ except Exception:
40
+ logger.debug("Failed to send ACP update", exc_info=True)
41
+
42
+
43
+ # ------------------------------------------------------------------
44
+ # Tool progress callback
45
+ # ------------------------------------------------------------------
46
+
47
+ def make_tool_progress_cb(
48
+ conn: acp.Client,
49
+ session_id: str,
50
+ loop: asyncio.AbstractEventLoop,
51
+ tool_call_ids: Dict[str, Deque[str]],
52
+ ) -> Callable:
53
+ """Create a ``tool_progress_callback`` for AIAgent.
54
+
55
+ Signature expected by AIAgent::
56
+
57
+ tool_progress_callback(event_type: str, name: str, preview: str, args: dict, **kwargs)
58
+
59
+ Emits ``ToolCallStart`` for ``tool.started`` events and tracks IDs in a FIFO
60
+ queue per tool name so duplicate/parallel same-name calls still complete
61
+ against the correct ACP tool call. Other event types (``tool.completed``,
62
+ ``reasoning.available``) are silently ignored.
63
+ """
64
+
65
+ def _tool_progress(event_type: str, name: str = None, preview: str = None, args: Any = None, **kwargs) -> None:
66
+ # Only emit ACP ToolCallStart for tool.started; ignore other event types
67
+ if event_type != "tool.started":
68
+ return
69
+ if isinstance(args, str):
70
+ try:
71
+ args = json.loads(args)
72
+ except (json.JSONDecodeError, TypeError):
73
+ args = {"raw": args}
74
+ if not isinstance(args, dict):
75
+ args = {}
76
+
77
+ tc_id = make_tool_call_id()
78
+ queue = tool_call_ids.get(name)
79
+ if queue is None:
80
+ queue = deque()
81
+ tool_call_ids[name] = queue
82
+ elif isinstance(queue, str):
83
+ queue = deque([queue])
84
+ tool_call_ids[name] = queue
85
+ queue.append(tc_id)
86
+
87
+ update = build_tool_start(tc_id, name, args)
88
+ _send_update(conn, session_id, loop, update)
89
+
90
+ return _tool_progress
91
+
92
+
93
+ # ------------------------------------------------------------------
94
+ # Thinking callback
95
+ # ------------------------------------------------------------------
96
+
97
+ def make_thinking_cb(
98
+ conn: acp.Client,
99
+ session_id: str,
100
+ loop: asyncio.AbstractEventLoop,
101
+ ) -> Callable:
102
+ """Create a ``thinking_callback`` for AIAgent."""
103
+
104
+ def _thinking(text: str) -> None:
105
+ if not text:
106
+ return
107
+ update = acp.update_agent_thought_text(text)
108
+ _send_update(conn, session_id, loop, update)
109
+
110
+ return _thinking
111
+
112
+
113
+ # ------------------------------------------------------------------
114
+ # Step callback
115
+ # ------------------------------------------------------------------
116
+
117
+ def make_step_cb(
118
+ conn: acp.Client,
119
+ session_id: str,
120
+ loop: asyncio.AbstractEventLoop,
121
+ tool_call_ids: Dict[str, Deque[str]],
122
+ ) -> Callable:
123
+ """Create a ``step_callback`` for AIAgent.
124
+
125
+ Signature expected by AIAgent::
126
+
127
+ step_callback(api_call_count: int, prev_tools: list)
128
+ """
129
+
130
+ def _step(api_call_count: int, prev_tools: Any = None) -> None:
131
+ if prev_tools and isinstance(prev_tools, list):
132
+ for tool_info in prev_tools:
133
+ tool_name = None
134
+ result = None
135
+
136
+ if isinstance(tool_info, dict):
137
+ tool_name = tool_info.get("name") or tool_info.get("function_name")
138
+ result = tool_info.get("result") or tool_info.get("output")
139
+ elif isinstance(tool_info, str):
140
+ tool_name = tool_info
141
+
142
+ queue = tool_call_ids.get(tool_name or "")
143
+ if isinstance(queue, str):
144
+ queue = deque([queue])
145
+ tool_call_ids[tool_name] = queue
146
+ if tool_name and queue:
147
+ tc_id = queue.popleft()
148
+ update = build_tool_complete(
149
+ tc_id, tool_name, result=str(result) if result is not None else None
150
+ )
151
+ _send_update(conn, session_id, loop, update)
152
+ if not queue:
153
+ tool_call_ids.pop(tool_name, None)
154
+
155
+ return _step
156
+
157
+
158
+ # ------------------------------------------------------------------
159
+ # Agent message callback
160
+ # ------------------------------------------------------------------
161
+
162
+ def make_message_cb(
163
+ conn: acp.Client,
164
+ session_id: str,
165
+ loop: asyncio.AbstractEventLoop,
166
+ ) -> Callable:
167
+ """Create a callback that streams agent response text to the editor."""
168
+
169
+ def _message(text: str) -> None:
170
+ if not text:
171
+ return
172
+ update = acp.update_agent_message_text(text)
173
+ _send_update(conn, session_id, loop, update)
174
+
175
+ return _message
@@ -0,0 +1,77 @@
1
+ """ACP permission bridging — maps ACP approval requests to hermes approval callbacks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from concurrent.futures import TimeoutError as FutureTimeout
8
+ from typing import Callable
9
+
10
+ from acp.schema import (
11
+ AllowedOutcome,
12
+ PermissionOption,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Maps ACP PermissionOptionKind -> hermes approval result strings
18
+ _KIND_TO_HERMES = {
19
+ "allow_once": "once",
20
+ "allow_always": "always",
21
+ "reject_once": "deny",
22
+ "reject_always": "deny",
23
+ }
24
+
25
+
26
+ def make_approval_callback(
27
+ request_permission_fn: Callable,
28
+ loop: asyncio.AbstractEventLoop,
29
+ session_id: str,
30
+ timeout: float = 60.0,
31
+ ) -> Callable[[str, str], str]:
32
+ """
33
+ Return a hermes-compatible ``approval_callback(command, description) -> str``
34
+ that bridges to the ACP client's ``request_permission`` call.
35
+
36
+ Args:
37
+ request_permission_fn: The ACP connection's ``request_permission`` coroutine.
38
+ loop: The event loop on which the ACP connection lives.
39
+ session_id: Current ACP session id.
40
+ timeout: Seconds to wait for a response before auto-denying.
41
+ """
42
+
43
+ def _callback(command: str, description: str) -> str:
44
+ options = [
45
+ PermissionOption(option_id="allow_once", kind="allow_once", name="Allow once"),
46
+ PermissionOption(option_id="allow_always", kind="allow_always", name="Allow always"),
47
+ PermissionOption(option_id="deny", kind="reject_once", name="Deny"),
48
+ ]
49
+ import acp as _acp
50
+
51
+ tool_call = _acp.start_tool_call("perm-check", command, kind="execute")
52
+
53
+ coro = request_permission_fn(
54
+ session_id=session_id,
55
+ tool_call=tool_call,
56
+ options=options,
57
+ )
58
+
59
+ try:
60
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
61
+ response = future.result(timeout=timeout)
62
+ except (FutureTimeout, Exception) as exc:
63
+ logger.warning("Permission request timed out or failed: %s", exc)
64
+ return "deny"
65
+
66
+ outcome = response.outcome
67
+ if isinstance(outcome, AllowedOutcome):
68
+ option_id = outcome.option_id
69
+ # Look up the kind from our options list
70
+ for opt in options:
71
+ if opt.option_id == option_id:
72
+ return _KIND_TO_HERMES.get(opt.kind, "deny")
73
+ return "once" # fallback for unknown option_id
74
+ else:
75
+ return "deny"
76
+
77
+ return _callback