devops-bot-sdk 1.6.4__tar.gz → 1.6.8__tar.gz
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.
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agentd.py +8 -6
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/chat.py +56 -10
- devops_bot_sdk-1.6.8/sdk/agents/engines/claude_code.py +122 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/manifest.py +59 -2
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/secrets.py +9 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/ticket_run.py +15 -14
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/cli.py +14 -22
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/config.py +64 -6
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/loop.py +5 -4
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/local_exec.py +23 -11
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_run_ticket.py +56 -14
- devops_bot_sdk-1.6.8/tests/test_agent_workspace_settings.py +140 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agents_connectors_builder.py +16 -2
- devops_bot_sdk-1.6.8/tests/test_allowed_tools_config.py +102 -0
- devops_bot_sdk-1.6.4/sdk/agents/engines/claude_code.py +0 -61
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/README.md +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.8}/tests/test_vps_health.py +0 -0
|
@@ -97,7 +97,9 @@ tests/test_agent_listing.py
|
|
|
97
97
|
tests/test_agent_prompt_async.py
|
|
98
98
|
tests/test_agent_prompt_models.py
|
|
99
99
|
tests/test_agent_run_ticket.py
|
|
100
|
+
tests/test_agent_workspace_settings.py
|
|
100
101
|
tests/test_agents_connectors_builder.py
|
|
102
|
+
tests/test_allowed_tools_config.py
|
|
101
103
|
tests/test_git_action.py
|
|
102
104
|
tests/test_github_ingest.py
|
|
103
105
|
tests/test_health.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -30,7 +30,7 @@ Rules:
|
|
|
30
30
|
- All data egress through submit_webhook only
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.6.
|
|
33
|
+
__version__ = "1.6.8" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
34
34
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -543,7 +543,7 @@ class AgentDaemon:
|
|
|
543
543
|
validate) is logged and skipped — it must never prevent PERSONAL
|
|
544
544
|
agents from loading.
|
|
545
545
|
"""
|
|
546
|
-
from sdk.agents.manifest import
|
|
546
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
547
547
|
|
|
548
548
|
try:
|
|
549
549
|
rows = await self.client.list_department_agents()
|
|
@@ -555,13 +555,15 @@ class AgentDaemon:
|
|
|
555
555
|
for row in rows:
|
|
556
556
|
if row.get("status") != "active":
|
|
557
557
|
continue
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
558
|
+
# Prompt-based rows: the `manifest` column this used to read was
|
|
559
|
+
# dropped from the table, so every row raised KeyError here.
|
|
560
|
+
manifest = manifest_from_backend_row(row)
|
|
561
|
+
if manifest is None:
|
|
561
562
|
logger.warning(
|
|
562
|
-
"agentd.
|
|
563
|
-
row.get("name"), exc,
|
|
563
|
+
"agentd.department_agent_not_runnable name=%s", row.get("name"),
|
|
564
564
|
)
|
|
565
|
+
continue
|
|
566
|
+
manifests.append(manifest)
|
|
565
567
|
return manifests
|
|
566
568
|
|
|
567
569
|
async def watch_tickets(self) -> None:
|
|
@@ -41,14 +41,60 @@ def _tee_events(log_path, on_event: "EventHandler | None") -> "EventHandler":
|
|
|
41
41
|
EventHandler = Callable[[dict], Awaitable[None]]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def _find_agent(name: str) -> AgentManifest:
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
def _find_agent(name: str) -> AgentManifest | None:
|
|
45
|
+
"""A LOCAL manifest by name (``~/.agentos/agents``), or None."""
|
|
46
|
+
return next((m for m in load_agents() if m.name == name), None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def resolve_agent(name: str, client=None) -> AgentManifest:
|
|
50
|
+
"""Find the agent to run: local manifest → your backend agents → department.
|
|
51
|
+
|
|
52
|
+
The BACKEND is where agents live now — they are authored in the web app, and
|
|
53
|
+
nothing writes ``~/.agentos/agents`` anymore. Resolving only locally meant
|
|
54
|
+
every agent the UI could list was unrunnable ("no local agent named …"), for
|
|
55
|
+
department defaults as much as personal ones. A local manifest still wins when
|
|
56
|
+
present, so a hand-written one keeps overriding its backend namesake.
|
|
57
|
+
|
|
58
|
+
``client`` lets a caller that already has one pass it in rather than having a
|
|
59
|
+
second one built here behind its back.
|
|
60
|
+
|
|
61
|
+
Raises ``LookupError`` naming every place that was checked and why.
|
|
62
|
+
"""
|
|
63
|
+
local = _find_agent(name)
|
|
64
|
+
if local is not None:
|
|
65
|
+
return local
|
|
66
|
+
|
|
67
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
68
|
+
|
|
69
|
+
if client is None:
|
|
70
|
+
from sdk.client import BackendClient
|
|
71
|
+
client = BackendClient.from_config()
|
|
72
|
+
|
|
73
|
+
reasons: list[str] = []
|
|
74
|
+
for label, attr in (("your agents", "list_custom_agents"),
|
|
75
|
+
("department defaults", "list_department_agents")):
|
|
76
|
+
fetch = getattr(client, attr, None)
|
|
77
|
+
if fetch is None:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
rows = await fetch() or []
|
|
81
|
+
except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
|
|
82
|
+
reasons.append(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
|
|
83
|
+
continue
|
|
84
|
+
for row in rows:
|
|
85
|
+
if row.get("name") == name:
|
|
86
|
+
manifest = manifest_from_backend_row(row)
|
|
87
|
+
if manifest is not None:
|
|
88
|
+
return manifest
|
|
89
|
+
reasons.append(
|
|
90
|
+
f"'{name}' is in your {label} but its prompt is empty, so there "
|
|
91
|
+
f"is no instruction to run — add one in the web app"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
detail = "; ".join(reasons) if reasons else (
|
|
95
|
+
"not in ~/.agentos/agents/, your backend agents, or your department defaults"
|
|
96
|
+
)
|
|
97
|
+
raise LookupError(f"no agent named '{name}' — {detail}")
|
|
52
98
|
|
|
53
99
|
|
|
54
100
|
def _render_conversation(history: list[dict] | None, message: str) -> str:
|
|
@@ -87,7 +133,7 @@ async def chat_with_agent(
|
|
|
87
133
|
``files_changed``, ``error``). ``claude_code``/``model`` engines run through
|
|
88
134
|
the local Claude executor; ``shell`` runs the composed prompt as a command.
|
|
89
135
|
"""
|
|
90
|
-
manifest =
|
|
136
|
+
manifest = await resolve_agent(name)
|
|
91
137
|
|
|
92
138
|
skills = load_skills()
|
|
93
139
|
skill_text = resolve_skill_text(manifest, skills)
|
|
@@ -164,7 +210,7 @@ async def run_agent(
|
|
|
164
210
|
``{{variable}}`` is left unfilled — the UI gates the Run button on this, and
|
|
165
211
|
this is the server-side backstop.
|
|
166
212
|
"""
|
|
167
|
-
manifest =
|
|
213
|
+
manifest = await resolve_agent(name)
|
|
168
214
|
|
|
169
215
|
skills = load_skills()
|
|
170
216
|
skill_text = resolve_skill_text(manifest, skills)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""claude_code engine -- a thin adapter from an agent manifest onto ``sdk.local_exec.run_claude_local``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from sdk import config
|
|
10
|
+
from sdk.local_exec import run_claude_local
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sdk.agents.manifest import AgentManifest
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("sdk.agents.engines.claude_code")
|
|
16
|
+
|
|
17
|
+
# Same shape as sdk.local_exec.EventHandler -- kept local so this module doesn't
|
|
18
|
+
# need to reach into local_exec for a type alias.
|
|
19
|
+
EventHandler = Callable[[dict], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
# Directory every agent without a real code workspace runs in.
|
|
22
|
+
AGENT_RUNS_DIR = Path.home() / ".agentos" / "runs"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ensure_agent_workspace(agent_name: str) -> Path:
|
|
26
|
+
"""The working directory for ``agent_name``, with its Claude settings in place.
|
|
27
|
+
|
|
28
|
+
Returns ``~/.agentos/runs/<agent>`` and makes sure
|
|
29
|
+
``<that>/.claude/settings.json`` grants the configured tools
|
|
30
|
+
(``config.get_allowed_tools()``) under ``permissions.allow``.
|
|
31
|
+
|
|
32
|
+
Why write it at all when ``run_claude_local`` already passes
|
|
33
|
+
``--allowedTools``: that flag covers the headless run, but the run directory IS
|
|
34
|
+
a Claude Code project, so anything reading project settings — a resumed
|
|
35
|
+
session, a nested/interactive invocation, a hook — sees the same grants instead
|
|
36
|
+
of falling back to prompting in a place where nobody can answer.
|
|
37
|
+
|
|
38
|
+
MERGES rather than overwrites: existing keys and any extra entries already in
|
|
39
|
+
``permissions.allow`` are preserved, so a hand-added rule survives, and a tool
|
|
40
|
+
added to the configured list later shows up on the next run. Best-effort — a
|
|
41
|
+
write failure is logged and the run proceeds on ``--allowedTools`` alone.
|
|
42
|
+
|
|
43
|
+
Applies to CUSTOM and DEPARTMENT agents alike: both arrive as prompt-based
|
|
44
|
+
backend rows and both run through here.
|
|
45
|
+
"""
|
|
46
|
+
workspace = AGENT_RUNS_DIR / agent_name
|
|
47
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
settings_path = workspace / ".claude" / "settings.json"
|
|
50
|
+
try:
|
|
51
|
+
settings: dict = {}
|
|
52
|
+
if settings_path.exists():
|
|
53
|
+
try:
|
|
54
|
+
loaded = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
55
|
+
if isinstance(loaded, dict):
|
|
56
|
+
settings = loaded
|
|
57
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
58
|
+
# A corrupt file silently disables ALL settings from it, so replace
|
|
59
|
+
# it rather than leave the agent running with none.
|
|
60
|
+
logger.warning("rewriting unparseable %s: %s", settings_path, exc)
|
|
61
|
+
|
|
62
|
+
permissions = settings.get("permissions")
|
|
63
|
+
if not isinstance(permissions, dict):
|
|
64
|
+
permissions = {}
|
|
65
|
+
allow = [t for t in (permissions.get("allow") or []) if isinstance(t, str)]
|
|
66
|
+
for tool in config.get_allowed_tools():
|
|
67
|
+
if tool not in allow:
|
|
68
|
+
allow.append(tool)
|
|
69
|
+
permissions["allow"] = allow
|
|
70
|
+
settings["permissions"] = permissions
|
|
71
|
+
|
|
72
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
74
|
+
except OSError as exc:
|
|
75
|
+
logger.warning("could not write %s: %s", settings_path, exc)
|
|
76
|
+
|
|
77
|
+
return workspace
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def run(
|
|
81
|
+
manifest: "AgentManifest",
|
|
82
|
+
skill_text: str,
|
|
83
|
+
mcp_config: dict[str, Any] | None,
|
|
84
|
+
project_path: str | None = None,
|
|
85
|
+
on_event: EventHandler | None = None,
|
|
86
|
+
extra_env: dict[str, str] | None = None,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Run a custom agent's manifest through the local ``claude`` CLI.
|
|
89
|
+
|
|
90
|
+
This does not reimplement any of ``run_claude_local``'s subprocess handling,
|
|
91
|
+
stream-json parsing, or crucial-hook logic -- it only assembles the prompt
|
|
92
|
+
and a working directory, then delegates.
|
|
93
|
+
|
|
94
|
+
``skill_text`` is expected to already be the resolved skill body (see
|
|
95
|
+
``sdk.agents.manifest.resolve_skill_text``), concatenated by the caller
|
|
96
|
+
where needed. To be safe, the final prompt is (re-)assembled here from
|
|
97
|
+
``manifest.prompt`` plus, when ``skill_text`` is truthy, two newlines and
|
|
98
|
+
the skill body.
|
|
99
|
+
"""
|
|
100
|
+
full_prompt = manifest.prompt
|
|
101
|
+
if skill_text:
|
|
102
|
+
full_prompt = f"{full_prompt}\n\n{skill_text}"
|
|
103
|
+
|
|
104
|
+
if project_path is None:
|
|
105
|
+
# Most custom agents (e.g. "summarise my PRs daily") have no real code
|
|
106
|
+
# workspace -- Claude Code still needs a valid current-working-directory
|
|
107
|
+
# to run in, so fall back to a per-agent scratch directory.
|
|
108
|
+
project_path = str(ensure_agent_workspace(manifest.name))
|
|
109
|
+
|
|
110
|
+
# mcp_config: accepted here for forward compatibility only. Wiring an
|
|
111
|
+
# --mcp-config flag into run_claude_local's subprocess command is a later
|
|
112
|
+
# follow-up, once run_claude_local itself grows an mcp_config parameter --
|
|
113
|
+
# this module does not attempt to work around that in the meantime, so the
|
|
114
|
+
# argument is currently unused.
|
|
115
|
+
|
|
116
|
+
return await run_claude_local(
|
|
117
|
+
prompt=full_prompt,
|
|
118
|
+
project_path=project_path,
|
|
119
|
+
allowed_tools=manifest.allowed_tools or None,
|
|
120
|
+
on_event=on_event,
|
|
121
|
+
extra_env=extra_env,
|
|
122
|
+
)
|
|
@@ -23,8 +23,14 @@ INTEGRATIONS_DIR = Path.home() / ".agentos" / "integrations"
|
|
|
23
23
|
# Full-string match: ${ENV_VAR_NAME} — no inline secrets allowed in integration env.
|
|
24
24
|
_ENV_PLACEHOLDER_RE = re.compile(r"^\$\{[A-Za-z_][A-Za-z0-9_]*\}$")
|
|
25
25
|
|
|
26
|
-
# Agent names
|
|
27
|
-
|
|
26
|
+
# Agent names. Agents are authored in the WEB APP now, where names are free text
|
|
27
|
+
# — "AI Research Agent", "Jira & Confluence Project Management Agent". A rule of
|
|
28
|
+
# `[A-Za-z0-9_-]+` rejected every one of them, so a backend agent could not be
|
|
29
|
+
# materialized into a manifest and therefore could not be run at all. Spaces and
|
|
30
|
+
# ordinary punctuation are allowed; what stays banned is anything that could
|
|
31
|
+
# escape a path or break a log line, since the name is used to derive filenames
|
|
32
|
+
# (see agents/secrets.py, varstore.py, run_log.py — all of which sanitize too).
|
|
33
|
+
_AGENT_NAME_RE = re.compile(r"^[^\x00-\x1f/\\<>:\"|?*]+$")
|
|
28
34
|
|
|
29
35
|
# Flags an inline secret like `token: abc123...` in free-text fields while
|
|
30
36
|
# allowing the ${ENV_VAR} placeholder form. Mirrors the backend
|
|
@@ -210,6 +216,57 @@ def assert_secret_free(obj: object, _where: str = "payload") -> None:
|
|
|
210
216
|
)
|
|
211
217
|
|
|
212
218
|
|
|
219
|
+
def manifest_from_backend_row(row: dict) -> AgentManifest | None:
|
|
220
|
+
"""Materialize a runnable manifest from a backend prompt-based ``agents`` row.
|
|
221
|
+
|
|
222
|
+
Agents are authored in the web app, so the row — not a local YAML file — is
|
|
223
|
+
what the engine has to run. The row shape (docs/DATABASE_SCHEMA.md §4) is
|
|
224
|
+
``{name, status, prompt: {system_prompt, variables:[…]}, skills:[{name,
|
|
225
|
+
description, …}]}``; there is no ``engine``/``trigger``/``manifest`` column
|
|
226
|
+
anymore, so those are supplied here:
|
|
227
|
+
|
|
228
|
+
* ``engine="claude_code"`` — the only engine a web-app agent targets.
|
|
229
|
+
* ``trigger=manual`` — the row carries no trigger; scheduled and
|
|
230
|
+
ticket-label dispatch stay a local-manifest feature.
|
|
231
|
+
|
|
232
|
+
Each attached skill's description is folded into the prompt rather than left
|
|
233
|
+
in ``manifest.skills``: skill names on the row refer to backend ``skills``
|
|
234
|
+
rows, which ``resolve_skill_text`` (local ``~/.agentos/skills``) would drop as
|
|
235
|
+
dangling references, silently losing the instruction text.
|
|
236
|
+
|
|
237
|
+
Returns ``None`` if the row cannot make a valid manifest (no name, or an empty
|
|
238
|
+
/ too-short prompt), so a malformed row is skipped rather than raising.
|
|
239
|
+
"""
|
|
240
|
+
name = str((row or {}).get("name") or "").strip()
|
|
241
|
+
if not name:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
prompt_obj = row.get("prompt") or {}
|
|
245
|
+
if not isinstance(prompt_obj, dict):
|
|
246
|
+
prompt_obj = {}
|
|
247
|
+
prompt = str(prompt_obj.get("system_prompt") or "").strip()
|
|
248
|
+
|
|
249
|
+
for skill in row.get("skills") or []:
|
|
250
|
+
if not isinstance(skill, dict):
|
|
251
|
+
continue
|
|
252
|
+
s_name = str(skill.get("name") or "").strip()
|
|
253
|
+
s_body = str(skill.get("description") or "").strip()
|
|
254
|
+
if s_name and s_body:
|
|
255
|
+
prompt = f"{prompt}\n\n## Skill: {s_name}\n\n{s_body}"
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
return AgentManifest(
|
|
259
|
+
name=name,
|
|
260
|
+
description=prompt[:200],
|
|
261
|
+
engine="claude_code",
|
|
262
|
+
trigger=TriggerSpec(type="manual"),
|
|
263
|
+
prompt=prompt,
|
|
264
|
+
)
|
|
265
|
+
except ValidationError as exc:
|
|
266
|
+
logger.warning("backend agent '%s' is not runnable: %s", name, exc)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
213
270
|
def load_agents(dir: Path = AGENTS_DIR) -> list[AgentManifest]:
|
|
214
271
|
"""Load every agent manifest under `dir`.
|
|
215
272
|
|
|
@@ -34,7 +34,15 @@ SECRETS_DIR = Path.home() / ".agentos" / "agent_secrets"
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _path(agent_name: str) -> Path:
|
|
37
|
-
|
|
37
|
+
"""Store file for an agent's keys.
|
|
38
|
+
|
|
39
|
+
The name is sanitized because agent names are free text from the web app now
|
|
40
|
+
("Jira & Confluence Project Management Agent") — an unsanitized name
|
|
41
|
+
containing a path separator would resolve outside SECRETS_DIR. Mirrors
|
|
42
|
+
``varstore._path`` so the two stores key the same way.
|
|
43
|
+
"""
|
|
44
|
+
safe = "".join(c for c in (agent_name or "") if c.isalnum() or c in "-_") or "agent"
|
|
45
|
+
return SECRETS_DIR / f"{safe}.json.enc"
|
|
38
46
|
|
|
39
47
|
|
|
40
48
|
def load_secrets(agent_name: str) -> dict[str, str]:
|
|
@@ -25,15 +25,19 @@ async def resolve_manifest(
|
|
|
25
25
|
) -> "AgentManifest | None":
|
|
26
26
|
"""Find the agent manifest to run, by ``scope``.
|
|
27
27
|
|
|
28
|
-
* ``custom`` (default) — a personal agent from the local manifests
|
|
29
|
-
(``~/.agentos/agents``).
|
|
30
28
|
* ``department`` — an ACTIVE department default pulled from the backend
|
|
31
|
-
(``list_department_agents``)
|
|
32
|
-
|
|
29
|
+
(``list_department_agents``) and materialized read-only (never written back).
|
|
30
|
+
* ``custom`` (default) — a local manifest, then the caller's own backend
|
|
31
|
+
agents, then their department defaults, via ``chat.resolve_agent``.
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
Backend rows are prompt-based (``prompt.system_prompt`` + attached skills);
|
|
34
|
+
the ``manifest`` column this used to read no longer exists on the table, so
|
|
35
|
+
reading it raised ``KeyError`` and made every department agent unrunnable.
|
|
36
|
+
|
|
37
|
+
Returns None when no matching agent is found, so the caller reports it.
|
|
35
38
|
"""
|
|
36
|
-
from sdk.agents.
|
|
39
|
+
from sdk.agents.chat import resolve_agent
|
|
40
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
37
41
|
|
|
38
42
|
name = (name or "").strip()
|
|
39
43
|
if scope == "department":
|
|
@@ -43,16 +47,13 @@ async def resolve_manifest(
|
|
|
43
47
|
return None
|
|
44
48
|
for row in rows:
|
|
45
49
|
if row.get("name") == name and row.get("status") == "active":
|
|
46
|
-
|
|
47
|
-
return AgentManifest(**row["manifest"])
|
|
48
|
-
except Exception: # noqa: BLE001 — invalid manifest → not runnable
|
|
49
|
-
return None
|
|
50
|
+
return manifest_from_backend_row(row)
|
|
50
51
|
return None
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
try:
|
|
54
|
+
return await resolve_agent(name, client)
|
|
55
|
+
except LookupError:
|
|
56
|
+
return None
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
async def run_on_ticket(
|
|
@@ -333,35 +333,27 @@ def _agent_run_ticket(name: str, flags: list[str]) -> None:
|
|
|
333
333
|
|
|
334
334
|
|
|
335
335
|
def _agent_run(name: str) -> None:
|
|
336
|
-
|
|
336
|
+
"""`agentos agent run NAME` — dispatch one run locally.
|
|
337
|
+
|
|
338
|
+
Resolution order is shared with the UI and `agent chat` (see
|
|
339
|
+
``sdk.agents.chat.resolve_agent``): a local manifest wins, then the caller's
|
|
340
|
+
own backend agents, then their department defaults. It used to check local
|
|
341
|
+
manifests and department rows only — and read a `manifest` column the
|
|
342
|
+
prompt-based `agents` table no longer has — so nothing authored in the web app
|
|
343
|
+
could be run.
|
|
344
|
+
"""
|
|
345
|
+
from sdk.agents.chat import resolve_agent
|
|
337
346
|
from sdk.client import BackendClient
|
|
338
347
|
from sdk import agentd
|
|
339
348
|
|
|
340
349
|
client = BackendClient.from_config()
|
|
341
|
-
|
|
342
|
-
matched = next((m for m in load_agents() if m.name == name), None)
|
|
343
|
-
if matched is not None:
|
|
344
|
-
asyncio.run(agentd._dispatch_agent_run(matched, client))
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
# Not a local personal agent -- check this user's department defaults
|
|
348
|
-
# (read-only, pulled from the backend; see AgentDaemon._load_department_agents).
|
|
349
350
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
except Exception as exc: # noqa: BLE001
|
|
355
|
-
print(f"error: could not fetch department agents ({exc})")
|
|
356
|
-
sys.exit(1)
|
|
357
|
-
|
|
358
|
-
if dept_row is None:
|
|
359
|
-
print(f"error: no agent named '{name}' (checked ~/.agentos/agents/ and "
|
|
360
|
-
f"this user's department default agents)")
|
|
351
|
+
manifest = asyncio.run(resolve_agent(name, client))
|
|
352
|
+
except LookupError as exc:
|
|
353
|
+
print(f"error: {exc}")
|
|
361
354
|
sys.exit(1)
|
|
362
355
|
|
|
363
|
-
|
|
364
|
-
asyncio.run(agentd._dispatch_agent_run(dept_manifest, client))
|
|
356
|
+
asyncio.run(agentd._dispatch_agent_run(manifest, client))
|
|
365
357
|
|
|
366
358
|
|
|
367
359
|
def _agent_logs(name: str) -> None:
|
|
@@ -156,20 +156,29 @@ def _dotenv_value(key: str) -> str:
|
|
|
156
156
|
_DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148"]
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def
|
|
160
|
-
"""Split
|
|
159
|
+
def _parse_token_list(raw: str) -> list[str]:
|
|
160
|
+
"""Split a comma/space-separated list, tolerant of JSON-ish syntax.
|
|
161
161
|
|
|
162
|
-
Accepts ``"
|
|
162
|
+
Accepts ``"a, b"``, ``a b``, or ``["a","b"]``. De-duplicates, order-preserving.
|
|
163
|
+
Used for every list-shaped setting (IP allowlist, tool allowlist).
|
|
163
164
|
"""
|
|
164
165
|
raw = (raw or "").strip().strip("[]")
|
|
165
166
|
out: list[str] = []
|
|
166
167
|
for token in re.split(r"[,\s]+", raw):
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
out.append(
|
|
168
|
+
item = token.strip().strip('"').strip("'")
|
|
169
|
+
if item and item not in out:
|
|
170
|
+
out.append(item)
|
|
170
171
|
return out
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
def _parse_ip_list(raw: str) -> list[str]:
|
|
175
|
+
"""Split an IP allowlist string, tolerant of JSON-ish list syntax.
|
|
176
|
+
|
|
177
|
+
Accepts ``"1.2.3.4, 5.6.7.8"``, ``1.2.3.4 5.6.7.8``, or ``["1.2.3.4","5.6.7.8"]``.
|
|
178
|
+
"""
|
|
179
|
+
return _parse_token_list(raw)
|
|
180
|
+
|
|
181
|
+
|
|
173
182
|
def get_allowed_ips() -> list[str]:
|
|
174
183
|
"""Client IPs permitted to reach the ``agentos-ui`` API (``/api/ui/*``).
|
|
175
184
|
|
|
@@ -201,6 +210,55 @@ def get_allowed_ips() -> list[str]:
|
|
|
201
210
|
return out
|
|
202
211
|
|
|
203
212
|
|
|
213
|
+
# Built-in baseline of tools a headless agent run may use. Kept here (not in
|
|
214
|
+
# local_exec) so it is one configurable setting rather than a literal buried in the
|
|
215
|
+
# spawn code: the set WILL grow, and a new tool must be addable without editing
|
|
216
|
+
# and re-releasing the SDK — see get_allowed_tools().
|
|
217
|
+
_DEFAULT_ALLOWED_TOOLS: list[str] = [
|
|
218
|
+
"Read", "Write", "Edit", "Glob", "Grep", "Bash", "Skill", "WebSearch", "WebFetch",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_allowed_tools() -> list[str]:
|
|
223
|
+
"""Tools a headless ``claude -p`` agent run is allowed to use.
|
|
224
|
+
|
|
225
|
+
A headless run cannot answer a permission prompt, so anything missing from
|
|
226
|
+
this list is silently UNAVAILABLE to the agent — which is how a research agent
|
|
227
|
+
ended up reporting it could not search the web. The baseline therefore covers
|
|
228
|
+
the file, shell, skill, and web tools, and everything else is configuration.
|
|
229
|
+
|
|
230
|
+
Sources, merged additively (all optional), each accepting a comma/space-separated
|
|
231
|
+
or bracketed list:
|
|
232
|
+
- the built-in baseline ``_DEFAULT_ALLOWED_TOOLS``
|
|
233
|
+
- ``.env`` key ``AGENTOS_ALLOWED_TOOLS`` (``~/.agentos/.env`` or CWD ``.env``)
|
|
234
|
+
- process env ``AGENTOS_ALLOWED_TOOLS``
|
|
235
|
+
- config.toml key ``allowed_tools``
|
|
236
|
+
|
|
237
|
+
Additive, like ``get_allowed_ips``: naming a tool ANYWHERE adds it, so granting
|
|
238
|
+
a newly-shipped Claude Code tool is a config line on the box, not a code change
|
|
239
|
+
and a release. Read at spawn time, so an edit applies to the next run without
|
|
240
|
+
restarting the daemon or the UI server. MCP/plugin tools are NOT listed here —
|
|
241
|
+
they are discovered per-machine at spawn time (see
|
|
242
|
+
``local_exec._discover_mcp_allow_entries``).
|
|
243
|
+
|
|
244
|
+
To run with a REDUCED set, pass ``allowed_tools`` explicitly to
|
|
245
|
+
``local_exec.run_claude_local`` (an agent manifest's ``allowed_tools`` does
|
|
246
|
+
exactly that) — that overrides this list rather than extending it.
|
|
247
|
+
"""
|
|
248
|
+
sources = [
|
|
249
|
+
",".join(_DEFAULT_ALLOWED_TOOLS),
|
|
250
|
+
_dotenv_value("AGENTOS_ALLOWED_TOOLS"),
|
|
251
|
+
os.environ.get("AGENTOS_ALLOWED_TOOLS", ""),
|
|
252
|
+
_load_raw().get("allowed_tools", ""),
|
|
253
|
+
]
|
|
254
|
+
out: list[str] = []
|
|
255
|
+
for src in sources:
|
|
256
|
+
for tool in _parse_token_list(src):
|
|
257
|
+
if tool not in out:
|
|
258
|
+
out.append(tool)
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
204
262
|
class PermanentAllowlistEntry(ValueError):
|
|
205
263
|
"""Raised when trying to disable an IP that is permanently whitelisted via ``.env``."""
|
|
206
264
|
|
|
@@ -24,7 +24,7 @@ from dataclasses import dataclass, field
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Awaitable, Callable
|
|
26
26
|
|
|
27
|
-
from sdk.local_exec import
|
|
27
|
+
from sdk.local_exec import run_claude_local
|
|
28
28
|
|
|
29
29
|
from .bootstrap import ensure_agent_browser
|
|
30
30
|
from .browser import BrowserError, diff_baseline, render_screenshot
|
|
@@ -67,9 +67,10 @@ async def _default_coder(project_path: str, correction_prompt: str) -> None:
|
|
|
67
67
|
"unrelated code.\n\n"
|
|
68
68
|
f"{correction_prompt}"
|
|
69
69
|
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
# allowed_tools omitted on purpose: run_claude_local then uses the configured
|
|
71
|
+
# allowlist (config.get_allowed_tools), so this run picks up any tool the box
|
|
72
|
+
# grants instead of a snapshot taken at import.
|
|
73
|
+
result = await run_claude_local(prompt, project_path)
|
|
73
74
|
if not result.get("ok"):
|
|
74
75
|
raise RuntimeError(f"correction run failed: {result.get('error')}")
|
|
75
76
|
|
|
@@ -28,15 +28,22 @@ from datetime import datetime, timedelta
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any, Awaitable, Callable
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
31
|
+
from sdk import config # tool allowlist lives there (config.get_allowed_tools)
|
|
32
|
+
|
|
33
|
+
# The tool allowlist is a SETTING, not a literal here: see
|
|
34
|
+
# ``sdk.config.get_allowed_tools`` for the baseline and the ways to extend it
|
|
35
|
+
# (.env / process env ``AGENTOS_ALLOWED_TOOLS`` / config.toml ``allowed_tools``).
|
|
36
|
+
# It is read at spawn time inside ``run_claude_local`` so a change applies to the
|
|
37
|
+
# next run without a restart, and so granting a newly-shipped Claude Code tool
|
|
38
|
+
# never needs an SDK edit and release.
|
|
39
|
+
#
|
|
40
|
+
# Bash is in the baseline so the agent runs ALL its commands locally — git clone,
|
|
41
|
+
# install, build, test, and `gh pr create` for PRs — on the user's machine, not the
|
|
42
|
+
# server. ``Skill`` lets it invoke installed plugin/user Skills (e.g. the Figma
|
|
43
|
+
# skills), which DO load in headless ``-p`` runs even when a plugin's MCP server
|
|
44
|
+
# can't authenticate there. MCP/plugin *tools* are not configured there either —
|
|
45
|
+
# they are discovered per-machine and appended at spawn time; see
|
|
46
|
+
# ``_discover_mcp_allow_entries`` / ``_local_mcp_servers`` below.
|
|
40
47
|
|
|
41
48
|
# Run limits. The OLD design capped TOTAL runtime at 30 min, which killed long
|
|
42
49
|
# but healthy runs (big builds, many steps) mid-work. Instead we use an IDLE
|
|
@@ -445,7 +452,8 @@ async def run_claude_local(
|
|
|
445
452
|
Spawns ``claude -p <prompt> --output-format stream-json …`` with the working
|
|
446
453
|
directory set to the project so all edits land on the real local files.
|
|
447
454
|
``--permission-mode acceptEdits`` lets file edits apply unattended (no
|
|
448
|
-
interactive prompt)
|
|
455
|
+
interactive prompt). The tools enabled come from ``config.get_allowed_tools()``
|
|
456
|
+
unless ``allowed_tools`` is given, which replaces that list.
|
|
449
457
|
|
|
450
458
|
``on_event`` (optional, async) is called for each streamed JSON event — used
|
|
451
459
|
by the sidecar to forward live progress (tool calls, assistant text) to the
|
|
@@ -477,7 +485,11 @@ async def run_claude_local(
|
|
|
477
485
|
path = Path(project_path).expanduser()
|
|
478
486
|
path.mkdir(parents=True, exist_ok=True)
|
|
479
487
|
|
|
480
|
-
|
|
488
|
+
# Read the allowlist per spawn (not at import) so a config/.env change takes
|
|
489
|
+
# effect on the next run — the UI server and agentd are long-lived processes.
|
|
490
|
+
# An explicit `allowed_tools` (e.g. a manifest's) REPLACES it, so an agent can
|
|
491
|
+
# still be restricted to fewer tools than the configured set.
|
|
492
|
+
tools = list(allowed_tools or config.get_allowed_tools())
|
|
481
493
|
# Append per-server MCP allow-entries so installed+authenticated plugins/MCP
|
|
482
494
|
# servers are usable in this headless run. Discovered once and cached; no-ops
|
|
483
495
|
# (adds nothing) when no servers are configured or discovery fails, so a machine
|