gobby 0.2.6__py3-none-any.whl → 0.2.7__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +45 -2
- gobby/hooks/hook_manager.py +2 -2
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +2 -0
- gobby/mcp_proxy/registries.py +1 -4
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -343
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/skills/parser.py +30 -2
- gobby/storage/migrations.py +159 -372
- gobby/storage/sessions.py +43 -7
- gobby/storage/skills.py +37 -4
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +80 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +94 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/agents/sandbox.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sandbox Configuration Models.
|
|
3
|
+
|
|
4
|
+
This module defines configuration models for sandbox/isolation settings
|
|
5
|
+
when spawning agents. The actual sandboxing is handled by each CLI's
|
|
6
|
+
built-in sandbox implementation - Gobby just passes the right flags.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SandboxConfig(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Configuration for sandbox/isolation when spawning agents.
|
|
18
|
+
|
|
19
|
+
This is opt-in - by default sandboxing is disabled to preserve
|
|
20
|
+
existing behavior. When enabled, the appropriate CLI flags are
|
|
21
|
+
passed to enable the CLI's built-in sandbox.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
enabled: Whether to enable sandboxing. Default False.
|
|
25
|
+
mode: Sandbox strictness level.
|
|
26
|
+
- "permissive": Allow more operations (easier debugging)
|
|
27
|
+
- "restrictive": Stricter isolation (more secure)
|
|
28
|
+
allow_network: Whether to allow network access (except localhost:60887
|
|
29
|
+
which is always allowed for Gobby daemon communication).
|
|
30
|
+
extra_read_paths: Additional paths to allow read access.
|
|
31
|
+
extra_write_paths: Additional paths to allow write access
|
|
32
|
+
(worktree paths are always allowed).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
enabled: bool = False
|
|
36
|
+
mode: Literal["permissive", "restrictive"] = "permissive"
|
|
37
|
+
allow_network: bool = True
|
|
38
|
+
extra_read_paths: list[str] = Field(default_factory=list)
|
|
39
|
+
extra_write_paths: list[str] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResolvedSandboxPaths(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
Resolved paths and settings for sandbox execution.
|
|
45
|
+
|
|
46
|
+
This is the computed result after resolving a SandboxConfig against
|
|
47
|
+
the actual workspace and daemon configuration. It contains the concrete
|
|
48
|
+
paths and settings that will be passed to CLI sandbox flags.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
workspace_path: The primary workspace/worktree path for the agent.
|
|
52
|
+
gobby_daemon_port: Port where Gobby daemon is running (for network allowlist).
|
|
53
|
+
read_paths: All paths the sandbox should allow read access to.
|
|
54
|
+
write_paths: All paths the sandbox should allow write access to.
|
|
55
|
+
allow_external_network: Whether to allow network access beyond localhost.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
workspace_path: str
|
|
59
|
+
gobby_daemon_port: int = 60887
|
|
60
|
+
read_paths: list[str]
|
|
61
|
+
write_paths: list[str]
|
|
62
|
+
allow_external_network: bool
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SandboxResolver(ABC):
|
|
66
|
+
"""
|
|
67
|
+
Abstract base class for CLI-specific sandbox configuration resolution.
|
|
68
|
+
|
|
69
|
+
Each CLI (Claude Code, Codex, Gemini) has different mechanisms for
|
|
70
|
+
enabling sandboxing. Subclasses implement the resolve() method to
|
|
71
|
+
convert a SandboxConfig and ResolvedSandboxPaths into CLI-specific
|
|
72
|
+
arguments and environment variables.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def cli_name(self) -> str:
|
|
78
|
+
"""Return the name of the CLI this resolver handles."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def resolve(
|
|
83
|
+
self, config: SandboxConfig, paths: ResolvedSandboxPaths
|
|
84
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
85
|
+
"""
|
|
86
|
+
Resolve sandbox configuration to CLI-specific args and env vars.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: The sandbox configuration from the agent definition.
|
|
90
|
+
paths: The resolved paths for the sandbox environment.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A tuple of (cli_args, env_vars) where:
|
|
94
|
+
- cli_args: List of command-line arguments to pass to the CLI
|
|
95
|
+
- env_vars: Dict of environment variables to set
|
|
96
|
+
"""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ClaudeSandboxResolver(SandboxResolver):
|
|
101
|
+
"""
|
|
102
|
+
Sandbox resolver for Claude Code CLI.
|
|
103
|
+
|
|
104
|
+
Claude Code uses --settings with a JSON object containing sandbox config.
|
|
105
|
+
See: https://code.claude.com/docs/en/sandboxing
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def cli_name(self) -> str:
|
|
110
|
+
return "claude"
|
|
111
|
+
|
|
112
|
+
def resolve(
|
|
113
|
+
self, config: SandboxConfig, paths: ResolvedSandboxPaths
|
|
114
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
115
|
+
if not config.enabled:
|
|
116
|
+
return ([], {})
|
|
117
|
+
|
|
118
|
+
import json
|
|
119
|
+
|
|
120
|
+
# Build settings JSON for Claude Code
|
|
121
|
+
settings = {
|
|
122
|
+
"sandbox": {
|
|
123
|
+
"enabled": True,
|
|
124
|
+
"autoAllowBashIfSandboxed": True,
|
|
125
|
+
# Network config - allow localhost for Gobby daemon
|
|
126
|
+
"network": {
|
|
127
|
+
"allowLocalBinding": True,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (["--settings", json.dumps(settings)], {})
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CodexSandboxResolver(SandboxResolver):
|
|
136
|
+
"""
|
|
137
|
+
Sandbox resolver for OpenAI Codex CLI.
|
|
138
|
+
|
|
139
|
+
Codex uses --sandbox flag with mode (read-only, workspace-write, danger-full-access)
|
|
140
|
+
and --add-dir for additional writable paths.
|
|
141
|
+
See: https://developers.openai.com/codex/cli/reference/
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def cli_name(self) -> str:
|
|
146
|
+
return "codex"
|
|
147
|
+
|
|
148
|
+
def resolve(
|
|
149
|
+
self, config: SandboxConfig, paths: ResolvedSandboxPaths
|
|
150
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
151
|
+
if not config.enabled:
|
|
152
|
+
return ([], {})
|
|
153
|
+
|
|
154
|
+
args: list[str] = []
|
|
155
|
+
|
|
156
|
+
# Sandbox mode
|
|
157
|
+
if config.mode == "restrictive":
|
|
158
|
+
args.extend(["--sandbox", "read-only"])
|
|
159
|
+
else:
|
|
160
|
+
args.extend(["--sandbox", "workspace-write"])
|
|
161
|
+
|
|
162
|
+
# Add extra write paths (workspace is implicit in workspace-write mode)
|
|
163
|
+
for path in paths.write_paths:
|
|
164
|
+
if path != paths.workspace_path:
|
|
165
|
+
args.extend(["--add-dir", path])
|
|
166
|
+
|
|
167
|
+
return (args, {})
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class GeminiSandboxResolver(SandboxResolver):
|
|
171
|
+
"""
|
|
172
|
+
Sandbox resolver for Google Gemini CLI.
|
|
173
|
+
|
|
174
|
+
Gemini uses -s/--sandbox flag and SEATBELT_PROFILE env var for macOS.
|
|
175
|
+
See: https://geminicli.com/docs/cli/sandbox/
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def cli_name(self) -> str:
|
|
180
|
+
return "gemini"
|
|
181
|
+
|
|
182
|
+
def resolve(
|
|
183
|
+
self, config: SandboxConfig, paths: ResolvedSandboxPaths
|
|
184
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
185
|
+
if not config.enabled:
|
|
186
|
+
return ([], {})
|
|
187
|
+
|
|
188
|
+
args = ["-s"]
|
|
189
|
+
env: dict[str, str] = {}
|
|
190
|
+
|
|
191
|
+
# Set SEATBELT_PROFILE based on mode (macOS)
|
|
192
|
+
if config.mode == "restrictive":
|
|
193
|
+
env["SEATBELT_PROFILE"] = "restrictive-closed"
|
|
194
|
+
else:
|
|
195
|
+
env["SEATBELT_PROFILE"] = "permissive-open"
|
|
196
|
+
|
|
197
|
+
return (args, env)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_sandbox_resolver(cli: str) -> SandboxResolver:
|
|
201
|
+
"""
|
|
202
|
+
Factory function to get the appropriate sandbox resolver for a CLI.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
cli: The CLI name ("claude", "codex", or "gemini")
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The appropriate SandboxResolver subclass instance.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ValueError: If the CLI is not recognized.
|
|
212
|
+
"""
|
|
213
|
+
resolvers: dict[str, type[SandboxResolver]] = {
|
|
214
|
+
"claude": ClaudeSandboxResolver,
|
|
215
|
+
"codex": CodexSandboxResolver,
|
|
216
|
+
"gemini": GeminiSandboxResolver,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if cli not in resolvers:
|
|
220
|
+
raise ValueError(f"Unknown CLI: {cli}. Must be one of: {list(resolvers.keys())}")
|
|
221
|
+
|
|
222
|
+
return resolvers[cli]()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def compute_sandbox_paths(
|
|
226
|
+
config: SandboxConfig,
|
|
227
|
+
workspace_path: str,
|
|
228
|
+
gobby_daemon_port: int = 60887,
|
|
229
|
+
) -> ResolvedSandboxPaths:
|
|
230
|
+
"""
|
|
231
|
+
Compute resolved sandbox paths from a SandboxConfig.
|
|
232
|
+
|
|
233
|
+
This helper function combines the workspace path with extra paths
|
|
234
|
+
from the config to produce the final ResolvedSandboxPaths.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
config: The sandbox configuration.
|
|
238
|
+
workspace_path: The primary workspace/worktree path.
|
|
239
|
+
gobby_daemon_port: Port where Gobby daemon is running.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
ResolvedSandboxPaths with all paths computed.
|
|
243
|
+
"""
|
|
244
|
+
# Start with workspace in write paths
|
|
245
|
+
write_paths = [workspace_path]
|
|
246
|
+
|
|
247
|
+
# Add extra write paths
|
|
248
|
+
for path in config.extra_write_paths:
|
|
249
|
+
if path not in write_paths:
|
|
250
|
+
write_paths.append(path)
|
|
251
|
+
|
|
252
|
+
# Collect read paths
|
|
253
|
+
read_paths = list(config.extra_read_paths)
|
|
254
|
+
|
|
255
|
+
return ResolvedSandboxPaths(
|
|
256
|
+
workspace_path=workspace_path,
|
|
257
|
+
gobby_daemon_port=gobby_daemon_port,
|
|
258
|
+
read_paths=read_paths,
|
|
259
|
+
write_paths=write_paths,
|
|
260
|
+
allow_external_network=config.allow_network,
|
|
261
|
+
)
|
gobby/agents/spawn.py
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
"""Terminal spawning for agent execution.
|
|
1
|
+
"""Terminal spawning for agent execution.
|
|
2
|
+
|
|
3
|
+
This module provides the TerminalSpawner orchestrator and PreparedSpawn helpers
|
|
4
|
+
for spawning CLI agents in terminal windows.
|
|
5
|
+
|
|
6
|
+
Implementation is split across submodules:
|
|
7
|
+
- spawners/prompt_manager.py: Prompt file creation and cleanup
|
|
8
|
+
- spawners/command_builder.py: CLI command construction
|
|
9
|
+
- spawners/: Platform-specific terminal spawners
|
|
10
|
+
"""
|
|
2
11
|
|
|
3
12
|
from __future__ import annotations
|
|
4
13
|
|
|
5
|
-
import atexit
|
|
6
14
|
import logging
|
|
7
|
-
import os
|
|
8
|
-
import tempfile
|
|
9
15
|
from dataclasses import dataclass
|
|
10
16
|
from pathlib import Path
|
|
11
17
|
|
|
12
18
|
from gobby.agents.constants import get_terminal_env_vars
|
|
19
|
+
from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
|
|
13
20
|
from gobby.agents.session import ChildSessionConfig, ChildSessionManager
|
|
14
21
|
from gobby.agents.spawners import (
|
|
22
|
+
MAX_ENV_PROMPT_LENGTH,
|
|
15
23
|
AlacrittySpawner,
|
|
16
24
|
CmdSpawner,
|
|
17
25
|
EmbeddedSpawner,
|
|
@@ -30,6 +38,11 @@ from gobby.agents.spawners import (
|
|
|
30
38
|
TmuxSpawner,
|
|
31
39
|
WindowsTerminalSpawner,
|
|
32
40
|
WSLSpawner,
|
|
41
|
+
build_cli_command,
|
|
42
|
+
build_codex_command_with_resume,
|
|
43
|
+
build_gemini_command_with_resume,
|
|
44
|
+
create_prompt_file,
|
|
45
|
+
read_prompt_from_env,
|
|
33
46
|
)
|
|
34
47
|
from gobby.agents.spawners.base import EmbeddedPTYResult, HeadlessResult
|
|
35
48
|
from gobby.agents.tty_config import get_tty_config
|
|
@@ -71,161 +84,12 @@ __all__ = [
|
|
|
71
84
|
"build_cli_command",
|
|
72
85
|
"build_gemini_command_with_resume",
|
|
73
86
|
"build_codex_command_with_resume",
|
|
87
|
+
"create_prompt_file",
|
|
74
88
|
"MAX_ENV_PROMPT_LENGTH",
|
|
75
89
|
]
|
|
76
90
|
|
|
77
|
-
# Maximum prompt length to pass via environment variable
|
|
78
|
-
# Longer prompts will be written to a temp file
|
|
79
|
-
MAX_ENV_PROMPT_LENGTH = 4096
|
|
80
|
-
|
|
81
91
|
logger = logging.getLogger(__name__)
|
|
82
92
|
|
|
83
|
-
# Module-level set for tracking prompt files to clean up on exit
|
|
84
|
-
# This avoids registering a new atexit handler for each prompt file
|
|
85
|
-
_prompt_files_to_cleanup: set[Path] = set()
|
|
86
|
-
_atexit_registered = False
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _cleanup_all_prompt_files() -> None:
|
|
90
|
-
"""Clean up all tracked prompt files on process exit."""
|
|
91
|
-
for prompt_path in list(_prompt_files_to_cleanup):
|
|
92
|
-
try:
|
|
93
|
-
if prompt_path.exists():
|
|
94
|
-
prompt_path.unlink()
|
|
95
|
-
except OSError:
|
|
96
|
-
pass
|
|
97
|
-
_prompt_files_to_cleanup.clear()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _create_prompt_file(prompt: str, session_id: str) -> str:
|
|
101
|
-
"""
|
|
102
|
-
Create a prompt file with secure permissions.
|
|
103
|
-
|
|
104
|
-
The file is created in the system temp directory with restrictive
|
|
105
|
-
permissions (owner read/write only) and tracked for cleanup on exit.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
prompt: The prompt content to write
|
|
109
|
-
session_id: Session ID for naming the file
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Path to the created temp file
|
|
113
|
-
"""
|
|
114
|
-
global _atexit_registered
|
|
115
|
-
|
|
116
|
-
# Create temp directory with restrictive permissions
|
|
117
|
-
temp_dir = Path(tempfile.gettempdir()) / "gobby-prompts"
|
|
118
|
-
temp_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
119
|
-
|
|
120
|
-
# Create the prompt file path
|
|
121
|
-
prompt_path = temp_dir / f"prompt-{session_id}.txt"
|
|
122
|
-
|
|
123
|
-
# Write with secure permissions atomically - create with mode 0o600 from the start
|
|
124
|
-
# This avoids the TOCTOU window between write_text and chmod
|
|
125
|
-
fd = os.open(str(prompt_path), os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
|
|
126
|
-
try:
|
|
127
|
-
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
128
|
-
f.write(prompt)
|
|
129
|
-
f.flush()
|
|
130
|
-
os.fsync(f.fileno())
|
|
131
|
-
except Exception:
|
|
132
|
-
# fd is closed by fdopen, but if fdopen fails we need to close it
|
|
133
|
-
try:
|
|
134
|
-
os.close(fd)
|
|
135
|
-
except OSError:
|
|
136
|
-
pass
|
|
137
|
-
raise
|
|
138
|
-
|
|
139
|
-
# Track for cleanup
|
|
140
|
-
_prompt_files_to_cleanup.add(prompt_path)
|
|
141
|
-
|
|
142
|
-
# Register cleanup handler once
|
|
143
|
-
if not _atexit_registered:
|
|
144
|
-
atexit.register(_cleanup_all_prompt_files)
|
|
145
|
-
_atexit_registered = True
|
|
146
|
-
|
|
147
|
-
logger.debug(f"Created secure prompt file: {prompt_path}")
|
|
148
|
-
return str(prompt_path)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def build_cli_command(
|
|
152
|
-
cli: str,
|
|
153
|
-
prompt: str | None = None,
|
|
154
|
-
session_id: str | None = None,
|
|
155
|
-
auto_approve: bool = False,
|
|
156
|
-
working_directory: str | None = None,
|
|
157
|
-
mode: str = "terminal",
|
|
158
|
-
) -> list[str]:
|
|
159
|
-
"""
|
|
160
|
-
Build the CLI command with proper prompt passing and permission flags.
|
|
161
|
-
|
|
162
|
-
Each CLI has different syntax for passing prompts and handling permissions:
|
|
163
|
-
|
|
164
|
-
Claude Code:
|
|
165
|
-
- claude --session-id <uuid> --dangerously-skip-permissions [prompt]
|
|
166
|
-
- Use --dangerously-skip-permissions for autonomous subagent operation
|
|
167
|
-
|
|
168
|
-
Gemini CLI:
|
|
169
|
-
- gemini -i "prompt" (interactive mode with initial prompt)
|
|
170
|
-
- gemini --approval-mode yolo -i "prompt" (YOLO + interactive)
|
|
171
|
-
- gemini "prompt" (one-shot non-interactive for headless)
|
|
172
|
-
|
|
173
|
-
Codex CLI:
|
|
174
|
-
- codex --full-auto -C <dir> [PROMPT]
|
|
175
|
-
- Or: codex -c 'sandbox_permissions=["disk-full-read-access"]' -a never [PROMPT]
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
cli: CLI name (claude, gemini, codex)
|
|
179
|
-
prompt: Optional prompt to pass
|
|
180
|
-
session_id: Optional session ID (used by Claude CLI)
|
|
181
|
-
auto_approve: If True, add flags to auto-approve actions/permissions
|
|
182
|
-
working_directory: Optional working directory (used by Codex -C flag)
|
|
183
|
-
mode: Execution mode - "terminal" (interactive) or "headless" (non-interactive)
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
Command list for subprocess execution
|
|
187
|
-
"""
|
|
188
|
-
command = [cli]
|
|
189
|
-
|
|
190
|
-
if cli == "claude":
|
|
191
|
-
# Claude CLI flags
|
|
192
|
-
if session_id:
|
|
193
|
-
command.extend(["--session-id", session_id])
|
|
194
|
-
if auto_approve:
|
|
195
|
-
# Skip all permission prompts for autonomous subagent operation
|
|
196
|
-
command.append("--dangerously-skip-permissions")
|
|
197
|
-
if prompt:
|
|
198
|
-
# Use -p (print mode) for non-interactive execution.
|
|
199
|
-
# NOTE: Print mode bypasses hooks - headless spawner manually tracks status.
|
|
200
|
-
command.append("-p")
|
|
201
|
-
|
|
202
|
-
elif cli == "gemini":
|
|
203
|
-
# Gemini CLI flags
|
|
204
|
-
if auto_approve:
|
|
205
|
-
command.extend(["--approval-mode", "yolo"])
|
|
206
|
-
# For terminal mode, use -i (prompt-interactive) to execute prompt and stay interactive
|
|
207
|
-
# For headless mode, use positional prompt for one-shot execution
|
|
208
|
-
if prompt:
|
|
209
|
-
if mode == "terminal":
|
|
210
|
-
command.extend(["-i", prompt])
|
|
211
|
-
return command # Don't add prompt again as positional
|
|
212
|
-
# else: fall through to add as positional for headless
|
|
213
|
-
|
|
214
|
-
elif cli == "codex":
|
|
215
|
-
# Codex CLI flags
|
|
216
|
-
if auto_approve:
|
|
217
|
-
# --full-auto: low-friction sandboxed automatic execution
|
|
218
|
-
command.append("--full-auto")
|
|
219
|
-
if working_directory:
|
|
220
|
-
command.extend(["-C", working_directory])
|
|
221
|
-
|
|
222
|
-
# All three CLIs accept prompt as positional argument (must come last)
|
|
223
|
-
# For Gemini terminal mode, this is skipped (handled above with -i flag)
|
|
224
|
-
if prompt:
|
|
225
|
-
command.append(prompt)
|
|
226
|
-
|
|
227
|
-
return command
|
|
228
|
-
|
|
229
93
|
|
|
230
94
|
class TerminalSpawner:
|
|
231
95
|
"""
|
|
@@ -369,6 +233,7 @@ class TerminalSpawner:
|
|
|
369
233
|
max_agent_depth: int = 3,
|
|
370
234
|
terminal: TerminalType | str = TerminalType.AUTO,
|
|
371
235
|
prompt: str | None = None,
|
|
236
|
+
sandbox_config: SandboxConfig | None = None,
|
|
372
237
|
) -> SpawnResult:
|
|
373
238
|
"""
|
|
374
239
|
Spawn a CLI agent in a new terminal with Gobby environment variables.
|
|
@@ -385,10 +250,22 @@ class TerminalSpawner:
|
|
|
385
250
|
max_agent_depth: Maximum allowed depth
|
|
386
251
|
terminal: Terminal type or "auto"
|
|
387
252
|
prompt: Optional initial prompt
|
|
253
|
+
sandbox_config: Optional sandbox configuration
|
|
388
254
|
|
|
389
255
|
Returns:
|
|
390
256
|
SpawnResult with success status
|
|
391
257
|
"""
|
|
258
|
+
# Resolve sandbox configuration if enabled
|
|
259
|
+
sandbox_args: list[str] | None = None
|
|
260
|
+
sandbox_env: dict[str, str] = {}
|
|
261
|
+
|
|
262
|
+
if sandbox_config and sandbox_config.enabled:
|
|
263
|
+
# Compute sandbox paths based on cwd (workspace)
|
|
264
|
+
resolved_paths = compute_sandbox_paths(sandbox_config, str(cwd))
|
|
265
|
+
# Get CLI-specific resolver and generate args/env
|
|
266
|
+
resolver = get_sandbox_resolver(cli)
|
|
267
|
+
sandbox_args, sandbox_env = resolver.resolve(sandbox_config, resolved_paths)
|
|
268
|
+
|
|
392
269
|
# Build command with prompt as CLI argument and auto-approve for autonomous work
|
|
393
270
|
command = build_cli_command(
|
|
394
271
|
cli,
|
|
@@ -397,6 +274,7 @@ class TerminalSpawner:
|
|
|
397
274
|
auto_approve=True, # Subagents need to work autonomously
|
|
398
275
|
working_directory=str(cwd) if cli == "codex" else None,
|
|
399
276
|
mode="terminal", # Interactive terminal mode
|
|
277
|
+
sandbox_args=sandbox_args,
|
|
400
278
|
)
|
|
401
279
|
|
|
402
280
|
# Handle prompt for environment variables (backup for hooks/context)
|
|
@@ -422,6 +300,10 @@ class TerminalSpawner:
|
|
|
422
300
|
prompt_file=prompt_file,
|
|
423
301
|
)
|
|
424
302
|
|
|
303
|
+
# Merge sandbox environment variables if present
|
|
304
|
+
if sandbox_env:
|
|
305
|
+
env.update(sandbox_env)
|
|
306
|
+
|
|
425
307
|
# Set title (avoid colons/parentheses which Ghostty interprets as config syntax)
|
|
426
308
|
title = f"gobby-{cli}-d{agent_depth}"
|
|
427
309
|
|
|
@@ -437,8 +319,8 @@ class TerminalSpawner:
|
|
|
437
319
|
"""
|
|
438
320
|
Write prompt to a temp file for passing to spawned agent.
|
|
439
321
|
|
|
440
|
-
Delegates to the
|
|
441
|
-
|
|
322
|
+
Delegates to the create_prompt_file helper which handles
|
|
323
|
+
secure permissions and cleanup tracking.
|
|
442
324
|
|
|
443
325
|
Args:
|
|
444
326
|
prompt: The prompt content
|
|
@@ -447,7 +329,7 @@ class TerminalSpawner:
|
|
|
447
329
|
Returns:
|
|
448
330
|
Path to the created temp file
|
|
449
331
|
"""
|
|
450
|
-
return
|
|
332
|
+
return create_prompt_file(prompt, session_id)
|
|
451
333
|
|
|
452
334
|
|
|
453
335
|
@dataclass
|
|
@@ -545,7 +427,7 @@ def prepare_terminal_spawn(
|
|
|
545
427
|
prompt_env = prompt
|
|
546
428
|
else:
|
|
547
429
|
# Write to temp file with secure permissions
|
|
548
|
-
prompt_file =
|
|
430
|
+
prompt_file = create_prompt_file(prompt, child_session.id)
|
|
549
431
|
|
|
550
432
|
# Build environment variables
|
|
551
433
|
env_vars = get_terminal_env_vars(
|
|
@@ -571,34 +453,6 @@ def prepare_terminal_spawn(
|
|
|
571
453
|
)
|
|
572
454
|
|
|
573
455
|
|
|
574
|
-
def read_prompt_from_env() -> str | None:
|
|
575
|
-
"""
|
|
576
|
-
Read initial prompt from environment variables.
|
|
577
|
-
|
|
578
|
-
Checks GOBBY_PROMPT_FILE first (for long prompts),
|
|
579
|
-
then falls back to GOBBY_PROMPT (for short prompts).
|
|
580
|
-
|
|
581
|
-
Returns:
|
|
582
|
-
Prompt string or None if not set
|
|
583
|
-
"""
|
|
584
|
-
from gobby.agents.constants import GOBBY_PROMPT, GOBBY_PROMPT_FILE
|
|
585
|
-
|
|
586
|
-
# Check for prompt file first
|
|
587
|
-
prompt_file = os.environ.get(GOBBY_PROMPT_FILE)
|
|
588
|
-
if prompt_file:
|
|
589
|
-
try:
|
|
590
|
-
prompt_path = Path(prompt_file)
|
|
591
|
-
if prompt_path.exists():
|
|
592
|
-
return prompt_path.read_text(encoding="utf-8")
|
|
593
|
-
else:
|
|
594
|
-
logger.warning(f"Prompt file not found: {prompt_file}")
|
|
595
|
-
except Exception as e:
|
|
596
|
-
logger.error(f"Error reading prompt file: {e}")
|
|
597
|
-
|
|
598
|
-
# Fall back to inline prompt
|
|
599
|
-
return os.environ.get(GOBBY_PROMPT)
|
|
600
|
-
|
|
601
|
-
|
|
602
456
|
async def prepare_gemini_spawn_with_preflight(
|
|
603
457
|
session_manager: ChildSessionManager,
|
|
604
458
|
parent_session_id: str,
|
|
@@ -677,7 +531,7 @@ async def prepare_gemini_spawn_with_preflight(
|
|
|
677
531
|
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
678
532
|
prompt_env = prompt
|
|
679
533
|
else:
|
|
680
|
-
prompt_file =
|
|
534
|
+
prompt_file = create_prompt_file(prompt, child_session.id)
|
|
681
535
|
|
|
682
536
|
# Build environment variables
|
|
683
537
|
env_vars = get_terminal_env_vars(
|
|
@@ -708,57 +562,6 @@ async def prepare_gemini_spawn_with_preflight(
|
|
|
708
562
|
)
|
|
709
563
|
|
|
710
564
|
|
|
711
|
-
def build_gemini_command_with_resume(
|
|
712
|
-
gemini_external_id: str,
|
|
713
|
-
prompt: str | None = None,
|
|
714
|
-
auto_approve: bool = False,
|
|
715
|
-
gobby_session_id: str | None = None,
|
|
716
|
-
) -> list[str]:
|
|
717
|
-
"""
|
|
718
|
-
Build Gemini CLI command with session resume.
|
|
719
|
-
|
|
720
|
-
Uses -r flag to resume a preflight-captured session, with session context
|
|
721
|
-
injected into the initial prompt.
|
|
722
|
-
|
|
723
|
-
Args:
|
|
724
|
-
gemini_external_id: Gemini's session_id from preflight capture
|
|
725
|
-
prompt: Optional user prompt
|
|
726
|
-
auto_approve: If True, add --approval-mode yolo
|
|
727
|
-
gobby_session_id: Gobby session ID to inject into context
|
|
728
|
-
|
|
729
|
-
Returns:
|
|
730
|
-
Command list for subprocess execution
|
|
731
|
-
"""
|
|
732
|
-
command = ["gemini"]
|
|
733
|
-
|
|
734
|
-
# Resume the preflight session
|
|
735
|
-
command.extend(["-r", gemini_external_id])
|
|
736
|
-
|
|
737
|
-
if auto_approve:
|
|
738
|
-
command.extend(["--approval-mode", "yolo"])
|
|
739
|
-
|
|
740
|
-
# Build prompt with session context
|
|
741
|
-
if gobby_session_id:
|
|
742
|
-
context_prefix = (
|
|
743
|
-
f"Your Gobby session_id is: {gobby_session_id}\n"
|
|
744
|
-
f"Use this when calling Gobby MCP tools.\n\n"
|
|
745
|
-
)
|
|
746
|
-
full_prompt = context_prefix + (prompt or "")
|
|
747
|
-
else:
|
|
748
|
-
full_prompt = prompt or ""
|
|
749
|
-
|
|
750
|
-
# Use -i for interactive mode with initial prompt
|
|
751
|
-
if full_prompt:
|
|
752
|
-
command.extend(["-i", full_prompt])
|
|
753
|
-
|
|
754
|
-
return command
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
# =============================================================================
|
|
758
|
-
# Codex Preflight Capture
|
|
759
|
-
# =============================================================================
|
|
760
|
-
|
|
761
|
-
|
|
762
565
|
async def prepare_codex_spawn_with_preflight(
|
|
763
566
|
session_manager: ChildSessionManager,
|
|
764
567
|
parent_session_id: str,
|
|
@@ -837,7 +640,7 @@ async def prepare_codex_spawn_with_preflight(
|
|
|
837
640
|
if len(prompt) <= MAX_ENV_PROMPT_LENGTH:
|
|
838
641
|
prompt_env = prompt
|
|
839
642
|
else:
|
|
840
|
-
prompt_file =
|
|
643
|
+
prompt_file = create_prompt_file(prompt, child_session.id)
|
|
841
644
|
|
|
842
645
|
# Build environment variables
|
|
843
646
|
env_vars = get_terminal_env_vars(
|
|
@@ -866,51 +669,3 @@ async def prepare_codex_spawn_with_preflight(
|
|
|
866
669
|
agent_depth=child_session.agent_depth,
|
|
867
670
|
env_vars=env_vars,
|
|
868
671
|
)
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
def build_codex_command_with_resume(
|
|
872
|
-
codex_external_id: str,
|
|
873
|
-
prompt: str | None = None,
|
|
874
|
-
auto_approve: bool = False,
|
|
875
|
-
gobby_session_id: str | None = None,
|
|
876
|
-
working_directory: str | None = None,
|
|
877
|
-
) -> list[str]:
|
|
878
|
-
"""
|
|
879
|
-
Build Codex CLI command with session resume.
|
|
880
|
-
|
|
881
|
-
Uses `codex resume {session_id}` to resume a preflight-captured session,
|
|
882
|
-
with session context injected into the prompt.
|
|
883
|
-
|
|
884
|
-
Args:
|
|
885
|
-
codex_external_id: Codex's session_id from preflight capture
|
|
886
|
-
prompt: Optional user prompt
|
|
887
|
-
auto_approve: If True, add --full-auto flag
|
|
888
|
-
gobby_session_id: Gobby session ID to inject into context
|
|
889
|
-
working_directory: Optional working directory override
|
|
890
|
-
|
|
891
|
-
Returns:
|
|
892
|
-
Command list for subprocess execution
|
|
893
|
-
"""
|
|
894
|
-
command = ["codex", "resume", codex_external_id]
|
|
895
|
-
|
|
896
|
-
if auto_approve:
|
|
897
|
-
command.append("--full-auto")
|
|
898
|
-
|
|
899
|
-
if working_directory:
|
|
900
|
-
command.extend(["-C", working_directory])
|
|
901
|
-
|
|
902
|
-
# Build prompt with session context
|
|
903
|
-
if gobby_session_id:
|
|
904
|
-
context_prefix = (
|
|
905
|
-
f"Your Gobby session_id is: {gobby_session_id}\n"
|
|
906
|
-
f"Use this when calling Gobby MCP tools.\n\n"
|
|
907
|
-
)
|
|
908
|
-
full_prompt = context_prefix + (prompt or "")
|
|
909
|
-
else:
|
|
910
|
-
full_prompt = prompt or ""
|
|
911
|
-
|
|
912
|
-
# Prompt is a positional argument after session_id
|
|
913
|
-
if full_prompt:
|
|
914
|
-
command.append(full_prompt)
|
|
915
|
-
|
|
916
|
-
return command
|