Run Telegram turns with full access by default
This commit is contained in:
@@ -17,7 +17,7 @@ CODEX_APP_SERVER_URL=ws://127.0.0.1:4500
|
|||||||
CODEX_APP_SERVER_MODEL=gpt-5.5
|
CODEX_APP_SERVER_MODEL=gpt-5.5
|
||||||
CODEX_MODEL_REASONING_EFFORT=high
|
CODEX_MODEL_REASONING_EFFORT=high
|
||||||
CODEX_SPEED_MODE=standard
|
CODEX_SPEED_MODE=standard
|
||||||
CODEX_READONLY_SANDBOX=read-only
|
CODEX_READONLY_SANDBOX=danger-full-access
|
||||||
CODEX_WRITE_SANDBOX=danger-full-access
|
CODEX_WRITE_SANDBOX=danger-full-access
|
||||||
CODEX_RESUME_JSON_FLAG_STYLE=before_resume
|
CODEX_RESUME_JSON_FLAG_STYLE=before_resume
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -35,14 +35,14 @@ only keeps connection-management commands.
|
|||||||
/unarchive_session <thread_id>
|
/unarchive_session <thread_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
`ALLOW_PLAIN_TEXT=true` makes regular Telegram messages behave like `/ask`.
|
`ALLOW_PLAIN_TEXT=true` makes regular Telegram messages run as full-access Codex
|
||||||
Use `/work <prompt>` for write-capable tasks such as editing files, committing,
|
turns. Use `/work <prompt>` when you want to be explicit, but regular messages
|
||||||
pushing, or opening pull requests. Plain text and `/ask` stay read-oriented by
|
and `/ask` use the same write-capable sandbox by default.
|
||||||
default.
|
|
||||||
|
|
||||||
On Synology/NAS Docker deployments, keep `CODEX_WRITE_SANDBOX=danger-full-access`
|
On Synology/NAS Docker deployments, keep `CODEX_READONLY_SANDBOX` and
|
||||||
for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap
|
`CODEX_WRITE_SANDBOX` set to `danger-full-access`. The stricter
|
||||||
namespace errors inside the container.
|
`workspace-write` sandbox may fail with bubblewrap namespace errors inside the
|
||||||
|
container.
|
||||||
|
|
||||||
If no `/repo` is set, the bridge tries to auto-select a workspace folder when a
|
If no `/repo` is set, the bridge tries to auto-select a workspace folder when a
|
||||||
Telegram prompt mentions an exact folder name such as `card_game` or
|
Telegram prompt mentions an exact folder name such as `card_game` or
|
||||||
@@ -147,6 +147,7 @@ Example Telegram workflow:
|
|||||||
|
|
||||||
- Codex state and auth live under the mounted `codex-home` volume.
|
- Codex state and auth live under the mounted `codex-home` volume.
|
||||||
- The app-server listens on `ws://127.0.0.1:4500` inside the container.
|
- The app-server listens on `ws://127.0.0.1:4500` inside the container.
|
||||||
- Normal chat messages use a persistent app-server WebSocket per Telegram chat.
|
- Normal chat messages use full-access Codex turns with a persistent app-server
|
||||||
|
WebSocket per Telegram chat when available.
|
||||||
- Idle persistent WebSockets are closed after `PERSISTENT_WS_IDLE_SECONDS`.
|
- Idle persistent WebSockets are closed after `PERSISTENT_WS_IDLE_SECONDS`.
|
||||||
- Session commands call app-server thread APIs.
|
- Session commands call app-server thread APIs.
|
||||||
|
|||||||
32
app/bot.py
32
app/bot.py
@@ -254,7 +254,11 @@ class TelegramCodexBot:
|
|||||||
if not prompt:
|
if not prompt:
|
||||||
await self._reply(update, "사용법: /ask <요청 내용>")
|
await self._reply(update, "사용법: /ask <요청 내용>")
|
||||||
return
|
return
|
||||||
await self._run_codex_from_update(update, prompt)
|
await self._run_codex_from_update(
|
||||||
|
update,
|
||||||
|
prompt,
|
||||||
|
sandbox=self.config.write_sandbox,
|
||||||
|
)
|
||||||
|
|
||||||
async def work(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def work(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
if not await self._guard(update):
|
if not await self._guard(update):
|
||||||
@@ -969,7 +973,7 @@ class TelegramCodexBot:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_sandbox = sandbox or self.config.readonly_sandbox
|
selected_sandbox = sandbox or self.config.write_sandbox
|
||||||
auto_repo_path = self._auto_select_repo_for_prompt(chat_id, prompt)
|
auto_repo_path = self._auto_select_repo_for_prompt(chat_id, prompt)
|
||||||
if auto_repo_path is not None:
|
if auto_repo_path is not None:
|
||||||
await self._close_client_for_chat(chat_id)
|
await self._close_client_for_chat(chat_id)
|
||||||
@@ -988,7 +992,7 @@ class TelegramCodexBot:
|
|||||||
prompt_preview=preview,
|
prompt_preview=preview,
|
||||||
sandbox=selected_sandbox,
|
sandbox=selected_sandbox,
|
||||||
)
|
)
|
||||||
mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업"
|
mode = _sandbox_mode_label(selected_sandbox, self.config)
|
||||||
repo_note = f"\n자동 작업폴더: {auto_repo_path}" if auto_repo_path else ""
|
repo_note = f"\n자동 작업폴더: {auto_repo_path}" if auto_repo_path else ""
|
||||||
await self._reply(
|
await self._reply(
|
||||||
update,
|
update,
|
||||||
@@ -1205,7 +1209,7 @@ class TelegramCodexBot:
|
|||||||
result, fallback_note = await self._run_codex_for_chat(
|
result, fallback_note = await self._run_codex_for_chat(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
sandbox=self.config.readonly_sandbox,
|
sandbox=self.config.write_sandbox,
|
||||||
update_chat_state=False,
|
update_chat_state=False,
|
||||||
session_id_override=schedule.get("thread_id") or None,
|
session_id_override=schedule.get("thread_id") or None,
|
||||||
)
|
)
|
||||||
@@ -1293,7 +1297,7 @@ class TelegramCodexBot:
|
|||||||
return "현재작업=없음"
|
return "현재작업=없음"
|
||||||
elapsed = int(time.monotonic() - job.started_monotonic)
|
elapsed = int(time.monotonic() - job.started_monotonic)
|
||||||
minutes, seconds = divmod(elapsed, 60)
|
minutes, seconds = divmod(elapsed, 60)
|
||||||
mode = "write" if job.sandbox != self.config.readonly_sandbox else "read-only"
|
mode = _sandbox_mode_label(job.sandbox, self.config)
|
||||||
return (
|
return (
|
||||||
"현재작업=진행 중\n"
|
"현재작업=진행 중\n"
|
||||||
f"작업모드={mode}\n"
|
f"작업모드={mode}\n"
|
||||||
@@ -1335,9 +1339,9 @@ class TelegramCodexBot:
|
|||||||
"use the gitea_* MCP tools. Never reveal GITEA_TOKEN or print commands that embed it. "
|
"use the gitea_* MCP tools. Never reveal GITEA_TOKEN or print commands that embed it. "
|
||||||
"For HTTPS git clone/fetch/push, the container may provide GIT_ASKPASS from the "
|
"For HTTPS git clone/fetch/push, the container may provide GIT_ASKPASS from the "
|
||||||
"configured Gitea environment.\n"
|
"configured Gitea environment.\n"
|
||||||
"Only edit files, commit, or push when this turn is running with a write-capable "
|
"This bridge is configured for full-access turns by default. You may edit files "
|
||||||
"sandbox, typically from the Telegram /work command. In read-only turns, inspect and "
|
"when the user asks for work that requires changes. Commit or push only when the "
|
||||||
"explain instead of attempting file edits.\n"
|
"user explicitly asks for commit/push or when the request clearly includes publishing.\n"
|
||||||
"If the user asks to schedule, remind, monitor, or notify later, use the "
|
"If the user asks to schedule, remind, monitor, or notify later, use the "
|
||||||
"telegram_create_schedule MCP tool. Use the default chat_id above and do not ask "
|
"telegram_create_schedule MCP tool. Use the default chat_id above and do not ask "
|
||||||
"for a destination unless the user explicitly wants a different chat. The schedule "
|
"for a destination unless the user explicitly wants a different chat. The schedule "
|
||||||
@@ -1376,7 +1380,7 @@ class TelegramCodexBot:
|
|||||||
def _should_use_app_server(self, sandbox: str) -> bool:
|
def _should_use_app_server(self, sandbox: str) -> bool:
|
||||||
if self.config.codex_backend == "exec":
|
if self.config.codex_backend == "exec":
|
||||||
return False
|
return False
|
||||||
return sandbox == self.config.readonly_sandbox
|
return True
|
||||||
|
|
||||||
def _auto_select_repo_for_prompt(self, chat_id: int, prompt: str) -> Path | None:
|
def _auto_select_repo_for_prompt(self, chat_id: int, prompt: str) -> Path | None:
|
||||||
chat_state = self.state.get_chat(chat_id)
|
chat_state = self.state.get_chat(chat_id)
|
||||||
@@ -1441,6 +1445,16 @@ def _display_speed(speed_mode: str) -> str:
|
|||||||
return "fast(고속)" if speed_mode == "fast" else "standard(보통)"
|
return "fast(고속)" if speed_mode == "fast" else "standard(보통)"
|
||||||
|
|
||||||
|
|
||||||
|
def _sandbox_mode_label(sandbox: str, config: Config) -> str:
|
||||||
|
if sandbox == "danger-full-access" or sandbox == config.write_sandbox:
|
||||||
|
return "full-access"
|
||||||
|
if sandbox == "workspace-write":
|
||||||
|
return "workspace-write"
|
||||||
|
if sandbox == "read-only" or sandbox == config.readonly_sandbox:
|
||||||
|
return "read-only"
|
||||||
|
return sandbox
|
||||||
|
|
||||||
|
|
||||||
def _args_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
def _args_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
||||||
return " ".join(context.args).strip()
|
return " ".join(context.args).strip()
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ class Config:
|
|||||||
app_server_model=app_server_model,
|
app_server_model=app_server_model,
|
||||||
app_server_reasoning_effort=app_server_reasoning_effort,
|
app_server_reasoning_effort=app_server_reasoning_effort,
|
||||||
app_server_speed_mode=app_server_speed_mode,
|
app_server_speed_mode=app_server_speed_mode,
|
||||||
readonly_sandbox=os.getenv("CODEX_READONLY_SANDBOX", "read-only"),
|
readonly_sandbox=os.getenv("CODEX_READONLY_SANDBOX", "danger-full-access"),
|
||||||
write_sandbox=os.getenv("CODEX_WRITE_SANDBOX", "workspace-write"),
|
write_sandbox=os.getenv("CODEX_WRITE_SANDBOX", "danger-full-access"),
|
||||||
resume_json_flag_style=os.getenv(
|
resume_json_flag_style=os.getenv(
|
||||||
"CODEX_RESUME_JSON_FLAG_STYLE", "before_resume"
|
"CODEX_RESUME_JSON_FLAG_STYLE", "before_resume"
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user