codex-autorunner 0.1.0__py3-none-any.whl → 0.1.2__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.
- codex_autorunner/agents/__init__.py +21 -0
- codex_autorunner/agents/base.py +62 -0
- codex_autorunner/agents/codex/__init__.py +5 -0
- codex_autorunner/agents/codex/harness.py +220 -0
- codex_autorunner/agents/execution/policy.py +292 -0
- codex_autorunner/agents/factory.py +52 -0
- codex_autorunner/agents/opencode/__init__.py +18 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +553 -0
- codex_autorunner/agents/opencode/events.py +67 -0
- codex_autorunner/agents/opencode/harness.py +263 -0
- codex_autorunner/agents/opencode/logging.py +209 -0
- codex_autorunner/agents/opencode/run_prompt.py +260 -0
- codex_autorunner/agents/opencode/runtime.py +1491 -0
- codex_autorunner/agents/opencode/supervisor.py +520 -0
- codex_autorunner/agents/orchestrator.py +358 -0
- codex_autorunner/agents/registry.py +130 -0
- codex_autorunner/agents/types.py +42 -0
- codex_autorunner/bootstrap.py +32 -23
- codex_autorunner/cli.py +389 -116
- codex_autorunner/codex_cli.py +5 -0
- codex_autorunner/core/about_car.py +20 -7
- codex_autorunner/core/app_server_events.py +192 -0
- codex_autorunner/core/app_server_logging.py +205 -0
- codex_autorunner/core/app_server_prompts.py +378 -0
- codex_autorunner/core/app_server_threads.py +195 -0
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/config.py +888 -82
- codex_autorunner/core/doc_chat.py +1248 -349
- codex_autorunner/core/docs.py +83 -6
- codex_autorunner/core/engine.py +2037 -120
- codex_autorunner/core/exceptions.py +60 -0
- codex_autorunner/core/git_utils.py +28 -0
- codex_autorunner/core/hub.py +232 -99
- codex_autorunner/core/locks.py +230 -3
- codex_autorunner/core/logging_utils.py +14 -7
- codex_autorunner/core/optional_dependencies.py +7 -4
- codex_autorunner/core/patch_utils.py +224 -0
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +4 -31
- codex_autorunner/core/request_context.py +18 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +164 -0
- codex_autorunner/core/run_index.py +217 -0
- codex_autorunner/core/runner_controller.py +56 -1
- codex_autorunner/core/runner_process.py +27 -1
- codex_autorunner/core/snapshot.py +136 -132
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +379 -58
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +15 -1
- codex_autorunner/core/usage.py +760 -69
- codex_autorunner/core/utils.py +167 -5
- codex_autorunner/discovery.py +115 -30
- codex_autorunner/integrations/app_server/client.py +161 -73
- codex_autorunner/integrations/app_server/env.py +110 -0
- codex_autorunner/integrations/app_server/supervisor.py +1 -0
- codex_autorunner/integrations/github/chatops.py +268 -0
- codex_autorunner/integrations/github/pr_flow.py +1314 -0
- codex_autorunner/integrations/github/service.py +269 -1
- codex_autorunner/integrations/telegram/adapter.py +395 -41
- codex_autorunner/integrations/telegram/config.py +161 -2
- codex_autorunner/integrations/telegram/constants.py +17 -0
- codex_autorunner/integrations/telegram/dispatch.py +82 -40
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +27 -2
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +27 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2599 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1412 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +2229 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +1243 -3422
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +10 -12
- codex_autorunner/integrations/telegram/handlers/messages.py +398 -46
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +79 -1
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +129 -121
- codex_autorunner/integrations/telegram/notifications.py +422 -26
- codex_autorunner/integrations/telegram/outbox.py +77 -56
- codex_autorunner/integrations/telegram/overflow.py +194 -0
- codex_autorunner/integrations/telegram/progress_stream.py +237 -0
- codex_autorunner/integrations/telegram/runtime.py +33 -20
- codex_autorunner/integrations/telegram/service.py +427 -34
- codex_autorunner/integrations/telegram/state.py +1225 -330
- codex_autorunner/integrations/telegram/transport.py +89 -9
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +48 -1
- codex_autorunner/routes/__init__.py +14 -0
- codex_autorunner/routes/agents.py +138 -0
- codex_autorunner/routes/app_server.py +132 -0
- codex_autorunner/routes/base.py +202 -47
- codex_autorunner/routes/docs.py +132 -26
- codex_autorunner/routes/github.py +136 -6
- codex_autorunner/routes/repos.py +76 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/runs.py +250 -0
- codex_autorunner/routes/sessions.py +49 -10
- codex_autorunner/routes/settings.py +169 -0
- codex_autorunner/routes/shared.py +149 -10
- codex_autorunner/routes/system.py +16 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/server.py +0 -7
- codex_autorunner/spec_ingest.py +778 -79
- codex_autorunner/static/agentControls.js +351 -0
- codex_autorunner/static/app.js +85 -78
- codex_autorunner/static/autoRefresh.js +118 -147
- codex_autorunner/static/bootstrap.js +117 -99
- codex_autorunner/static/bus.js +16 -17
- codex_autorunner/static/cache.js +26 -41
- codex_autorunner/static/constants.js +44 -45
- codex_autorunner/static/dashboard.js +723 -717
- codex_autorunner/static/docChatActions.js +287 -0
- codex_autorunner/static/docChatEvents.js +300 -0
- codex_autorunner/static/docChatRender.js +205 -0
- codex_autorunner/static/docChatStream.js +361 -0
- codex_autorunner/static/docs.js +18 -1512
- codex_autorunner/static/docsClipboard.js +69 -0
- codex_autorunner/static/docsCrud.js +257 -0
- codex_autorunner/static/docsDocUpdates.js +62 -0
- codex_autorunner/static/docsDrafts.js +16 -0
- codex_autorunner/static/docsElements.js +69 -0
- codex_autorunner/static/docsInit.js +285 -0
- codex_autorunner/static/docsParse.js +160 -0
- codex_autorunner/static/docsSnapshot.js +87 -0
- codex_autorunner/static/docsSpecIngest.js +263 -0
- codex_autorunner/static/docsState.js +127 -0
- codex_autorunner/static/docsThreadRegistry.js +44 -0
- codex_autorunner/static/docsUi.js +153 -0
- codex_autorunner/static/docsVoice.js +56 -0
- codex_autorunner/static/env.js +29 -79
- codex_autorunner/static/github.js +489 -153
- codex_autorunner/static/hub.js +1235 -1331
- codex_autorunner/static/index.html +407 -49
- codex_autorunner/static/liveUpdates.js +58 -0
- codex_autorunner/static/loader.js +26 -26
- codex_autorunner/static/logs.js +598 -610
- codex_autorunner/static/mobileCompact.js +215 -263
- codex_autorunner/static/review.js +157 -0
- codex_autorunner/static/runs.js +418 -0
- codex_autorunner/static/settings.js +341 -0
- codex_autorunner/static/snapshot.js +104 -96
- codex_autorunner/static/state.js +76 -69
- codex_autorunner/static/styles.css +1905 -436
- codex_autorunner/static/tabs.js +34 -43
- codex_autorunner/static/terminal.js +6 -15
- codex_autorunner/static/terminalManager.js +3532 -3468
- codex_autorunner/static/todoPreview.js +25 -23
- codex_autorunner/static/utils.js +567 -534
- codex_autorunner/static/voice.js +498 -537
- codex_autorunner/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +907 -172
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +54 -18
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +144 -0
- codex_autorunner/web/static_assets.py +57 -0
- codex_autorunner/web/static_refresh.py +86 -0
- {codex_autorunner-0.1.0.dist-info → codex_autorunner-0.1.2.dist-info}/METADATA +17 -8
- codex_autorunner-0.1.2.dist-info/RECORD +222 -0
- {codex_autorunner-0.1.0.dist-info → codex_autorunner-0.1.2.dist-info}/WHEEL +1 -1
- codex_autorunner/static/types.d.ts +0 -8
- codex_autorunner-0.1.0.dist-info/RECORD +0 -147
- {codex_autorunner-0.1.0.dist-info → codex_autorunner-0.1.2.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.0.dist-info → codex_autorunner-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.0.dist-info → codex_autorunner-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Agent harness abstractions."""
|
|
2
|
+
|
|
3
|
+
from .registry import (
|
|
4
|
+
AgentCapability,
|
|
5
|
+
AgentDescriptor,
|
|
6
|
+
get_agent_descriptor,
|
|
7
|
+
get_available_agents,
|
|
8
|
+
get_registered_agents,
|
|
9
|
+
has_capability,
|
|
10
|
+
validate_agent_id,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentCapability",
|
|
15
|
+
"AgentDescriptor",
|
|
16
|
+
"get_registered_agents",
|
|
17
|
+
"get_available_agents",
|
|
18
|
+
"get_agent_descriptor",
|
|
19
|
+
"validate_agent_id",
|
|
20
|
+
"has_capability",
|
|
21
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, AsyncIterator, Optional, Protocol
|
|
5
|
+
|
|
6
|
+
from .types import AgentId, ConversationRef, ModelCatalog, TurnRef
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgentHarness(Protocol):
|
|
10
|
+
agent_id: AgentId
|
|
11
|
+
display_name: str
|
|
12
|
+
|
|
13
|
+
async def ensure_ready(self, workspace_root: Path) -> None: ...
|
|
14
|
+
|
|
15
|
+
async def model_catalog(self, workspace_root: Path) -> ModelCatalog: ...
|
|
16
|
+
|
|
17
|
+
async def new_conversation(
|
|
18
|
+
self, workspace_root: Path, title: Optional[str] = None
|
|
19
|
+
) -> ConversationRef: ...
|
|
20
|
+
|
|
21
|
+
async def list_conversations(
|
|
22
|
+
self, workspace_root: Path
|
|
23
|
+
) -> list[ConversationRef]: ...
|
|
24
|
+
|
|
25
|
+
async def resume_conversation(
|
|
26
|
+
self, workspace_root: Path, conversation_id: str
|
|
27
|
+
) -> ConversationRef: ...
|
|
28
|
+
|
|
29
|
+
async def start_turn(
|
|
30
|
+
self,
|
|
31
|
+
workspace_root: Path,
|
|
32
|
+
conversation_id: str,
|
|
33
|
+
prompt: str,
|
|
34
|
+
model: Optional[str],
|
|
35
|
+
reasoning: Optional[str],
|
|
36
|
+
*,
|
|
37
|
+
approval_mode: Optional[str],
|
|
38
|
+
sandbox_policy: Optional[Any],
|
|
39
|
+
) -> TurnRef: ...
|
|
40
|
+
|
|
41
|
+
async def start_review(
|
|
42
|
+
self,
|
|
43
|
+
workspace_root: Path,
|
|
44
|
+
conversation_id: str,
|
|
45
|
+
prompt: str,
|
|
46
|
+
model: Optional[str],
|
|
47
|
+
reasoning: Optional[str],
|
|
48
|
+
*,
|
|
49
|
+
approval_mode: Optional[str],
|
|
50
|
+
sandbox_policy: Optional[Any],
|
|
51
|
+
) -> TurnRef: ...
|
|
52
|
+
|
|
53
|
+
async def interrupt(
|
|
54
|
+
self, workspace_root: Path, conversation_id: str, turn_id: Optional[str]
|
|
55
|
+
) -> None: ...
|
|
56
|
+
|
|
57
|
+
def stream_events(
|
|
58
|
+
self, workspace_root: Path, conversation_id: str, turn_id: str
|
|
59
|
+
) -> AsyncIterator[str]: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["AgentHarness"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, AsyncIterator, Optional
|
|
5
|
+
|
|
6
|
+
from ...core.app_server_events import AppServerEventBuffer
|
|
7
|
+
from ...integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
8
|
+
from ..base import AgentHarness
|
|
9
|
+
from ..types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
|
|
10
|
+
|
|
11
|
+
_DEFAULT_REASONING_EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _coerce_entries(result: Any, keys: tuple[str, ...]) -> list[dict[str, Any]]:
|
|
15
|
+
if isinstance(result, list):
|
|
16
|
+
return [entry for entry in result if isinstance(entry, dict)]
|
|
17
|
+
if isinstance(result, dict):
|
|
18
|
+
for key in keys:
|
|
19
|
+
value = result.get(key)
|
|
20
|
+
if isinstance(value, list):
|
|
21
|
+
return [entry for entry in value if isinstance(entry, dict)]
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _select_default_model(result: Any, entries: list[dict[str, Any]]) -> str:
|
|
26
|
+
if isinstance(result, dict):
|
|
27
|
+
for key in (
|
|
28
|
+
"defaultModel",
|
|
29
|
+
"default_model",
|
|
30
|
+
"default",
|
|
31
|
+
"model",
|
|
32
|
+
"modelId",
|
|
33
|
+
"model_id",
|
|
34
|
+
):
|
|
35
|
+
value = result.get(key)
|
|
36
|
+
if isinstance(value, str) and value:
|
|
37
|
+
return value
|
|
38
|
+
config = result.get("config")
|
|
39
|
+
if isinstance(config, dict):
|
|
40
|
+
for key in ("defaultModel", "default_model", "model", "modelId"):
|
|
41
|
+
value = config.get(key)
|
|
42
|
+
if isinstance(value, str) and value:
|
|
43
|
+
return value
|
|
44
|
+
for entry in entries:
|
|
45
|
+
if entry.get("default") or entry.get("isDefault"):
|
|
46
|
+
model_id = entry.get("model") or entry.get("id")
|
|
47
|
+
if isinstance(model_id, str) and model_id:
|
|
48
|
+
return model_id
|
|
49
|
+
for entry in entries:
|
|
50
|
+
model_id = entry.get("model") or entry.get("id")
|
|
51
|
+
if isinstance(model_id, str) and model_id:
|
|
52
|
+
return model_id
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _coerce_reasoning_efforts(entry: dict[str, Any]) -> list[str]:
|
|
57
|
+
efforts_raw = entry.get("supportedReasoningEfforts")
|
|
58
|
+
efforts: list[str] = []
|
|
59
|
+
if isinstance(efforts_raw, list):
|
|
60
|
+
for effort in efforts_raw:
|
|
61
|
+
if isinstance(effort, dict):
|
|
62
|
+
value = effort.get("reasoningEffort")
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
efforts.append(value)
|
|
65
|
+
elif isinstance(effort, str):
|
|
66
|
+
efforts.append(effort)
|
|
67
|
+
default_effort = entry.get("defaultReasoningEffort")
|
|
68
|
+
if isinstance(default_effort, str) and default_effort:
|
|
69
|
+
efforts.append(default_effort)
|
|
70
|
+
if not efforts:
|
|
71
|
+
efforts = list(_DEFAULT_REASONING_EFFORTS)
|
|
72
|
+
return list(dict.fromkeys(efforts))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CodexHarness(AgentHarness):
|
|
76
|
+
agent_id: AgentId = AgentId("codex")
|
|
77
|
+
display_name = "Codex"
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
supervisor: WorkspaceAppServerSupervisor,
|
|
82
|
+
events: AppServerEventBuffer,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._supervisor = supervisor
|
|
85
|
+
self._events = events
|
|
86
|
+
|
|
87
|
+
async def ensure_ready(self, workspace_root: Path) -> None:
|
|
88
|
+
await self._supervisor.get_client(workspace_root)
|
|
89
|
+
|
|
90
|
+
async def model_catalog(self, workspace_root: Path) -> ModelCatalog:
|
|
91
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
92
|
+
result = await client.model_list()
|
|
93
|
+
entries = _coerce_entries(result, ("data", "models", "items", "results"))
|
|
94
|
+
models: list[ModelSpec] = []
|
|
95
|
+
for entry in entries:
|
|
96
|
+
model_id = entry.get("model") or entry.get("id")
|
|
97
|
+
if not isinstance(model_id, str) or not model_id:
|
|
98
|
+
continue
|
|
99
|
+
display_name = entry.get("displayName") or entry.get("name") or model_id
|
|
100
|
+
if not isinstance(display_name, str) or not display_name:
|
|
101
|
+
display_name = model_id
|
|
102
|
+
efforts = _coerce_reasoning_efforts(entry)
|
|
103
|
+
models.append(
|
|
104
|
+
ModelSpec(
|
|
105
|
+
id=model_id,
|
|
106
|
+
display_name=display_name,
|
|
107
|
+
supports_reasoning=bool(efforts),
|
|
108
|
+
reasoning_options=efforts,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
default_model = _select_default_model(result, entries)
|
|
112
|
+
if not default_model and models:
|
|
113
|
+
default_model = models[0].id
|
|
114
|
+
return ModelCatalog(default_model=default_model, models=models)
|
|
115
|
+
|
|
116
|
+
async def new_conversation(
|
|
117
|
+
self, workspace_root: Path, title: Optional[str] = None
|
|
118
|
+
) -> ConversationRef:
|
|
119
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
120
|
+
result = await client.thread_start(str(workspace_root))
|
|
121
|
+
thread_id = result.get("id")
|
|
122
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
123
|
+
raise ValueError("Codex app-server did not return a thread id")
|
|
124
|
+
return ConversationRef(agent=self.agent_id, id=thread_id)
|
|
125
|
+
|
|
126
|
+
async def list_conversations(self, workspace_root: Path) -> list[ConversationRef]:
|
|
127
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
128
|
+
result = await client.thread_list()
|
|
129
|
+
entries = _coerce_entries(result, ("threads", "data", "items", "results"))
|
|
130
|
+
conversations: list[ConversationRef] = []
|
|
131
|
+
for entry in entries:
|
|
132
|
+
thread_id = entry.get("id")
|
|
133
|
+
if isinstance(thread_id, str) and thread_id:
|
|
134
|
+
conversations.append(ConversationRef(agent=self.agent_id, id=thread_id))
|
|
135
|
+
return conversations
|
|
136
|
+
|
|
137
|
+
async def resume_conversation(
|
|
138
|
+
self, workspace_root: Path, conversation_id: str
|
|
139
|
+
) -> ConversationRef:
|
|
140
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
141
|
+
result = await client.thread_resume(conversation_id)
|
|
142
|
+
thread_id = result.get("id") or conversation_id
|
|
143
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
144
|
+
thread_id = conversation_id
|
|
145
|
+
return ConversationRef(agent=self.agent_id, id=thread_id)
|
|
146
|
+
|
|
147
|
+
async def start_turn(
|
|
148
|
+
self,
|
|
149
|
+
workspace_root: Path,
|
|
150
|
+
conversation_id: str,
|
|
151
|
+
prompt: str,
|
|
152
|
+
model: Optional[str],
|
|
153
|
+
reasoning: Optional[str],
|
|
154
|
+
*,
|
|
155
|
+
approval_mode: Optional[str],
|
|
156
|
+
sandbox_policy: Optional[Any],
|
|
157
|
+
) -> TurnRef:
|
|
158
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
159
|
+
turn_kwargs: dict[str, Any] = {}
|
|
160
|
+
if model:
|
|
161
|
+
turn_kwargs["model"] = model
|
|
162
|
+
if reasoning:
|
|
163
|
+
turn_kwargs["effort"] = reasoning
|
|
164
|
+
handle = await client.turn_start(
|
|
165
|
+
conversation_id,
|
|
166
|
+
prompt,
|
|
167
|
+
approval_policy=approval_mode,
|
|
168
|
+
sandbox_policy=sandbox_policy,
|
|
169
|
+
**turn_kwargs,
|
|
170
|
+
)
|
|
171
|
+
await self._events.register_turn(handle.thread_id, handle.turn_id)
|
|
172
|
+
return TurnRef(conversation_id=handle.thread_id, turn_id=handle.turn_id)
|
|
173
|
+
|
|
174
|
+
async def start_review(
|
|
175
|
+
self,
|
|
176
|
+
workspace_root: Path,
|
|
177
|
+
conversation_id: str,
|
|
178
|
+
prompt: str,
|
|
179
|
+
model: Optional[str],
|
|
180
|
+
reasoning: Optional[str],
|
|
181
|
+
*,
|
|
182
|
+
approval_mode: Optional[str],
|
|
183
|
+
sandbox_policy: Optional[Any],
|
|
184
|
+
) -> TurnRef:
|
|
185
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
186
|
+
review_kwargs: dict[str, Any] = {}
|
|
187
|
+
if model:
|
|
188
|
+
review_kwargs["model"] = model
|
|
189
|
+
if reasoning:
|
|
190
|
+
review_kwargs["effort"] = reasoning
|
|
191
|
+
instructions = (prompt or "").strip()
|
|
192
|
+
if instructions:
|
|
193
|
+
target = {"type": "custom", "instructions": instructions}
|
|
194
|
+
else:
|
|
195
|
+
target = {"type": "uncommittedChanges"}
|
|
196
|
+
handle = await client.review_start(
|
|
197
|
+
conversation_id,
|
|
198
|
+
target=target,
|
|
199
|
+
approval_policy=approval_mode,
|
|
200
|
+
sandbox_policy=sandbox_policy,
|
|
201
|
+
**review_kwargs,
|
|
202
|
+
)
|
|
203
|
+
await self._events.register_turn(handle.thread_id, handle.turn_id)
|
|
204
|
+
return TurnRef(conversation_id=handle.thread_id, turn_id=handle.turn_id)
|
|
205
|
+
|
|
206
|
+
async def interrupt(
|
|
207
|
+
self, workspace_root: Path, conversation_id: str, turn_id: Optional[str]
|
|
208
|
+
) -> None:
|
|
209
|
+
if not turn_id:
|
|
210
|
+
return
|
|
211
|
+
client = await self._supervisor.get_client(workspace_root)
|
|
212
|
+
await client.turn_interrupt(turn_id, thread_id=conversation_id)
|
|
213
|
+
|
|
214
|
+
def stream_events(
|
|
215
|
+
self, workspace_root: Path, conversation_id: str, turn_id: str
|
|
216
|
+
) -> AsyncIterator[str]:
|
|
217
|
+
return self._events.stream(conversation_id, turn_id)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = ["CodexHarness"]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Centralized approval and sandbox policy mappings for Codex and OpenCode agents."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
# ========================================================================
|
|
7
|
+
# Approval Policies
|
|
8
|
+
# ========================================================================
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApprovalPolicy:
|
|
12
|
+
"""Canonical approval policy values for both Codex and OpenCode."""
|
|
13
|
+
|
|
14
|
+
NEVER = "never"
|
|
15
|
+
ON_FAILURE = "on-failure"
|
|
16
|
+
ON_REQUEST = "on-request"
|
|
17
|
+
UNTRUSTED = "untrusted"
|
|
18
|
+
|
|
19
|
+
ALL_VALUES = {NEVER, ON_FAILURE, ON_REQUEST, UNTRUSTED}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ========================================================================
|
|
23
|
+
# Sandbox Policies (Codex)
|
|
24
|
+
# ========================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SandboxPolicy:
|
|
28
|
+
"""Canonical sandbox policy values for Codex app-server."""
|
|
29
|
+
|
|
30
|
+
DANGER_FULL_ACCESS = "dangerFullAccess"
|
|
31
|
+
READ_ONLY = "readOnly"
|
|
32
|
+
WORKSPACE_WRITE = "workspaceWrite"
|
|
33
|
+
EXTERNAL_SANDBOX = "externalSandbox"
|
|
34
|
+
|
|
35
|
+
ALL_VALUES = {
|
|
36
|
+
DANGER_FULL_ACCESS,
|
|
37
|
+
READ_ONLY,
|
|
38
|
+
WORKSPACE_WRITE,
|
|
39
|
+
EXTERNAL_SANDBOX,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ========================================================================
|
|
44
|
+
# Permission Policies (OpenCode)
|
|
45
|
+
# ========================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PermissionPolicy:
|
|
49
|
+
"""Canonical permission policy values for OpenCode."""
|
|
50
|
+
|
|
51
|
+
ALLOW = "allow"
|
|
52
|
+
DENY = "deny"
|
|
53
|
+
ASK = "ask"
|
|
54
|
+
|
|
55
|
+
ALL_VALUES = {ALLOW, DENY, ASK}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ========================================================================
|
|
59
|
+
# Data Classes
|
|
60
|
+
# ========================================================================
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class SandboxPolicyConfig:
|
|
65
|
+
"""Configuration for Codex sandbox policies."""
|
|
66
|
+
|
|
67
|
+
policy: Union[str, dict[str, Any]]
|
|
68
|
+
"""Either a string policy type or full policy dict with type and options."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class PolicyMapping:
|
|
73
|
+
"""Unified policy mapping for both Codex and OpenCode agents."""
|
|
74
|
+
|
|
75
|
+
approval_policy: str
|
|
76
|
+
sandbox_policy: Union[str, dict[str, Any]]
|
|
77
|
+
permission_policy: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ========================================================================
|
|
81
|
+
# Normalization Functions
|
|
82
|
+
# ========================================================================
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalize_approval_policy(policy: Optional[str]) -> str:
|
|
86
|
+
"""Normalize approval policy to canonical value.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
policy: Approval policy string (case-insensitive, various aliases accepted).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Canonical approval policy value.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If policy is not a recognized value.
|
|
96
|
+
"""
|
|
97
|
+
if policy is None:
|
|
98
|
+
return ApprovalPolicy.NEVER
|
|
99
|
+
|
|
100
|
+
if not isinstance(policy, str):
|
|
101
|
+
raise ValueError(f"Invalid approval policy: {policy!r}")
|
|
102
|
+
|
|
103
|
+
normalized = policy.strip()
|
|
104
|
+
if not normalized:
|
|
105
|
+
raise ValueError(f"Invalid approval policy: {policy!r}")
|
|
106
|
+
|
|
107
|
+
normalized = normalized.lower()
|
|
108
|
+
|
|
109
|
+
# Aliases for never
|
|
110
|
+
if normalized in ("never", "no", "false", "0"):
|
|
111
|
+
return ApprovalPolicy.NEVER
|
|
112
|
+
|
|
113
|
+
# Aliases for on-failure
|
|
114
|
+
if normalized in (
|
|
115
|
+
"on-failure",
|
|
116
|
+
"on_failure",
|
|
117
|
+
"onfailure",
|
|
118
|
+
"fail",
|
|
119
|
+
"failure",
|
|
120
|
+
):
|
|
121
|
+
return ApprovalPolicy.ON_FAILURE
|
|
122
|
+
|
|
123
|
+
# Aliases for on-request
|
|
124
|
+
if normalized in ("on-request", "on_request", "onrequest", "ask", "prompt"):
|
|
125
|
+
return ApprovalPolicy.ON_REQUEST
|
|
126
|
+
|
|
127
|
+
# Aliases for untrusted
|
|
128
|
+
if normalized in (
|
|
129
|
+
"untrusted",
|
|
130
|
+
"unlesstrusted",
|
|
131
|
+
"unless-trusted",
|
|
132
|
+
"unless trusted",
|
|
133
|
+
"auto",
|
|
134
|
+
):
|
|
135
|
+
return ApprovalPolicy.UNTRUSTED
|
|
136
|
+
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Invalid approval policy: {policy!r}. "
|
|
139
|
+
f"Valid values: {', '.join(sorted(ApprovalPolicy.ALL_VALUES))}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def normalize_sandbox_policy(policy: Optional[Any]) -> Union[str, dict[str, Any]]:
|
|
144
|
+
"""Normalize sandbox policy to canonical value.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
policy: Sandbox policy (string or dict with 'type' field).
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Normalized sandbox policy as string or dict.
|
|
151
|
+
"""
|
|
152
|
+
if policy is None:
|
|
153
|
+
return SandboxPolicy.DANGER_FULL_ACCESS
|
|
154
|
+
|
|
155
|
+
# If it's a dict, normalize the type field
|
|
156
|
+
if isinstance(policy, dict):
|
|
157
|
+
policy_value = policy.copy()
|
|
158
|
+
type_value = policy_value.get("type")
|
|
159
|
+
if isinstance(type_value, str):
|
|
160
|
+
policy_value["type"] = normalize_sandbox_policy_type(type_value)
|
|
161
|
+
return policy_value
|
|
162
|
+
|
|
163
|
+
# If it's a string, wrap in dict structure
|
|
164
|
+
if isinstance(policy, str):
|
|
165
|
+
normalized_type = normalize_sandbox_policy_type(policy)
|
|
166
|
+
return {"type": normalized_type}
|
|
167
|
+
|
|
168
|
+
# For other types, convert to string and wrap
|
|
169
|
+
return {"type": SandboxPolicy.DANGER_FULL_ACCESS}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def normalize_sandbox_policy_type(raw: str) -> str:
|
|
173
|
+
"""Normalize sandbox policy type string to canonical value.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
raw: Sandbox policy type string (case-insensitive).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Canonical sandbox policy type.
|
|
180
|
+
"""
|
|
181
|
+
if not raw:
|
|
182
|
+
return SandboxPolicy.DANGER_FULL_ACCESS
|
|
183
|
+
|
|
184
|
+
# Normalize case and remove special characters
|
|
185
|
+
import re
|
|
186
|
+
|
|
187
|
+
cleaned = re.sub(r"[^a-zA-Z0-9]+", "", raw.strip())
|
|
188
|
+
if not cleaned:
|
|
189
|
+
return SandboxPolicy.DANGER_FULL_ACCESS
|
|
190
|
+
|
|
191
|
+
canonical = _SANDBOX_POLICY_CANONICAL.get(cleaned.lower())
|
|
192
|
+
return canonical or raw.strip()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_SANDBOX_POLICY_CANONICAL = {
|
|
196
|
+
"dangerfullaccess": SandboxPolicy.DANGER_FULL_ACCESS,
|
|
197
|
+
"readonly": SandboxPolicy.READ_ONLY,
|
|
198
|
+
"workspacewrite": SandboxPolicy.WORKSPACE_WRITE,
|
|
199
|
+
"externalsandbox": SandboxPolicy.EXTERNAL_SANDBOX,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ========================================================================
|
|
204
|
+
# Mapping Functions
|
|
205
|
+
# ========================================================================
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def map_approval_to_permission(
|
|
209
|
+
approval_policy: Optional[str], *, default: str = PermissionPolicy.ALLOW
|
|
210
|
+
) -> str:
|
|
211
|
+
"""Map approval policy to OpenCode permission policy.
|
|
212
|
+
|
|
213
|
+
This maps Codex-style approval policies to OpenCode-style permission policies.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
approval_policy: Codex approval policy.
|
|
217
|
+
default: Default permission if policy is None or unrecognized.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
OpenCode permission policy (allow/deny/ask).
|
|
221
|
+
"""
|
|
222
|
+
if approval_policy is None:
|
|
223
|
+
return default
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
normalized = normalize_approval_policy(approval_policy)
|
|
227
|
+
except ValueError:
|
|
228
|
+
# Invalid policy, return default
|
|
229
|
+
return default
|
|
230
|
+
|
|
231
|
+
# Direct matches
|
|
232
|
+
if normalized == ApprovalPolicy.NEVER:
|
|
233
|
+
return PermissionPolicy.ALLOW
|
|
234
|
+
if normalized == ApprovalPolicy.ON_FAILURE:
|
|
235
|
+
return PermissionPolicy.ASK
|
|
236
|
+
if normalized == ApprovalPolicy.ON_REQUEST:
|
|
237
|
+
return PermissionPolicy.ASK
|
|
238
|
+
if normalized == ApprovalPolicy.UNTRUSTED:
|
|
239
|
+
return PermissionPolicy.ASK
|
|
240
|
+
|
|
241
|
+
return default
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build_codex_sandbox_policy(
|
|
245
|
+
sandbox_mode: Optional[str],
|
|
246
|
+
*,
|
|
247
|
+
repo_root: Optional[Any] = None,
|
|
248
|
+
network_access: bool = False,
|
|
249
|
+
) -> Union[str, dict[str, Any]]:
|
|
250
|
+
"""Build Codex sandbox policy from mode string.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
sandbox_mode: Sandbox mode string.
|
|
254
|
+
repo_root: Repository root path (for workspaceWrite policy).
|
|
255
|
+
network_access: Whether to allow network access (for workspaceWrite).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Sandbox policy string or dict.
|
|
259
|
+
"""
|
|
260
|
+
if not sandbox_mode:
|
|
261
|
+
return SandboxPolicy.DANGER_FULL_ACCESS
|
|
262
|
+
|
|
263
|
+
normalized_mode = normalize_sandbox_policy_type(sandbox_mode)
|
|
264
|
+
|
|
265
|
+
# workspaceWrite requires dict structure with writableRoots and networkAccess
|
|
266
|
+
if normalized_mode == SandboxPolicy.WORKSPACE_WRITE and repo_root is not None:
|
|
267
|
+
return {
|
|
268
|
+
"type": SandboxPolicy.WORKSPACE_WRITE,
|
|
269
|
+
"writableRoots": [str(repo_root)],
|
|
270
|
+
"networkAccess": network_access,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Other modes can be simple strings
|
|
274
|
+
return normalized_mode
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ========================================================================
|
|
278
|
+
# Exports
|
|
279
|
+
# ========================================================================
|
|
280
|
+
|
|
281
|
+
__all__ = [
|
|
282
|
+
"ApprovalPolicy",
|
|
283
|
+
"SandboxPolicy",
|
|
284
|
+
"PermissionPolicy",
|
|
285
|
+
"SandboxPolicyConfig",
|
|
286
|
+
"PolicyMapping",
|
|
287
|
+
"normalize_approval_policy",
|
|
288
|
+
"normalize_sandbox_policy",
|
|
289
|
+
"normalize_sandbox_policy_type",
|
|
290
|
+
"map_approval_to_permission",
|
|
291
|
+
"build_codex_sandbox_policy",
|
|
292
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, cast
|
|
4
|
+
|
|
5
|
+
from ..core.app_server_events import AppServerEventBuffer
|
|
6
|
+
from ..integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
7
|
+
from .codex.harness import CodexHarness
|
|
8
|
+
from .opencode.harness import OpenCodeHarness
|
|
9
|
+
from .opencode.supervisor import OpenCodeSupervisor
|
|
10
|
+
from .orchestrator import AgentOrchestrator, CodexOrchestrator, OpenCodeOrchestrator
|
|
11
|
+
from .registry import get_agent_descriptor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_orchestrator(
|
|
15
|
+
agent_id: str,
|
|
16
|
+
codex_supervisor: Optional[WorkspaceAppServerSupervisor] = None,
|
|
17
|
+
codex_events: Optional[AppServerEventBuffer] = None,
|
|
18
|
+
opencode_supervisor: Optional[OpenCodeSupervisor] = None,
|
|
19
|
+
) -> AgentOrchestrator:
|
|
20
|
+
descriptor = get_agent_descriptor(agent_id)
|
|
21
|
+
if descriptor is None:
|
|
22
|
+
raise ValueError(f"Unknown agent: {agent_id}")
|
|
23
|
+
|
|
24
|
+
class _AppContext:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
app_server_supervisor: Optional[WorkspaceAppServerSupervisor] = None,
|
|
28
|
+
app_server_events: Optional[AppServerEventBuffer] = None,
|
|
29
|
+
opencode_supervisor: Optional[OpenCodeSupervisor] = None,
|
|
30
|
+
):
|
|
31
|
+
self.app_server_supervisor = app_server_supervisor
|
|
32
|
+
self.app_server_events = app_server_events
|
|
33
|
+
self.opencode_supervisor = opencode_supervisor
|
|
34
|
+
|
|
35
|
+
app_ctx = _AppContext(codex_supervisor, codex_events, opencode_supervisor)
|
|
36
|
+
harness = descriptor.make_harness(app_ctx)
|
|
37
|
+
|
|
38
|
+
if agent_id == "codex":
|
|
39
|
+
if not isinstance(harness, CodexHarness):
|
|
40
|
+
raise RuntimeError(f"Expected CodexHarness but got {type(harness)}")
|
|
41
|
+
return CodexOrchestrator(harness, cast(AppServerEventBuffer, codex_events))
|
|
42
|
+
elif agent_id == "opencode":
|
|
43
|
+
if not isinstance(harness, OpenCodeHarness):
|
|
44
|
+
raise RuntimeError(f"Expected OpenCodeHarness but got {type(harness)}")
|
|
45
|
+
return OpenCodeOrchestrator(harness)
|
|
46
|
+
else:
|
|
47
|
+
raise RuntimeError(f"No orchestrator implementation for agent: {agent_id}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"create_orchestrator",
|
|
52
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""OpenCode harness support."""
|
|
2
|
+
|
|
3
|
+
from .client import OpenCodeClient
|
|
4
|
+
from .events import SSEEvent, parse_sse_lines
|
|
5
|
+
from .harness import OpenCodeHarness
|
|
6
|
+
from .run_prompt import OpenCodeRunConfig, OpenCodeRunResult, run_opencode_prompt
|
|
7
|
+
from .supervisor import OpenCodeSupervisor
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"OpenCodeClient",
|
|
11
|
+
"OpenCodeHarness",
|
|
12
|
+
"OpenCodeRunConfig",
|
|
13
|
+
"OpenCodeRunResult",
|
|
14
|
+
"OpenCodeSupervisor",
|
|
15
|
+
"SSEEvent",
|
|
16
|
+
"parse_sse_lines",
|
|
17
|
+
"run_opencode_prompt",
|
|
18
|
+
]
|