Skip to content

Commit 85a27f1

Browse files
authored
Merge pull request #9 from John-Lin/wip/shell-without-skills
feat: decouple shell mode from shell skills
2 parents 3f6c3dc + 0a52aa5 commit 85a27f1

6 files changed

Lines changed: 229 additions & 96 deletions

File tree

README.md

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ See also: [agentic-telegram-bot](https://github.com/John-Lin/agentic-telegram-bo
99
- Channel @mention and DM support
1010
- Thread-aware conversations (follow-ups stay in the same thread)
1111
- Connects to any MCP server via `servers_config.json`
12-
- Local shell skills via `ShellTool` (opt-in via `SHELL_SKILLS_ENABLED`)
12+
- Optional local shell via `ShellTool`, controlled by `SHELL_ENABLED` and `SHELL_SKILLS_DIR`
1313
- Supports OpenAI and OpenAI-compatible endpoints (including Azure OpenAI v1 API)
1414
- Per-conversation history with automatic truncation (last 10 turns)
1515

@@ -43,8 +43,9 @@ export SLACK_APP_TOKEN=""
4343
export OPENAI_API_KEY=""
4444
export OPENAI_MODEL="gpt-5.4"
4545
46-
# Shell skills (disabled by default)
47-
# export SHELL_SKILLS_ENABLED=1
46+
# Local shell (disabled by default)
47+
# export SHELL_ENABLED=1
48+
# export SHELL_SKILLS_DIR="./skills" # optional; mount skills alongside the shell
4849
```
4950

5051
If you are using Azure OpenAI (v1 API) or another OpenAI-compatible endpoint:
@@ -61,6 +62,32 @@ Optional HTTP proxy for outbound requests:
6162
export HTTP_PROXY=""
6263
```
6364

65+
Optional verbose OpenAI Agents SDK logging:
66+
67+
```
68+
export AGENT_VERBOSE_LOG=1
69+
```
70+
71+
`AGENT_VERBOSE_LOG` is defined by this project: when set to a truthy value, the app
72+
calls the SDK's `enable_verbose_stdout_logging()` helper during startup. Values like
73+
`0`, `false`, `no`, `off` (case-insensitive) are treated as disabled.
74+
75+
The following are environment variables read directly by the OpenAI Agents SDK (not
76+
this project). By default, the SDK does not log model or tool payloads. To include
77+
them temporarily for debugging:
78+
79+
```
80+
export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=0
81+
export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=0
82+
```
83+
84+
These payload logs may contain sensitive data. Re-enable the defaults after debugging:
85+
86+
```
87+
export OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1
88+
export OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1
89+
```
90+
6491
## Agent Instructions
6592

6693
Create an `instructions.md` file in the project root with the agent system prompt:
@@ -122,23 +149,34 @@ For local MCP servers, use `uv --directory`:
122149
uv run bot
123150
```
124151

125-
## Shell Skills (Optional)
152+
## Local Shell (Optional)
126153

127-
The bot can execute local shell commands via skills defined in a `skills/` directory. Each subdirectory containing a `SKILL.md` file is registered as a skill.
154+
The bot can expose a local `ShellTool`. This is **disabled by default**. Enable it with:
128155

129-
When using the Docker image, mount `skills/` at runtime (the image build excludes this directory by default):
130-
131-
```bash
132-
-v /path/to/skills:/app/skills:ro
133156
```
157+
export SHELL_ENABLED=1
158+
```
159+
160+
With just `SHELL_ENABLED=1`, the agent gets bare local shell access with no pre-defined skills.
161+
162+
### Shell Skills (Optional)
134163

135-
This feature is **disabled by default**. To enable it, set:
164+
You can optionally mount a skills directory alongside the shell. Each immediate subdirectory containing a `SKILL.md` file is registered as a skill and exposed to the agent as a hint (skills are advisory metadata — they do **not** sandbox command execution).
136165

137166
```
138-
SHELL_SKILLS_ENABLED=1
167+
export SHELL_ENABLED=1
168+
export SHELL_SKILLS_DIR="./skills"
169+
```
170+
171+
`SHELL_SKILLS_DIR` is ignored unless `SHELL_ENABLED` is set. If the directory is missing or contains no valid skills, the bot falls back to a bare shell and logs a warning.
172+
173+
When using the Docker image, mount your skills directory at runtime (the image build excludes it by default):
174+
175+
```bash
176+
-v /path/to/skills:/app/skills:ro
139177
```
140178

141-
Skills are auto-discovered at startup. The `SKILL.md` file should have YAML frontmatter with `name` and `description` fields:
179+
The `SKILL.md` file should have YAML frontmatter with `name` and `description` fields:
142180

143181
```markdown
144182
---
@@ -160,7 +198,8 @@ docker run -d \
160198
-e SLACK_APP_TOKEN="" \
161199
-e OPENAI_API_KEY="" \
162200
-e OPENAI_MODEL="gpt-5.4" \
163-
-e SHELL_SKILLS_ENABLED=1 \
201+
-e SHELL_ENABLED=1 \
202+
-e SHELL_SKILLS_DIR=/app/skills \
164203
-v /path/to/instructions.md:/app/instructions.md \
165204
-v /path/to/skills:/app/skills:ro \
166205
agentic-slackbot
@@ -175,7 +214,8 @@ docker run -d \
175214
-e SLACK_APP_TOKEN="" \
176215
-e OPENAI_API_KEY="" \
177216
-e OPENAI_MODEL="gpt-5.4" \
178-
-e SHELL_SKILLS_ENABLED=1 \
217+
-e SHELL_ENABLED=1 \
218+
-e SHELL_SKILLS_DIR=/app/skills \
179219
-v /path/to/instructions.md:/app/instructions.md \
180220
-v /path/to/skills:/app/skills:ro \
181221
-v /path/to/servers_config.json:/app/servers_config.json \

bot/agent.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
from agents.tracing import set_tracing_disabled
2121
from openai import AsyncOpenAI
2222

23+
from .config import env_flag
24+
2325
INSTRUCTIONS_FILE = Path("instructions.md")
2426

2527
MAX_TURNS = 10
2628
MCP_SESSION_TIMEOUT_SECONDS = 30.0
2729
SHELL_TIMEOUT = 30.0
28-
SKILLS_DIR = Path(__file__).resolve().parent.parent / "skills"
2930

3031
set_tracing_disabled(True)
3132

@@ -80,17 +81,17 @@ def _parse_skill_description(content: str) -> str:
8081
return ""
8182

8283

83-
def _load_shell_skills() -> list[ShellToolLocalSkill]:
84-
"""Discover local shell skills under SKILLS_DIR.
84+
def _load_shell_skills(skills_dir: Path) -> list[ShellToolLocalSkill]:
85+
"""Discover local shell skills under ``skills_dir``.
8586
86-
Each immediate subdirectory of SKILLS_DIR containing a SKILL.md is mounted
87-
as a ShellToolLocalSkill. The skill name is the directory name; the
88-
description is read from the SKILL.md YAML frontmatter.
87+
Each immediate subdirectory containing a ``SKILL.md`` file is mounted as a
88+
``ShellToolLocalSkill``. The skill name is the directory name; the
89+
description is read from the ``SKILL.md`` YAML frontmatter.
8990
"""
90-
if not SKILLS_DIR.is_dir():
91+
if not skills_dir.is_dir():
9192
return []
9293
skills: list[ShellToolLocalSkill] = []
93-
for skill_dir in sorted(SKILLS_DIR.iterdir()):
94+
for skill_dir in sorted(skills_dir.iterdir()):
9495
skill_md = skill_dir / "SKILL.md"
9596
if not skill_dir.is_dir() or not skill_md.is_file():
9697
continue
@@ -109,6 +110,37 @@ def _load_shell_skills() -> list[ShellToolLocalSkill]:
109110
return skills
110111

111112

113+
def _get_shell_environment() -> ShellToolLocalEnvironment | None:
114+
"""Return the configured local shell environment, if enabled.
115+
116+
Controlled by two independent environment variables:
117+
118+
* ``SHELL_ENABLED`` — when truthy, the ``ShellTool`` is attached to the agent.
119+
* ``SHELL_SKILLS_DIR`` — optional path; when set, skills discovered under it
120+
are mounted alongside the shell. Ignored if ``SHELL_ENABLED`` is not set.
121+
"""
122+
skills_dir_env = os.getenv("SHELL_SKILLS_DIR")
123+
if not env_flag("SHELL_ENABLED"):
124+
if skills_dir_env:
125+
logging.warning(
126+
"SHELL_SKILLS_DIR=%r is set but SHELL_ENABLED is not; ignoring skills dir.",
127+
skills_dir_env,
128+
)
129+
return None
130+
131+
environment: ShellToolLocalEnvironment = {"type": "local"}
132+
if skills_dir_env:
133+
skills = _load_shell_skills(Path(skills_dir_env))
134+
if skills:
135+
environment["skills"] = skills
136+
else:
137+
logging.warning(
138+
"SHELL_SKILLS_DIR=%r yielded no skills; attaching bare local shell.",
139+
skills_dir_env,
140+
)
141+
return environment
142+
143+
112144
async def _shell_executor(request: ShellCommandRequest) -> str:
113145
"""Run each shell command from the request and return combined output.
114146
@@ -215,11 +247,9 @@ def from_dict(cls, name: str, config: dict[str, Any]) -> OpenAIAgent:
215247
)
216248
)
217249
tools: list[Any] = []
218-
if os.getenv("SHELL_SKILLS_ENABLED"):
219-
skills = _load_shell_skills()
220-
if skills:
221-
environment = ShellToolLocalEnvironment(type="local", skills=skills)
222-
tools.append(ShellTool(executor=_shell_executor, environment=environment))
250+
environment = _get_shell_environment()
251+
if environment is not None:
252+
tools.append(ShellTool(executor=_shell_executor, environment=environment))
223253

224254
instructions = _load_instructions()
225255
return cls(name, instructions=instructions, mcp_servers=mcp_servers, tools=tools)

bot/app.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import asyncio
22
import logging
33

4+
from agents import enable_verbose_stdout_logging
5+
46
from .agent import OpenAIAgent
57
from .config import Configuration
8+
from .config import env_flag
69
from .slack import SlackMCPBot
710

811

9-
async def main() -> None:
12+
def _configure_logging() -> None:
1013
logging.basicConfig(
1114
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
1215
level=logging.INFO,
1316
)
17+
if env_flag("AGENT_VERBOSE_LOG"):
18+
enable_verbose_stdout_logging()
19+
20+
21+
async def main() -> None:
22+
_configure_logging()
1423

1524
config = Configuration()
1625
server_config = config.load_config("servers_config.json")

bot/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88

99
logger = logging.getLogger(__name__)
1010

11+
_FALSY_ENV_VALUES = frozenset({"", "0", "false", "no", "off"})
12+
13+
14+
def env_flag(name: str) -> bool:
15+
"""Return True if env var ``name`` is set to a truthy value.
16+
17+
Common falsy spellings (empty, "0", "false", "no", "off") are treated as
18+
disabled so that ``FOO=0`` behaves as users intuitively expect rather than
19+
as Python's default "non-empty string is truthy" rule.
20+
"""
21+
raw = os.getenv(name)
22+
if raw is None:
23+
return False
24+
return raw.strip().lower() not in _FALSY_ENV_VALUES
25+
1126

1227
class Configuration:
1328
"""Manages configuration and environment variables for the MCP Slackbot."""

0 commit comments

Comments
 (0)