From 5660b8f24b0bad7085b718e1c36868a534ca143c Mon Sep 17 00:00:00 2001 From: duomi Date: Sun, 15 Mar 2026 21:58:12 +0800 Subject: [PATCH 01/10] fix(heartbeat): ignore untouched default template --- pkg/heartbeat/service.go | 29 +++++++++++++++++++++- pkg/heartbeat/service_test.go | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index 09c93fc6b..5dda78ea9 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -26,6 +26,7 @@ import ( const ( minIntervalMinutes = 5 defaultIntervalMinutes = 30 + userTasksMarker = "Add your heartbeat tasks below this line:" ) // HeartbeatHandler is the function type for handling heartbeat. @@ -232,7 +233,7 @@ func (hs *HeartbeatService) buildPrompt() string { } content := string(data) - if len(content) == 0 { + if !heartbeatHasUserTasks(content) { return "" } @@ -284,6 +285,32 @@ Add your heartbeat tasks below this line: } } +func heartbeatHasUserTasks(content string) bool { + trimmed := strings.TrimSpace(content) + if trimmed == "" { + return false + } + + markerIdx := strings.Index(content, userTasksMarker) + if markerIdx < 0 { + return true + } + + tasksSection := content[markerIdx+len(userTasksMarker):] + for _, line := range strings.Split(tasksSection, "\n") { + trimmedLine := strings.TrimSpace(line) + if trimmedLine == "" { + continue + } + if strings.HasPrefix(trimmedLine, "#") { + continue + } + return true + } + + return false +} + // sendResponse sends the heartbeat response to the last channel func (hs *HeartbeatService) sendResponse(response string) { hs.mu.RLock() diff --git a/pkg/heartbeat/service_test.go b/pkg/heartbeat/service_test.go index 3b7eeeefb..309b4378f 100644 --- a/pkg/heartbeat/service_test.go +++ b/pkg/heartbeat/service_test.go @@ -3,6 +3,7 @@ package heartbeat import ( "os" "path/filepath" + "strings" "testing" "time" @@ -203,3 +204,47 @@ func TestHeartbeatFilePath(t *testing.T) { t.Errorf("Expected HEARTBEAT.md at %s, but it doesn't exist", expectedPath) } } + +func TestBuildPrompt_DefaultTemplateStaysIdle(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + hs := NewHeartbeatService(tmpDir, 30, true) + hs.createDefaultHeartbeatTemplate() + + if prompt := hs.buildPrompt(); prompt != "" { + t.Fatalf("buildPrompt() = %q, want empty prompt for untouched default template", prompt) + } +} + +func TestBuildPrompt_UserTasksAfterMarkerProducePrompt(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "heartbeat-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + hs := NewHeartbeatService(tmpDir, 30, true) + hs.createDefaultHeartbeatTemplate() + + path := filepath.Join(tmpDir, "HEARTBEAT.md") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("Failed to read HEARTBEAT.md: %v", err) + } + updated := string(data) + "\n- Check unread Feishu messages\n" + if err := os.WriteFile(path, []byte(updated), 0o644); err != nil { + t.Fatalf("Failed to update HEARTBEAT.md: %v", err) + } + + prompt := hs.buildPrompt() + if prompt == "" { + t.Fatal("buildPrompt() = empty, want non-empty prompt when user tasks are present") + } + if !strings.Contains(prompt, "Check unread Feishu messages") { + t.Fatalf("prompt = %q, want user task content", prompt) + } +} From 41ebe1e1c7ae64ce31bd35e8bbe81b4c392520b6 Mon Sep 17 00:00:00 2001 From: Avisek Date: Thu, 19 Mar 2026 16:44:07 +0530 Subject: [PATCH 02/10] chore: Ignore the `docker/data` directory. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8ba6a45fe..715ee57d0 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ cmd/telegram/ !web/backend/dist/ web/backend/dist/* !web/backend/dist/.gitkeep + + +docker/data \ No newline at end of file From 6148ccc52937fb12c1f943bc28a865dcca982dc1 Mon Sep 17 00:00:00 2001 From: BeaconCat <111232138+BeaconCat@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:36:51 +0800 Subject: [PATCH 03/10] docs(feishu): note that Feishu channel does not support 32-bit devices (#1851) Co-authored-by: BeaconCat --- docs/channels/feishu/README.fr.md | 4 ++++ docs/channels/feishu/README.ja.md | 4 ++++ docs/channels/feishu/README.md | 4 ++++ docs/channels/feishu/README.pt-br.md | 4 ++++ docs/channels/feishu/README.vi.md | 4 ++++ docs/channels/feishu/README.zh.md | 21 +++++++++------------ 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/channels/feishu/README.fr.md b/docs/channels/feishu/README.fr.md index 555dd2713..f1ff26480 100644 --- a/docs/channels/feishu/README.fr.md +++ b/docs/channels/feishu/README.fr.md @@ -46,3 +46,7 @@ Feishu (nom international : Lark) est une plateforme de collaboration d'entrepri > `encrypt_key` et `verification_token` sont optionnels ; l'activation du chiffrement des événements est recommandée pour les environnements de production. > > Pour les références d'emojis personnalisés, voir : [Liste des emojis Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) + +## Limitations de plateforme + +> ⚠️ **Le canal Feishu ne prend pas en charge les appareils 32 bits.** Le SDK Feishu ne fournit que des builds 64 bits. Les architectures 32 bits (armv6, armv7, mipsle, etc.) ne peuvent pas utiliser le canal Feishu. Pour la messagerie sur des appareils 32 bits, utilisez Telegram, Discord ou OneBot. diff --git a/docs/channels/feishu/README.ja.md b/docs/channels/feishu/README.ja.md index ca467dd4c..4bb75a734 100644 --- a/docs/channels/feishu/README.ja.md +++ b/docs/channels/feishu/README.ja.md @@ -46,3 +46,7 @@ > `encrypt_key` と `verification_token` はオプションですが、本番環境ではイベント暗号化を有効にすることを推奨します。 > > カスタム絵文字の参考:[飛書絵文字リスト](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) + +## プラットフォーム制限 + +> ⚠️ **飛書チャネルは 32 ビットデバイスをサポートしていません。** 飛書 SDK は 64 ビットビルドのみ提供しています。armv6 / armv7 / mipsle などの 32 ビットアーキテクチャでは飛書チャネルを使用できません。32 ビットデバイスでのメッセージングには、Telegram、Discord、または OneBot をご利用ください。 diff --git a/docs/channels/feishu/README.md b/docs/channels/feishu/README.md index a991c76af..2aeaa31cb 100644 --- a/docs/channels/feishu/README.md +++ b/docs/channels/feishu/README.md @@ -46,3 +46,7 @@ Feishu (international name: Lark) is an enterprise collaboration platform by Byt > `encrypt_key` and `verification_token` are optional; enabling event encryption is recommended for production environments. > > For custom emoji references, see: [Feishu Emoji List](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) + +## Platform Limitations + +> ⚠️ **Feishu channel does not support 32-bit devices.** The Feishu SDK only provides 64-bit builds. Devices running armv6, armv7, mipsle, or other 32-bit architectures cannot use the Feishu channel. For messaging on 32-bit devices, use Telegram, Discord, or OneBot instead. diff --git a/docs/channels/feishu/README.pt-br.md b/docs/channels/feishu/README.pt-br.md index 00a8c95b0..5b5fcaf68 100644 --- a/docs/channels/feishu/README.pt-br.md +++ b/docs/channels/feishu/README.pt-br.md @@ -46,3 +46,7 @@ Feishu (nome internacional: Lark) é uma plataforma de colaboração empresarial > `encrypt_key` e `verification_token` são opcionais; recomenda-se habilitar a criptografia de eventos em ambientes de produção. > > Para referências de emojis personalizados, consulte: [Lista de Emojis do Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) + +## Limitações de Plataforma + +> ⚠️ **O canal Feishu não suporta dispositivos 32 bits.** O SDK do Feishu fornece apenas builds 64 bits. Arquiteturas 32 bits (armv6, armv7, mipsle, etc.) não podem usar o canal Feishu. Para mensagens em dispositivos 32 bits, use Telegram, Discord ou OneBot. diff --git a/docs/channels/feishu/README.vi.md b/docs/channels/feishu/README.vi.md index 600dce260..e704b7794 100644 --- a/docs/channels/feishu/README.vi.md +++ b/docs/channels/feishu/README.vi.md @@ -46,3 +46,7 @@ Feishu (tên quốc tế: Lark) là nền tảng cộng tác doanh nghiệp củ > `encrypt_key` và `verification_token` là tùy chọn; nên bật mã hóa sự kiện trong môi trường sản xuất. > > Tham khảo emoji tùy chỉnh: [Danh sách Emoji Feishu](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) + +## Giới hạn nền tảng + +> ⚠️ **Kênh Feishu không hỗ trợ thiết bị 32 bit.** SDK Feishu chỉ cung cấp bản build 64 bit. Các kiến trúc 32 bit (armv6, armv7, mipsle, v.v.) không thể sử dụng kênh Feishu. Để nhắn tin trên thiết bị 32 bit, hãy dùng Telegram, Discord hoặc OneBot. diff --git a/docs/channels/feishu/README.zh.md b/docs/channels/feishu/README.zh.md index a967dbdc3..6e2829547 100644 --- a/docs/channels/feishu/README.zh.md +++ b/docs/channels/feishu/README.zh.md @@ -35,16 +35,13 @@ ## 设置流程 -1. 前往 [飞书开放平台](https://open.feishu.cn/)(国际版用户请前往 [Lark 开放平台](https://open.larksuite.com/))创建应用 -2. 在应用设置中启用**机器人**能力 -3. 创建版本并发布应用(应用发布后配置才会生效) -4. 获取 **App ID**(以 `cli_` 开头)和 **App Secret** -5. 将 App ID 和 App Secret 填入 PicoClaw 配置文件 -6. 运行 `picoclaw gateway` 启动服务 -7. 在飞书中搜索机器人名称,开始对话 +1. 前往 [飞书开放平台](https://open.feishu.cn/)(国际版用户请前往 [Lark 开放平台](https://open.larksuite.com/))创建应用程序 +2. 获取 App ID 和 App Secret +3. 配置事件订阅和Webhook URL +4. 设置加密(可选,生产环境建议启用) +5. 将 App ID、App Secret、Encrypt Key 和 Verification Token(如果启用加密) 填入配置文件中 +6. 自定义你希望 PicoClaw react 你消息时的表情(可选, Reference URL: [Feishu Emoji List](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)) -> PicoClaw 使用 WebSocket/SDK 模式连接飞书,无需配置公网回调地址或 Webhook URL。 -> -> `encrypt_key` 和 `verification_token` 为可选项,生产环境建议启用事件加密。 -> -> 自定义表情参考:[飞书表情列表](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce) +## 平台限制 + +> ⚠️ **飞书通道不支持 32 位设备。** 飞书官方 SDK 仅提供 64 位构建,armv6 / armv7 / mipsle 等 32 位架构无法使用飞书通道。如需在 32 位设备上接入即时通讯,请改用 Telegram、Discord 或 OneBot 等通道。 From bc0be17e88706299f5ec4b6bbe010aeb24df9cf4 Mon Sep 17 00:00:00 2001 From: Badgerbees Date: Sat, 21 Mar 2026 17:09:02 +0700 Subject: [PATCH 04/10] fix(identity): support negative integers in isNumeric for Telegram group IDs --- pkg/identity/identity.go | 11 ++++++++--- pkg/identity/identity_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index 372bbe38b..045725a8d 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -94,13 +94,18 @@ func MatchAllowed(sender bus.SenderInfo, allowed string) bool { return false } -// isNumeric returns true if s consists entirely of digits. +// isNumeric returns true if s consists entirely of digits, allowing for an optional leading minus sign +// (required for Telegram group/channel IDs like -1001234567890). func isNumeric(s string) bool { if s == "" { return false } - for _, r := range s { - if r < '0' || r > '9' { + start := 0 + if s[0] == '-' && len(s) > 1 { + start = 1 + } + for i := start; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { return false } } diff --git a/pkg/identity/identity_test.go b/pkg/identity/identity_test.go index a588f1484..c60402d19 100644 --- a/pkg/identity/identity_test.go +++ b/pkg/identity/identity_test.go @@ -97,6 +97,15 @@ func TestMatchAllowed(t *testing.T) { allowed: "654321", want: false, }, + { + name: "negative numeric ID matches PlatformID", + sender: bus.SenderInfo{ + Platform: "telegram", + PlatformID: "-1001234567890", + }, + allowed: "-1001234567890", + want: true, + }, // Username matching { name: "@username matches Username", @@ -238,6 +247,9 @@ func TestIsNumeric(t *testing.T) { {"abc", false}, {"12a34", false}, {"telegram", false}, + {"-1001234567890", true}, + {"-", false}, + {"-12a34", false}, } for _, tt := range tests { From ab93c235aee678d5414fc572ae973ad4154673c4 Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:36:29 +0800 Subject: [PATCH 05/10] docs: clean up README by removing duplicate sections --- README.md | 793 ------------------------------------------------------ 1 file changed, 793 deletions(-) diff --git a/README.md b/README.md index 652792d83..e3d39ded2 100644 --- a/README.md +++ b/README.md @@ -747,797 +747,4 @@ User Groups: discord: -PicoClaw center"> - PicoClaw - -

PicoClaw: Ultra-Efficient AI Assistant in Go

- -

$10 Hardware · <10MB RAM · <1s Boot · 皮皮虾,我们走!

-

- Go - Hardware - License -
- Website - Docs - Wiki -
- Twitter - - Discord -

- -[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | **English** - - - ---- - -> **PicoClaw** is an independent open-source project initiated by [Sipeed](https://sipeed.com). It is written entirely in **Go** — not a fork of OpenClaw, NanoBot, or any other project. - -🦐 PicoClaw is an ultra-lightweight personal AI Assistant inspired by [NanoBot](https://github.com/HKUDS/nanobot), refactored from the ground up in Go through a self-bootstrapping process, where the AI agent itself drove the entire architectural migration and code optimization. - -⚡️ Runs on $10 hardware with <10MB RAM: That's 99% less memory than OpenClaw and 98% cheaper than a Mac mini! - - - - - - -
-

- -

-
-

- -

-
- -> [!CAUTION] -> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明** -> -> * **NO CRYPTO:** PicoClaw has **NO** official token/coin. All claims on `pump.fun` or other trading platforms are **SCAMS**. -> -> * **OFFICIAL DOMAIN:** The **ONLY** official website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)** -> * **Warning:** Many `.ai/.org/.com/.net/...` domains are registered by third parties. -> * **Warning:** picoclaw is in early development now and may have unresolved network security issues. Do not deploy to production environments before the v1.0 release. -> * **Note:** picoclaw has recently merged a lot of PRs, which may result in a larger memory footprint (10–20MB) in the latest versions. We plan to prioritize resource optimization as soon as the current feature set reaches a stable state. - -## 📢 News - -2026-03-17 🚀 **v0.2.3 Released!** System tray UI (Windows & Linux), sub-agent status tracking (`spawn_status`), experimental gateway hot-reload, cron security gates, and 2 security fixes. PicoClaw now at **25K ⭐**! - -2026-03-09 🎉 **v0.2.1 — Biggest update yet!** MCP protocol support, 4 new channels (Matrix/IRC/WeCom/Discord Proxy), 3 new providers (Kimi/Minimax/Avian), vision pipeline, JSONL memory store, and model routing. - -2026-02-28 📦 **v0.2.0** released with Docker Compose support and Web UI launcher. - -2026-02-26 🎉 PicoClaw hit **20K stars** in just 17 days! Channel auto-orchestration and capability interfaces landed. - -
-Older news... - -2026-02-16 🎉 PicoClaw hit 12K stars in one week! Community maintainer roles and [roadmap](ROADMAP.md) officially posted. - -2026-02-13 🎉 PicoClaw hit 5000 stars in 4 days! Project Roadmap and Developer Group setup underway. - -2026-02-09 🎉 **PicoClaw Launched!** Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 PicoClaw,Let's Go! - -
- -## ✨ Features - -🪶 **Ultra-Lightweight**: <10MB Memory footprint — 99% smaller than OpenClaw core functionality.* - -💰 **Minimal Cost**: Efficient enough to run on $10 Hardware — 98% cheaper than a Mac mini. - -⚡️ **Lightning Fast**: 400X Faster startup time, boot in <1 second even on 0.6GHz single core. - -🌍 **True Portability**: Single self-contained binary across RISC-V, ARM, MIPS, and x86, One-click to Go! - -🤖 **AI-Bootstrapped**: Autonomous Go-native implementation — 95% Agent-generated core with human-in-the-loop refinement. - -🔌 **MCP Support**: Native [Model Context Protocol](https://modelcontextprotocol.io/) integration — connect any MCP server to extend agent capabilities. - -👁️ **Vision Pipeline**: Send images and files directly to the agent — automatic base64 encoding for multimodal LLMs. - -🧠 **Smart Routing**: Rule-based model routing — simple queries go to lightweight models, saving API costs. - -_*Recent versions may use 10–20MB due to rapid feature merges. Resource optimization is planned. Startup comparison based on 0.8GHz single-core benchmarks (see table below)._ - -| | OpenClaw | NanoBot | **PicoClaw** | -| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- | -| **Language** | TypeScript | Python | **Go** | -| **RAM** | >1GB | >100MB | **< 10MB*** | -| **Startup**
(0.8GHz core) | >500s | >30s | **<1s** | -| **Cost** | Mac Mini $599 | Most Linux SBC
~$50 | **Any Linux Board**
**As low as $10** | - -PicoClaw - -> 📋 **[Hardware Compatibility List](docs/hardware-compatibility.md)** — See all tested boards, from $5 RISC-V to Raspberry Pi to Android phones. Your board not listed? Submit a PR! - -## 🦾 Demonstration - -### 🛠️ Standard Assistant Workflows - - - - - - - - - - - - - - - - - -

🧩 Full-Stack Engineer

🗂️ Logging & Planning Management

🔎 Web Search & Learning

Develop • Deploy • ScaleSchedule • Automate • MemoryDiscovery • Insights • Trends
- -### 📱 Run on old Android Phones - -Give your decade-old phone a second life! Turn it into a smart AI Assistant with PicoClaw. Quick Start: - -1. **Install [Termux](https://github.com/termux/termux-app)** (Download from [GitHub Releases](https://github.com/termux/termux-app/releases), or search in F-Droid / Google Play). -2. **Execute cmds** - -```bash -# Download the latest release from https://github.com/sipeed/picoclaw/releases -wget https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_arm64.tar.gz -tar xzf picoclaw_Linux_arm64.tar.gz -pkg install proot -termux-chroot ./picoclaw onboard -``` - -And then follow the instructions in the "Quick Start" section to complete the configuration! - -PicoClaw - -### 🐜 Innovative Low-Footprint Deploy - -PicoClaw can be deployed on almost any Linux device! - -- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant -- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance -- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring - - - -🌟 More Deployment Cases Await! - -## 📦 Install - -### Install with precompiled binary - -Download the binary for your platform from the [Releases](https://github.com/sipeed/picoclaw/releases) page. - -### Install from source (latest features, recommended for development) - -```bash -git clone https://github.com/sipeed/picoclaw.git - -cd picoclaw -make deps - -# Build, no need to install -make build - -# Build for multiple platforms -make build-all - -# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) -make build-pi-zero - -# Build And Install -make install -``` - -**Raspberry Pi Zero 2 W:** Use the binary that matches your OS: 32-bit Raspberry Pi OS → `make build-linux-arm`; 64-bit → `make build-linux-arm64`. Or run `make build-pi-zero` to build both. - -## 📚 Documentation - -For detailed guides, see the docs below. The README covers quick start only. - -```bash -# 1. Clone this repo -git clone https://github.com/sipeed/picoclaw.git -cd picoclaw - -# 2. First run — auto-generates docker/data/config.json then exits -docker compose -f docker/docker-compose.yml --profile gateway up -# The container prints "First-run setup complete." and stops. - -# 3. Set your API keys -vim docker/data/config.json # Set provider API keys, bot tokens, etc. - -# 4. Start -docker compose -f docker/docker-compose.yml --profile gateway up -d -``` - -> [!TIP] -> **Docker Users**: By default, the Gateway listens on `127.0.0.1` which is not accessible from the host. If you need to access the health endpoints or expose ports, set `PICOCLAW_GATEWAY_HOST=0.0.0.0` in your environment or update `config.json`. - -```bash -# 5. Check logs -docker compose -f docker/docker-compose.yml logs -f picoclaw-gateway - -# 6. Stop -docker compose -f docker/docker-compose.yml --profile gateway down -``` - -### Launcher Mode (Web Console) - -The `launcher` image includes all three binaries (`picoclaw`, `picoclaw-launcher`, `picoclaw-launcher-tui`) and starts the web console by default, which provides a browser-based UI for configuration and chat. - -```bash -docker compose -f docker/docker-compose.yml --profile launcher up -d -``` - -Open http://localhost:18800 in your browser. The launcher manages the gateway process automatically. - -> [!WARNING] -> The web console does not yet support authentication. Avoid exposing it to the public internet. - -### Agent Mode (One-shot) - -```bash -# Ask a question -docker compose -f docker/docker-compose.yml run --rm picoclaw-agent -m "What is 2+2?" - -# Interactive mode -docker compose -f docker/docker-compose.yml run --rm picoclaw-agent -``` - -### Update - -```bash -docker compose -f docker/docker-compose.yml pull -docker compose -f docker/docker-compose.yml --profile gateway up -d -``` - -### 🚀 Quick Start - -> [!TIP] -> Set your API Key in `~/.picoclaw/config.json`. Get API Keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Web search is optional — get a free [Tavily API](https://tavily.com) (1000 free queries/month) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month). - -**1. Initialize** - -```bash -picoclaw onboard -``` - -**2. Configure** (`~/.picoclaw/config.json`) - -```json -{ - "agents": { - "defaults": { - "workspace": "~/.picoclaw/workspace", - "model_name": "gpt-5.4", - "max_tokens": 8192, - "temperature": 0.7, - "max_tool_iterations": 20 - } - }, - "model_list": [ - { - "model_name": "ark-code-latest", - "model": "volcengine/ark-code-latest", - "api_key": "sk-your-api-key" - }, - { - "model_name": "gpt-5.4", - "model": "openai/gpt-5.4", - "api_key": "your-api-key", - "request_timeout": 300 - }, - { - "model_name": "claude-sonnet-4.6", - "model": "anthropic/claude-sonnet-4.6", - "api_key": "your-anthropic-key" - } - ], - "tools": { - "web": { - "brave": { - "enabled": false, - "api_key": "YOUR_BRAVE_API_KEY", - "max_results": 5 - }, - "tavily": { - "enabled": false, - "api_key": "YOUR_TAVILY_API_KEY", - "max_results": 5 - }, - "duckduckgo": { - "enabled": true, - "max_results": 5 - }, - "perplexity": { - "enabled": false, - "api_key": "YOUR_PERPLEXITY_API_KEY", - "max_results": 5 - }, - "searxng": { - "enabled": false, - "base_url": "http://your-searxng-instance:8888", - "max_results": 5 - } - } - } -} -``` - -> **New**: The `model_list` configuration format allows zero-code provider addition. See [Model Configuration](#model-configuration-model_list) for details. -> `request_timeout` is optional and uses seconds. If omitted or set to `<= 0`, PicoClaw uses the default timeout (120s). - -**3. Get API Keys** - -* **LLM Provider**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys) -* **Web Search** (optional): - * [Brave Search](https://brave.com/search/api) - Paid ($5/1000 queries, ~$5-6/month) - * [Perplexity](https://www.perplexity.ai) - AI-powered search with chat interface - * [SearXNG](https://github.com/searxng/searxng) - Self-hosted metasearch engine (free, no API key needed) - * [Tavily](https://tavily.com) - Optimized for AI Agents (1000 requests/month) - * DuckDuckGo - Built-in fallback (no API key required) - -> **Note**: See `config.example.json` for a complete configuration template. - -**4. Chat** - -```bash -picoclaw agent -m "What is 2+2?" -``` - -That's it! You have a working AI assistant in 2 minutes. - ---- - -## 💬 Chat Apps - -Talk to your picoclaw through Telegram, Discord, WhatsApp, Matrix, QQ, DingTalk, LINE, or WeCom - -> **Note**: All webhook-based channels (LINE, WeCom, etc.) are served on a single shared Gateway HTTP server (`gateway.host`:`gateway.port`, default `127.0.0.1:18790`). There are no per-channel ports to configure. Note: Feishu uses WebSocket/SDK mode and does not use the shared HTTP webhook server. - -| Channel | Setup | -| ------------ | ---------------------------------- | -| **Telegram** | Easy (just a token) | -| **Discord** | Easy (bot token + intents) | -| **WhatsApp** | Easy (native: QR scan; or bridge URL) | -| **Matrix** | Medium (homeserver + bot access token) | -| **QQ** | Easy (AppID + AppSecret) | -| **DingTalk** | Medium (app credentials) | -| **LINE** | Medium (credentials + webhook URL) | -| **WeCom AI Bot** | Medium (Token + AES key) | - -
-Telegram (Recommended) - -**1. Create a bot** - -* Open Telegram, search `@BotFather` -* Send `/newbot`, follow prompts -* Copy the token - -**2. Configure** - -```json -{ - "channels": { - "telegram": { - "enabled": true, - "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] - } - } -} -``` - -> Get your user ID from `@userinfobot` on Telegram. - -**3. Run** - -```bash -picoclaw gateway -``` - -**4. Telegram command menu (auto-registered at startup)** - -PicoClaw now keeps command definitions in one shared registry. On startup, Telegram will automatically register supported bot commands (for example `/start`, `/help`, `/show`, `/list`) so command menu and runtime behavior stay in sync. -Telegram command menu registration remains channel-local discovery UX; generic command execution is handled centrally in the agent loop via the commands executor. - -If command registration fails (network/API transient errors), the channel still starts and PicoClaw retries registration in the background. - -
- -
-Discord - -**1. Create a bot** - -* Go to -* Create an application → Bot → Add Bot -* Copy the bot token - -**2. Enable intents** - -* In the Bot settings, enable **MESSAGE CONTENT INTENT** -* (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data - -**3. Get your User ID** -* Discord Settings → Advanced → enable **Developer Mode** -* Right-click your avatar → **Copy User ID** - -**4. Configure** - -```json -{ - "channels": { - "discord": { - "enabled": true, - "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] - } - } -} -``` - -**5. Invite the bot** - -* OAuth2 → URL Generator -* Scopes: `bot` -* Bot Permissions: `Send Messages`, `Read Message History` -* Open the generated invite URL and add the bot to your server - -**Optional: Group trigger mode** - -By default the bot responds to all messages in a server channel. To restrict responses to @-mentions only, add: - -```json -{ - "channels": { - "discord": { - "group_trigger": { "mention_only": true } - } - } -} -``` - -You can also trigger by keyword prefixes (e.g. `!bot`): - -```json -{ - "channels": { - "discord": { - "group_trigger": { "prefixes": ["!bot"] } - } - } -} -``` - -**6. Run** - -```bash -picoclaw gateway -``` - -
- -
-WhatsApp (native via whatsmeow) - -PicoClaw can connect to WhatsApp in two ways: - -- **Native (recommended):** In-process using [whatsmeow](https://github.com/tulir/whatsmeow). No separate bridge. Set `"use_native": true` and leave `bridge_url` empty. On first run, scan the QR code with WhatsApp (Linked Devices). Session is stored under your workspace (e.g. `workspace/whatsapp/`). The native channel is **optional** to keep the default binary small; build with `-tags whatsapp_native` (e.g. `make build-whatsapp-native` or `go build -tags whatsapp_native ./cmd/...`). -- **Bridge:** Connect to an external WebSocket bridge. Set `bridge_url` (e.g. `ws://localhost:3001`) and keep `use_native` false. - -**Configure (native)** - -```json -{ - "channels": { - "whatsapp": { - "enabled": true, - "use_native": true, - "session_store_path": "", - "allow_from": [] - } - } -} -``` - -If `session_store_path` is empty, the session is stored in `<workspace>/whatsapp/`. Run `picoclaw gateway`; on first run, scan the QR code printed in the terminal with WhatsApp → Linked Devices. - -
- -
-QQ - -**1. Create a bot** - -- Go to [QQ Open Platform](https://q.qq.com/#) -- Create an application → Get **AppID** and **AppSecret** - -**2. Configure** - -```json -{ - "channels": { - "qq": { - "enabled": true, - "app_id": "YOUR_APP_ID", - "app_secret": "YOUR_APP_SECRET", - "allow_from": [] - } - } -} -``` - -> Set `allow_from` to empty to allow all users, or specify QQ numbers to restrict access. - -**3. Run** - -```bash -picoclaw gateway -``` - -
- -
-DingTalk - -**1. Create a bot** - -* Go to [Open Platform](https://open.dingtalk.com/) -* Create an internal app -* Copy Client ID and Client Secret - -**2. Configure** - -```json -{ - "channels": { - "dingtalk": { - "enabled": true, - "client_id": "YOUR_CLIENT_ID", - "client_secret": "YOUR_CLIENT_SECRET", - "allow_from": [] - } - } -} -``` - -> Set `allow_from` to empty to allow all users, or specify DingTalk user IDs to restrict access. - -**3. Run** - -```bash -picoclaw gateway -``` -
- -
-Matrix - -**1. Prepare bot account** - -* Use your preferred homeserver (e.g. `https://matrix.org` or self-hosted) -* Create a bot user and obtain its access token - -**2. Configure** - -```json -{ - "channels": { - "matrix": { - "enabled": true, - "homeserver": "https://matrix.org", - "user_id": "@your-bot:matrix.org", - "access_token": "YOUR_MATRIX_ACCESS_TOKEN", - "allow_from": [] - } - } -} -``` - -**3. Run** - -```bash -picoclaw gateway -``` - -For full options (`device_id`, `join_on_invite`, `group_trigger`, `placeholder`, `reasoning_channel_id`), see [Matrix Channel Configuration Guide](docs/channels/matrix/README.md). - -
- -
-LINE - -**1. Create a LINE Official Account** - -- Go to [LINE Developers Console](https://developers.line.biz/) -- Create a provider → Create a Messaging API channel -- Copy **Channel Secret** and **Channel Access Token** - -**2. Configure** - -```json -{ - "channels": { - "line": { - "enabled": true, - "channel_secret": "YOUR_CHANNEL_SECRET", - "channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN", - "webhook_path": "/webhook/line", - "allow_from": [] - } - } -} -``` - -> LINE webhook is served on the shared Gateway server (`gateway.host`:`gateway.port`, default `127.0.0.1:18790`). - -**3. Set up Webhook URL** - -LINE requires HTTPS for webhooks. Use a reverse proxy or tunnel: - -```bash -# Example with ngrok (gateway default port is 18790) -ngrok http 18790 -``` - -Then set the Webhook URL in LINE Developers Console to `https://your-domain/webhook/line` and enable **Use webhook**. - -**4. Run** - -```bash -picoclaw gateway -``` - -> In group chats, the bot responds only when @mentioned. Replies quote the original message. - -
- -
-WeCom (企业微信) - -PicoClaw supports three types of WeCom integration: - -**Option 1: WeCom Bot (Bot)** - Easier setup, supports group chats -**Option 2: WeCom App (Custom App)** - More features, proactive messaging, private chat only -**Option 3: WeCom AI Bot (AI Bot)** - Official AI Bot, streaming replies, supports group & private chat - -See [WeCom AI Bot Configuration Guide](docs/channels/wecom/wecom_aibot/README.zh.md) for detailed setup instructions. - -**Quick Setup - WeCom AI Bot:** - -**1. Create an AI Bot** - -* Go to WeCom Admin Console → AI Bot -* Create a new AI Bot → Set name, avatar, etc. -* Copy **Bot ID** and **Secret** - -**2. Configure** - -```json -{ - "channels": { - "wecom_aibot": { - "enabled": true, - "bot_id": "YOUR_BOT_ID", - "secret": "YOUR_SECRET", - "allow_from": [], - "welcome_message": "Hello! How can I help you?" - } - } -} -``` - -**3. Run** - -```bash -picoclaw gateway -``` - -> **Note**: WeCom AI Bot uses streaming pull protocol — no reply timeout concerns. Long tasks (>30 seconds) automatically switch to `response_url` push delivery. - -
- -## ClawdChat Join the Agent Social Network - -Connect Picoclaw to the Agent Social Network simply by sending a single message via the CLI or any integrated Chat App. - -**Read `https://clawdchat.ai/skill.md` and follow the instructions to join [ClawdChat.ai](https://clawdchat.ai)** - -## 🖥️ CLI Reference - -| Command | Description | -| ------------------------- | ----------------------------- | -| `picoclaw onboard` | Initialize config & workspace | -| `picoclaw agent -m "..."` | Chat with the agent | -| `picoclaw agent` | Interactive chat mode | -| `picoclaw gateway` | Start the gateway | -| `picoclaw status` | Show status | -| `picoclaw version` | Show version info | -| `picoclaw cron list` | List all scheduled jobs | -| `picoclaw cron add ...` | Add a scheduled job | -| `picoclaw cron disable` | Disable a scheduled job | -| `picoclaw cron remove` | Remove a scheduled job | -| `picoclaw skills list` | List installed skills | -| `picoclaw skills install` | Install a skill | -| `picoclaw migrate` | Migrate data from older versions | -| `picoclaw auth login` | Authenticate with providers | - -### Scheduled Tasks / Reminders - -PicoClaw supports scheduled reminders and recurring tasks through the `cron` tool: - -* **One-time reminders**: "Remind me in 10 minutes" → triggers once after 10min -* **Recurring tasks**: "Remind me every 2 hours" → triggers every 2 hours -* **Cron expressions**: "Remind me at 9am daily" → uses cron expression - -## 🤝 Contribute & Roadmap - -PRs welcome! The codebase is intentionally small and readable. 🤗 - -See our full [Community Roadmap](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md). - -Developer group building, join after your first merged PR! - -User Groups: - -discord: - -PicoClaw - -## ClawdChat Join the Agent Social Network - -Connect Picoclaw to the Agent Social Network simply by sending a single message via the CLI or any integrated Chat App. - -**Read `https://clawdchat.ai/skill.md` and follow the instructions to join [ClawdChat.ai](https://clawdchat.ai)** - -## 🖥️ CLI Reference - -| Command | Description | -| ------------------------- | ----------------------------- | -| `picoclaw onboard` | Initialize config & workspace | -| `picoclaw agent -m "..."` | Chat with the agent | -| `picoclaw agent` | Interactive chat mode | -| `picoclaw gateway` | Start the gateway | -| `picoclaw status` | Show status | -| `picoclaw version` | Show version info | -| `picoclaw cron list` | List all scheduled jobs | -| `picoclaw cron add ...` | Add a scheduled job | -| `picoclaw cron disable` | Disable a scheduled job | -| `picoclaw cron remove` | Remove a scheduled job | -| `picoclaw skills list` | List installed skills | -| `picoclaw skills install` | Install a skill | -| `picoclaw migrate` | Migrate data from older versions | -| `picoclaw auth login` | Authenticate with providers | -| `picoclaw model` | View or switch the default model | - -### Scheduled Tasks / Reminders - -PicoClaw supports scheduled reminders and recurring tasks through the `cron` tool: - -* **One-time reminders**: "Remind me in 10 minutes" → triggers once after 10min -* **Recurring tasks**: "Remind me every 2 hours" → triggers every 2 hours -* **Cron expressions**: "Remind me at 9am daily" → uses cron expression - -## 🤝 Contribute & Roadmap - -PRs welcome! The codebase is intentionally small and readable. 🤗 - -See our full [Community Roadmap](https://github.com/sipeed/picoclaw/blob/main/ROADMAP.md). - -Developer group building, join after your first merged PR! - -User Groups: - -discord: - PicoClaw From e2e3e6d5b08ef216b6aa352d33c837360e610827 Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:40:10 +0800 Subject: [PATCH 06/10] docs: update WeChat QRCode for README --- assets/wechat.png | Bin 162306 -> 62722 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/wechat.png b/assets/wechat.png index 6512421edec830f5d3b1ced07b923656b499a599..effb4dab959a68bde6aa6c24d509cac80a1a1ae1 100644 GIT binary patch literal 62722 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 Js$|SQ{x4a%qip~H literal 162306 zcmeEtRaYEcv@MVjf(Lg9!QI^g1PJc#!QCAig1fs0cL=VHySp^*+PFi*CEq>gC)~ID zwD+hQWos;%YtB_+3UU(22>1w4P*BK{KYu7eLBaSzL464L4D)^@Uxv!|eSx?Csp$lT zLY@SLLg@#E$Ka(31@#3=@`tdBd*Zb8}8eA^v$ zS)DhBQ!>!ffe&DVN$~!VDZ_aO0O5Qa@r8xjychO^eh}ycrCuV%0AARi?q9t~9%oZB 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

+SpWi7D&(VT~X6z6}YNGzs@P$dlZL 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 Rba(}PDMHj_>!eLX{}+sE=|%tm From f71a6ff76ce62324da8d9ed5fa49d9f702c98512 Mon Sep 17 00:00:00 2001 From: LC <64722907+lc6464@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:52:39 +0800 Subject: [PATCH 07/10] docs: update alt text of wechat.png with a more meaningful description Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3d39ded2..4b0852ccd 100644 --- a/README.md +++ b/README.md @@ -747,4 +747,5 @@ User Groups: discord: -PicoClaw +WeChat: +WeChat group QR code From 520391643b7062f62cd58e987433d79a81ea0f3b Mon Sep 17 00:00:00 2001 From: Amir Mamaghani Date: Sat, 21 Mar 2026 15:14:32 +0100 Subject: [PATCH 08/10] feat: add agent-browser skill and Dockerfile.heavy with full runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add agent-browser skill to the default workspace with complete CLI reference for browser automation via Chrome/Chromium CDP. The skill includes a runtime guard that checks for the binary before use. Add Dockerfile.heavy — a batteries-included container image with: - Node.js 24 + npm - Python 3 + pip + uv - Chromium + Playwright (for agent-browser) - agent-browser CLI pre-installed - Non-root picoclaw user (UID/GID 1000) - Default workspace with all skills - Persistent workspace volume This complements the existing minimal Dockerfile and Dockerfile.full for deployments that need browser automation and rich tool support. --- docker/Dockerfile.heavy | 67 ++++++++++++ workspace/skills/agent-browser/SKILL.md | 129 ++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 docker/Dockerfile.heavy create mode 100644 workspace/skills/agent-browser/SKILL.md diff --git a/docker/Dockerfile.heavy b/docker/Dockerfile.heavy new file mode 100644 index 000000000..cbc243e39 --- /dev/null +++ b/docker/Dockerfile.heavy @@ -0,0 +1,67 @@ +# ============================================================ +# Stage 1: Build the picoclaw binary +# ============================================================ +FROM golang:1.26.0-alpine AS builder + +RUN apk add --no-cache git make + +WORKDIR /src + +# Cache dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source and build +COPY . . +RUN make build + +# ============================================================ +# Stage 2: Node.js runtime with Python + MCP support +# ============================================================ +FROM node:24-alpine3.23 + +RUN apk add --no-cache \ + ca-certificates \ + curl \ + git \ + python3 \ + py3-pip \ + chromium \ + jq + +# Install Playwright browsers for agent-browser +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers +RUN npm install -g agent-browser && \ + npx playwright install chromium && \ + chmod -R o+rx $PLAYWRIGHT_BROWSERS_PATH + +# Install uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + ln -s /root/.local/bin/uv /usr/local/bin/uv && \ + ln -s /root/.local/bin/uvx /usr/local/bin/uvx && \ + uv --version + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider http://localhost:18790/health || exit 1 + +# Copy binary +COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw + +# Reuse existing node user (UID/GID 1000) — rename to picoclaw +RUN deluser node 2>/dev/null; delgroup node 2>/dev/null; \ + addgroup -g 1000 picoclaw 2>/dev/null; \ + adduser -D -u 1000 -G picoclaw -h /home/picoclaw picoclaw 2>/dev/null || true + +USER picoclaw + +# Run onboard to create initial directories and config +RUN /usr/local/bin/picoclaw onboard + +# Copy default workspace +COPY --chown=picoclaw:picoclaw workspace/ /home/picoclaw/.picoclaw/workspace/ + +VOLUME /home/picoclaw/.picoclaw/workspace + +ENTRYPOINT ["picoclaw"] +CMD ["gateway"] diff --git a/workspace/skills/agent-browser/SKILL.md b/workspace/skills/agent-browser/SKILL.md new file mode 100644 index 000000000..43505996d --- /dev/null +++ b/workspace/skills/agent-browser/SKILL.md @@ -0,0 +1,129 @@ +--- +name: agent-browser +description: "Browser automation via agent-browser CLI. Use when the user needs to navigate websites, fill forms, click buttons, take screenshots, extract data, or test web apps." +metadata: {"nanobot":{"emoji":"🌐","requires":{"bins":["agent-browser"]},"install":[{"id":"npm","kind":"npm","package":"agent-browser","global":true,"bins":["agent-browser"],"label":"Install agent-browser (npm)"}]}} +--- + +# Agent Browser + +CLI browser automation via Chrome/Chromium CDP. Install: `npm i -g agent-browser && agent-browser install`. + +**Before using this skill**, verify the tool is available by running `which agent-browser`. If the command is not found, tell the user that browser automation requires the `agent-browser` CLI and Chromium, which are only available in the heavy container image. Do not attempt to install it at runtime. + +## Core Workflow + +1. `agent-browser open ` — navigate +2. `agent-browser snapshot -i` — get interactive elements with refs (`@e1`, `@e2`, ...) +3. Interact using refs — `click @e1`, `fill @e2 "text"` +4. Re-snapshot after any navigation or DOM change — refs are invalidated + +```bash +agent-browser open https://example.com/form +agent-browser snapshot -i +# @e1 [input] "Email", @e2 [input] "Password", @e3 [button] "Submit" +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "secret" +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser snapshot -i +``` + +Chain commands with `&&` when you don't need intermediate output: +```bash +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i +``` + +## Commands + +```bash +# Navigation +agent-browser open +agent-browser close + +# Snapshot +agent-browser snapshot -i # Interactive elements with refs +agent-browser snapshot -s "#selector" # Scope to CSS selector + +# Interaction (use @refs from snapshot) +agent-browser click @e1 +agent-browser fill @e2 "text" # Clear + type +agent-browser type @e2 "text" # Type without clearing +agent-browser select @e1 "option" +agent-browser check @e1 +agent-browser press Enter +agent-browser scroll down 500 + +# Get info +agent-browser get text @e1 +agent-browser get url +agent-browser get title + +# Wait +agent-browser wait @e1 # Wait for element +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --url "**/dashboard" # Wait for URL pattern +agent-browser wait --text "Welcome" # Wait for text +agent-browser wait 2000 # Wait ms + +# Capture +agent-browser screenshot # Screenshot to temp dir +agent-browser screenshot --full # Full page +agent-browser screenshot --annotate # With numbered element labels ([N] -> @eN) +agent-browser pdf output.pdf + +# Semantic locators (when refs unavailable) +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find role button click --name "Submit" +``` + +## Authentication + +```bash +# Option 1: Import from user's running Chrome +agent-browser --auto-connect state save ./auth.json +agent-browser --state ./auth.json open https://app.example.com + +# Option 2: Persistent profile +agent-browser --profile ~/.myapp open https://app.example.com/login +# ... login once, all future runs are authenticated + +# Option 3: Session name (auto-save/restore) +agent-browser --session-name myapp open https://app.example.com/login +# ... login, close, next run state is restored + +# Option 4: State file +agent-browser state save auth.json +agent-browser state load auth.json +``` + +## Iframes + +Iframe content is inlined in snapshots. Interact with iframe refs directly — no frame switch needed. + +## Parallel Sessions + +```bash +agent-browser --session s1 open https://site-a.com +agent-browser --session s2 open https://site-b.com +agent-browser session list +``` + +## JavaScript Eval + +```bash +agent-browser eval 'document.title' + +# Complex JS — use --stdin to avoid shell quoting issues +agent-browser eval --stdin <<'EVALEOF' +JSON.stringify(Array.from(document.querySelectorAll("a")).map(a => a.href)) +EVALEOF +``` + +## Cleanup + +Always close sessions when done: +```bash +agent-browser close +agent-browser --session s1 close +``` From f901af8cbc2d331fb500d24bf8b68a746538c4d9 Mon Sep 17 00:00:00 2001 From: Liu Yuan Date: Sat, 21 Mar 2026 22:38:03 +0800 Subject: [PATCH 09/10] feat(tools): add exec tool enhancement with background execution and PTY support (#1752) - Unified exec tool with actions: run/list/poll/read/write/send-keys/kill - PTY support using creack/pty library - Process session management with background execution - Process group kill for cleaning up child processes - Session cleanup: 30-minute TTL for old sessions - Output buffer: 100MB limit with truncation Actions: - run: execute command (sync or background) - list: list all sessions - poll: check session status - read: read session output - write: send input to session stdin - send-keys: send special keys (up, down, ctrl-c, enter, etc.) - kill: terminate session Tests: - PTY: allowed commands, write/read, poll, kill, process group kill - Non-PTY: background execution, list, read, write, poll, kill, process group kill - Session management: add/get/remove/list/cleanup --- go.mod | 3 +- go.sum | 1 + pkg/agent/instance_test.go | 5 +- pkg/tools/session.go | 252 +++++++ pkg/tools/session_process_unix.go | 14 + pkg/tools/session_process_windows.go | 13 + pkg/tools/session_test.go | 99 +++ pkg/tools/shell.go | 742 ++++++++++++++++++++- pkg/tools/shell_test.go | 962 ++++++++++++++++++++++++++- pkg/tools/shell_timeout_unix_test.go | 1 + pkg/tools/types.go | 21 + 11 files changed, 2082 insertions(+), 31 deletions(-) create mode 100644 pkg/tools/session.go create mode 100644 pkg/tools/session_process_unix.go create mode 100644 pkg/tools/session_process_windows.go create mode 100644 pkg/tools/session_test.go diff --git a/go.mod b/go.mod index cfc930d37..744e05e17 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/sipeed/picoclaw go 1.25.8 require ( - github.com/BurntSushi/toml v1.6.0 fyne.io/systray v1.12.0 + github.com/BurntSushi/toml v1.6.0 github.com/adhocore/gronx v1.19.6 github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 + github.com/creack/pty v1.1.9 github.com/ergochat/irc-go v0.6.0 github.com/ergochat/readline v0.1.3 github.com/gdamore/tcell/v2 v2.13.8 diff --git a/go.sum b/go.sum index f24b997d4..dc82d46ef 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go index b3318ad1f..84cfa81df 100644 --- a/pkg/agent/instance_test.go +++ b/pkg/agent/instance_test.go @@ -236,8 +236,9 @@ func TestNewAgentInstance_AllowsMediaTempDirForReadListAndExec(t *testing.T) { t.Fatal("exec tool not registered") } execResult := execTool.Execute(context.Background(), map[string]any{ - "command": "cat " + filepath.Base(mediaPath), - "working_dir": mediaDir, + "action": "run", + "command": "cat " + filepath.Base(mediaPath), + "cwd": mediaDir, }) if execResult.IsError { t.Fatalf("exec should allow media temp dir, got: %s", execResult.ForLLM) diff --git a/pkg/tools/session.go b/pkg/tools/session.go new file mode 100644 index 000000000..e32bc3ddf --- /dev/null +++ b/pkg/tools/session.go @@ -0,0 +1,252 @@ +package tools + +import ( + "bytes" + "errors" + "io" + "os" + "sync" + "time" + + "github.com/google/uuid" +) + +const maxOutputBufferSize = 100 * 1024 * 1024 // 100MB + +const outputTruncateMarker = "\n... [output truncated, exceeded 100MB]\n" + +// PtyKeyMode represents arrow key encoding mode for PTY sessions. +// Programs send smkx/rmkx sequences to switch between CSI and SS3 modes. +type PtyKeyMode uint8 + +const ( + PtyKeyModeCSI PtyKeyMode = iota // triggered by rmkx (\x1b[?1l) + PtyKeyModeSS3 // triggered by smkx (\x1b[?1h) +) + +const PtyKeyModeNotFound PtyKeyMode = 255 + +var ( + ErrSessionNotFound = errors.New("session not found") + ErrSessionDone = errors.New("session already completed") + ErrPTYNotSupported = errors.New("PTY is not supported on this platform") + ErrNoStdin = errors.New("no stdin available") +) + +type ProcessSession struct { + mu sync.Mutex + ID string + PID int + Command string + PTY bool + Background bool + StartTime int64 + ExitCode int + Status string + stdinWriter io.Writer + stdoutPipe io.Reader + outputBuffer *bytes.Buffer + outputTruncated bool + ptyMaster *os.File + + // ptyKeyMode tracks arrow key encoding mode (CSI vs SS3) + ptyKeyMode PtyKeyMode +} + +func (s *ProcessSession) IsDone() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.Status == "done" || s.Status == "exited" +} + +func (s *ProcessSession) GetPtyKeyMode() PtyKeyMode { + s.mu.Lock() + defer s.mu.Unlock() + return s.ptyKeyMode +} + +func (s *ProcessSession) SetPtyKeyMode(mode PtyKeyMode) { + s.mu.Lock() + defer s.mu.Unlock() + s.ptyKeyMode = mode +} + +func (s *ProcessSession) GetStatus() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.Status +} + +func (s *ProcessSession) SetStatus(status string) { + s.mu.Lock() + defer s.mu.Unlock() + s.Status = status +} + +func (s *ProcessSession) GetExitCode() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.ExitCode +} + +func (s *ProcessSession) SetExitCode(code int) { + s.mu.Lock() + defer s.mu.Unlock() + s.ExitCode = code +} + +func (s *ProcessSession) killProcess() error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.Status != "running" { + return ErrSessionDone + } + + pid := s.PID + if pid <= 0 { + return ErrSessionNotFound + } + + if err := killProcessGroup(pid); err != nil { + return err + } + + s.Status = "done" + s.ExitCode = -1 + return nil +} + +func (s *ProcessSession) Kill() error { + return s.killProcess() +} + +func (s *ProcessSession) Write(data string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.Status != "running" { + return ErrSessionDone + } + + var writer io.Writer + if s.PTY && s.ptyMaster != nil { + writer = s.ptyMaster + } else if s.stdinWriter != nil { + writer = s.stdinWriter + } else { + return ErrNoStdin + } + + _, err := writer.Write([]byte(data)) + return err +} + +func (s *ProcessSession) Read() string { + s.mu.Lock() + defer s.mu.Unlock() + + if s.outputBuffer.Len() == 0 { + return "" + } + + data := s.outputBuffer.String() + s.outputBuffer.Reset() + return data +} + +func (s *ProcessSession) ToSessionInfo() SessionInfo { + s.mu.Lock() + defer s.mu.Unlock() + + return SessionInfo{ + ID: s.ID, + Command: s.Command, + Status: s.Status, + PID: s.PID, + StartedAt: s.StartTime, + } +} + +type SessionManager struct { + mu sync.RWMutex + sessions map[string]*ProcessSession +} + +func NewSessionManager() *SessionManager { + sm := &SessionManager{ + sessions: make(map[string]*ProcessSession), + } + + // Start cleaner goroutine - runs every 5 minutes, cleans up sessions done for >30 minutes + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + sm.cleanupOldSessions() + } + }() + + return sm +} + +// cleanupOldSessions removes sessions that are done and older than 30 minutes +func (sm *SessionManager) cleanupOldSessions() { + sm.mu.Lock() + defer sm.mu.Unlock() + + cutoff := time.Now().Add(-30 * time.Minute) + for id, session := range sm.sessions { + if session.IsDone() && session.StartTime < cutoff.Unix() { + delete(sm.sessions, id) + } + } +} + +func (sm *SessionManager) Add(session *ProcessSession) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.sessions[session.ID] = session +} + +func (sm *SessionManager) Get(sessionID string) (*ProcessSession, error) { + sm.mu.RLock() + defer sm.mu.RUnlock() + + session, ok := sm.sessions[sessionID] + if !ok { + return nil, ErrSessionNotFound + } + + return session, nil +} + +func (sm *SessionManager) Remove(sessionID string) { + sm.mu.Lock() + defer sm.mu.Unlock() + delete(sm.sessions, sessionID) +} + +func (sm *SessionManager) List() []SessionInfo { + sm.mu.RLock() + defer sm.mu.RUnlock() + + result := make([]SessionInfo, 0, len(sm.sessions)) + for _, session := range sm.sessions { + result = append(result, session.ToSessionInfo()) + } + + return result +} + +func generateSessionID() string { + return uuid.New().String()[:8] +} + +type SessionInfo struct { + ID string `json:"id"` + Command string `json:"command"` + Status string `json:"status"` + PID int `json:"pid"` + StartedAt int64 `json:"startedAt"` +} diff --git a/pkg/tools/session_process_unix.go b/pkg/tools/session_process_unix.go new file mode 100644 index 000000000..2fe30166e --- /dev/null +++ b/pkg/tools/session_process_unix.go @@ -0,0 +1,14 @@ +//go:build !windows + +package tools + +import ( + "syscall" +) + +func killProcessGroup(pid int) error { + if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil { + _ = syscall.Kill(pid, syscall.SIGKILL) + } + return nil +} diff --git a/pkg/tools/session_process_windows.go b/pkg/tools/session_process_windows.go new file mode 100644 index 000000000..7cf558954 --- /dev/null +++ b/pkg/tools/session_process_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package tools + +import ( + "os/exec" + "strconv" +) + +func killProcessGroup(pid int) error { + _ = exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(pid)).Run() + return nil +} diff --git a/pkg/tools/session_test.go b/pkg/tools/session_test.go new file mode 100644 index 000000000..6cfe72a10 --- /dev/null +++ b/pkg/tools/session_test.go @@ -0,0 +1,99 @@ +package tools + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSessionManager_AddGet(t *testing.T) { + sm := NewSessionManager() + session := &ProcessSession{ + ID: "test-1", + Command: "echo hello", + Status: "running", + StartTime: 1000, + } + + sm.Add(session) + + got, err := sm.Get("test-1") + require.NoError(t, err) + require.Equal(t, "test-1", got.ID) +} + +func TestSessionManager_Remove(t *testing.T) { + sm := NewSessionManager() + session := &ProcessSession{ + ID: "test-1", + Command: "echo hello", + Status: "running", + StartTime: 1000, + } + sm.Add(session) + sm.Remove("test-1") + + _, err := sm.Get("test-1") + require.ErrorIs(t, err, ErrSessionNotFound) +} + +func TestSessionManager_List(t *testing.T) { + sm := NewSessionManager() + sm.Add(&ProcessSession{ + ID: "test-1", + Command: "echo hello", + Status: "running", + StartTime: 1000, + }) + sm.Add(&ProcessSession{ + ID: "test-2", + Command: "echo world", + Status: "running", + StartTime: 1001, + }) + sm.Add(&ProcessSession{ + ID: "test-3", + Command: "echo done", + Status: "done", + StartTime: 1002, + }) + + sessions := sm.List() + require.Len(t, sessions, 3) + + ids := make(map[string]bool) + for _, s := range sessions { + ids[s.ID] = true + } + require.True(t, ids["test-1"]) + require.True(t, ids["test-2"]) + require.True(t, ids["test-3"]) +} + +func TestProcessSession_IsDone(t *testing.T) { + session := &ProcessSession{Status: "running"} + require.False(t, session.IsDone()) + + session.Status = "done" + require.True(t, session.IsDone()) + + session.Status = "exited" + require.True(t, session.IsDone()) +} + +func TestProcessSession_ToSessionInfo(t *testing.T) { + session := &ProcessSession{ + ID: "test-1", + PID: 12345, + Command: "echo hello", + Status: "running", + StartTime: 1000, + } + + info := session.ToSessionInfo() + require.Equal(t, "test-1", info.ID) + require.Equal(t, "echo hello", info.Command) + require.Equal(t, "running", info.Status) + require.Equal(t, 12345, info.PID) + require.Equal(t, int64(1000), info.StartedAt) +} diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 78ad2b26d..f3869cc1c 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -3,20 +3,37 @@ package tools import ( "bytes" "context" + "encoding/json" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" + "sync" + "syscall" "time" + "github.com/creack/pty" + "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/constants" ) +var ( + globalSessionManager = NewSessionManager() + sessionManagerMu sync.RWMutex +) + +func getSessionManager() *SessionManager { + sessionManagerMu.RLock() + defer sessionManagerMu.RUnlock() + return globalSessionManager +} + type ExecTool struct { workingDir string timeout time.Duration @@ -26,6 +43,7 @@ type ExecTool struct { allowedPathPatterns []*regexp.Regexp restrictToWorkspace bool allowRemote bool + sessionManager *SessionManager } var ( @@ -145,7 +163,7 @@ func NewExecToolWithConfig( denyPatterns = append(denyPatterns, defaultDenyPatterns...) } - timeout := 60 * time.Second + var timeout time.Duration if config != nil && config.Tools.Exec.TimeoutSeconds > 0 { timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second } @@ -159,6 +177,7 @@ func NewExecToolWithConfig( allowedPathPatterns: allowedPathPatterns, restrictToWorkspace: restrict, allowRemote: allowRemote, + sessionManager: getSessionManager(), }, nil } @@ -167,27 +186,146 @@ func (t *ExecTool) Name() string { } func (t *ExecTool) Description() string { - return "Execute a shell command and return its output. Use with caution." + return `Execute shell commands. Use background=true for long-running commands (returns sessionId). Use pty=true for interactive commands (can combine with background=true). Use poll/read/write/send-keys/kill with sessionId to manage background sessions. Sessions auto-cleanup 30 minutes after process exits; use kill to terminate early. Output buffer limit: 100MB.` } func (t *ExecTool) Parameters() map[string]any { return map[string]any{ - "type": "object", - "properties": map[string]any{ - "command": map[string]any{ - "type": "string", - "description": "The shell command to execute", + "oneOf": []map[string]any{ + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{"const": "run", "description": "Execute a shell command"}, + "command": map[string]any{"type": "string", "description": "Shell command to execute"}, + "background": map[string]any{ + "type": "string", + "description": "Run in background immediately", + }, + "pty": map[string]any{ + "type": "string", + "description": "Run in a pseudo-terminal (PTY) when available", + }, + "cwd": map[string]any{ + "type": "string", + "description": "Working directory for the command", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 0 = no timeout, kills process on expiry)", + }, + }, + "required": []string{"action", "command"}, }, - "working_dir": map[string]any{ - "type": "string", - "description": "Optional working directory for the command", + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{"const": "list", "description": "List all active sessions"}, + }, + "required": []string{"action"}, + }, + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "const": "poll", + "description": "Check session status. Returns: {sessionId, status: running|done, exitCode}. exitCode only meaningful when status=done", + }, + "sessionId": map[string]any{ + "type": "string", + "description": "Session ID returned from background command", + }, + }, + "required": []string{"action", "sessionId"}, + }, + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "const": "read", + "description": "Read output from session. Returns: {sessionId, output, status: running|done}", + }, + "sessionId": map[string]any{ + "type": "string", + "description": "Session ID returned from background command", + }, + }, + "required": []string{"action", "sessionId"}, + }, + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "const": "write", + "description": "Send input to session stdin (only when status=running)", + }, + "sessionId": map[string]any{ + "type": "string", + "description": "Session ID returned from background command", + }, + "data": map[string]any{"type": "string", "description": "Data to write to session stdin."}, + }, + "required": []string{"action", "sessionId", "data"}, + }, + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{"const": "kill", "description": "Terminate session"}, + "sessionId": map[string]any{ + "type": "string", + "description": "Session ID returned from background command", + }, + }, + "required": []string{"action", "sessionId"}, + }, + { + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "const": "send-keys", + "description": "Send special keys to PTY session. Keys: down/up/left/right/enter/escape/tab/backspace/ctrl-c/ctrl-d/ctrl-z. Multiple keys separated by comma", + }, + "sessionId": map[string]any{ + "type": "string", + "description": "Session ID returned from background command", + }, + "keys": map[string]any{ + "type": "string", + "description": "Comma-separated key names (optional spaces around comma). Valid keys: up, down, left, right, enter, tab, escape, backspace, ctrl-c, ctrl-d, home, end, pageup, pagedown, f1-f12.", + }, + }, + "required": []string{"action", "sessionId", "keys"}, }, }, - "required": []string{"command"}, } } func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult { + action, _ := args["action"].(string) + if action == "" { + return ErrorResult("action is required") + } + + switch action { + case "run": + return t.executeRun(ctx, args) + case "list": + return t.executeList() + case "poll": + return t.executePoll(args) + case "read": + return t.executeRead(args) + case "write": + return t.executeWrite(args) + case "kill": + return t.executeKill(args) + case "send-keys": + return t.executeSendKeys(args) + default: + return ErrorResult(fmt.Sprintf("unknown action: %s", action)) + } +} + +func (t *ExecTool) executeRun(ctx context.Context, args map[string]any) *ToolResult { command, ok := args["command"].(string) if !ok { return ErrorResult("command is required") @@ -206,8 +344,26 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult } } + getBoolArg := func(key string) bool { + switch v := args[key].(type) { + case bool: + return v + case string: + return v == "true" + } + return false + } + isPty := getBoolArg("pty") + isBackground := getBoolArg("background") + + if isPty { + if runtime.GOOS == "windows" { + return ErrorResult("PTY is not supported on Windows. Use background=true without pty.") + } + } + cwd := t.workingDir - if wd, ok := args["working_dir"].(string); ok && wd != "" { + if wd, ok := args["cwd"].(string); ok && wd != "" { if t.restrictToWorkspace && t.workingDir != "" { resolvedWD, err := validatePathWithAllowPaths(wd, t.workingDir, true, t.allowedPathPatterns) if err != nil { @@ -253,6 +409,14 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult } } + if isBackground { + return t.runBackground(ctx, command, cwd, isPty) + } + + return t.runSync(ctx, command, cwd) +} + +func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult { // timeout == 0 means no timeout var cmdCtx context.Context var cancel context.CancelFunc @@ -361,6 +525,560 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult } } +func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEnabled bool) *ToolResult { + sessionID := generateSessionID() + session := &ProcessSession{ + ID: sessionID, + Command: command, + PTY: ptyEnabled, + Background: true, + StartTime: time.Now().Unix(), + Status: "running", + ptyKeyMode: PtyKeyModeCSI, + } + + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command) + } else { + cmd = exec.Command("sh", "-c", command) + } + if cwd != "" { + cmd.Dir = cwd + } + + prepareCommandForTermination(cmd) + + var stdoutReader io.ReadCloser + var stderrReader io.ReadCloser + var stdinWriter io.WriteCloser + + if ptyEnabled { + ptmx, tty, err := pty.Open() + if err != nil { + return ErrorResult(fmt.Sprintf("failed to create PTY: %v", err)) + } + + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + // For PTY, we need Setsid to create a new session. + // Note: Setsid and Setpgid conflict, so we must replace SysProcAttr entirely. + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + + session.ptyMaster = ptmx + } else { + var err error + stdoutReader, err = cmd.StdoutPipe() + if err != nil { + return ErrorResult(fmt.Sprintf("failed to create stdout pipe: %v", err)) + } + stderrReader, err = cmd.StderrPipe() + if err != nil { + return ErrorResult(fmt.Sprintf("failed to create stderr pipe: %v", err)) + } + stdinWriter, err = cmd.StdinPipe() + if err != nil { + return ErrorResult(fmt.Sprintf("failed to create stdin pipe: %v", err)) + } + session.stdoutPipe = io.MultiReader(stdoutReader, stderrReader) + session.stdinWriter = stdinWriter + } + + if err := cmd.Start(); err != nil { + if session.ptyMaster != nil { + session.ptyMaster.Close() + } + return ErrorResult(fmt.Sprintf("failed to start command: %v", err)) + } + + session.PID = cmd.Process.Pid + t.sessionManager.Add(session) + + session.outputBuffer = &bytes.Buffer{} + + // PTY mode: read from ptyMaster and wait for process + // Note: On Linux, closing ptyMaster doesn't interrupt blocking Read() calls, + // so we need cmd.Wait() in a separate goroutine to detect process exit. + if session.PTY && session.ptyMaster != nil { + go func() { + cmd.Wait() // Wait for process to exit + session.mu.Lock() + if cmd.ProcessState != nil { + session.ExitCode = cmd.ProcessState.ExitCode() + } + session.Status = "done" + session.mu.Unlock() + }() + + go func() { + buf := make([]byte, 4096) + for { + n, err := session.ptyMaster.Read(buf) + if n > 0 { + raw := string(buf[:n]) + if mode := detectPtyKeyMode(raw); mode != PtyKeyModeNotFound && mode != session.GetPtyKeyMode() { + session.SetPtyKeyMode(mode) + } + + session.mu.Lock() + if session.outputBuffer.Len() >= maxOutputBufferSize { + if !session.outputTruncated { + session.outputBuffer.WriteString(outputTruncateMarker) + session.outputTruncated = true + } + } else { + session.outputBuffer.Write(buf[:n]) + } + session.mu.Unlock() + } + if err != nil { + break + } + } + }() + } else { + // Non-PTY mode: single goroutine reads pipes. + // When Read() returns EOF (pipe closed), we break. + // When process exits, OS closes pipe write end → Read() returns EOF → we exit. + go func() { + buf := make([]byte, 4096) + + // Read stdout + for { + n, err := stdoutReader.Read(buf) + if n > 0 { + session.mu.Lock() + if session.outputBuffer.Len() >= maxOutputBufferSize { + if !session.outputTruncated { + session.outputBuffer.WriteString(outputTruncateMarker) + session.outputTruncated = true + } + } else { + session.outputBuffer.Write(buf[:n]) + } + session.mu.Unlock() + } + if err != nil { + break + } + } + + // Read stderr + for { + n, err := stderrReader.Read(buf) + if n > 0 { + session.mu.Lock() + if session.outputBuffer.Len() >= maxOutputBufferSize { + if !session.outputTruncated { + session.outputBuffer.WriteString(outputTruncateMarker) + session.outputTruncated = true + } + } else { + session.outputBuffer.Write(buf[:n]) + } + session.mu.Unlock() + } + if err != nil { + break + } + } + + // All pipes closed, get exit status + if stdinWriter != nil { + stdinWriter.Close() + } + cmd.Wait() + + session.mu.Lock() + if cmd.ProcessState != nil { + session.ExitCode = cmd.ProcessState.ExitCode() + } + session.Status = "done" + session.mu.Unlock() + }() + } + + resp := ExecResponse{ + SessionID: sessionID, + Status: "running", + } + data, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(data), + ForUser: fmt.Sprintf("Session %s started", sessionID), + IsError: false, + } +} + +func (t *ExecTool) executeList() *ToolResult { + sessions := t.sessionManager.List() + resp := ExecResponse{ + Sessions: sessions, + } + data, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(data), + ForUser: fmt.Sprintf("%d active sessions", len(sessions)), + IsError: false, + } +} + +func (t *ExecTool) executePoll(args map[string]any) *ToolResult { + sessionID, ok := args["sessionId"].(string) + if !ok { + return ErrorResult("sessionId is required") + } + + session, err := t.sessionManager.Get(sessionID) + if err != nil { + if errors.Is(err, ErrSessionNotFound) { + return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) + } + return ErrorResult(err.Error()) + } + + resp := ExecResponse{ + SessionID: sessionID, + Status: session.GetStatus(), + ExitCode: session.GetExitCode(), + } + data, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(data), + IsError: false, + } +} + +func (t *ExecTool) executeRead(args map[string]any) *ToolResult { + sessionID, ok := args["sessionId"].(string) + if !ok { + return ErrorResult("sessionId is required") + } + + session, err := t.sessionManager.Get(sessionID) + if err != nil { + if errors.Is(err, ErrSessionNotFound) { + return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) + } + return ErrorResult(err.Error()) + } + + output := session.Read() + + resp := ExecResponse{ + SessionID: sessionID, + Output: output, + Status: session.GetStatus(), + } + data, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(data), + IsError: false, + } +} + +func (t *ExecTool) executeWrite(args map[string]any) *ToolResult { + sessionID, ok := args["sessionId"].(string) + if !ok { + return ErrorResult("sessionId is required") + } + + data, ok := args["data"].(string) + if !ok { + return ErrorResult("data is required") + } + + session, err := t.sessionManager.Get(sessionID) + if err != nil { + if errors.Is(err, ErrSessionNotFound) { + return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) + } + return ErrorResult(err.Error()) + } + + if session.IsDone() { + return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) + } + + if err := session.Write(data); err != nil { + if errors.Is(err, ErrSessionDone) { + return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) + } + return ErrorResult(fmt.Sprintf("failed to write to session: %v", err)) + } + + resp := ExecResponse{ + SessionID: sessionID, + Status: session.GetStatus(), + } + respData, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(respData), + IsError: false, + } +} + +func (t *ExecTool) executeKill(args map[string]any) *ToolResult { + sessionID, ok := args["sessionId"].(string) + if !ok { + return ErrorResult("sessionId is required") + } + + session, err := t.sessionManager.Get(sessionID) + if err != nil { + if errors.Is(err, ErrSessionNotFound) { + return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) + } + return ErrorResult(err.Error()) + } + + if session.IsDone() { + return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) + } + + if err := session.Kill(); err != nil { + return ErrorResult(fmt.Sprintf("failed to kill session: %v", err)) + } + + t.sessionManager.Remove(sessionID) + + resp := ExecResponse{ + SessionID: sessionID, + Status: "done", + } + data, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(data), + ForUser: fmt.Sprintf("Session %s killed", sessionID), + IsError: false, + } +} + +// keyMap maps key names to their escape sequences. +var keyMap = map[string]string{ + "enter": "\r", + "return": "\r", + "tab": "\t", + "escape": "\x1b", + "esc": "\x1b", + "space": " ", + "backspace": "\x7f", + "bspace": "\x7f", + "up": "\x1b[A", + "down": "\x1b[B", + "right": "\x1b[C", + "left": "\x1b[D", + "home": "\x1b[1~", + "end": "\x1b[4~", + "pageup": "\x1b[5~", + "pagedown": "\x1b[6~", + "pgup": "\x1b[5~", + "pgdn": "\x1b[6~", + "insert": "\x1b[2~", + "ic": "\x1b[2~", + "delete": "\x1b[3~", + "del": "\x1b[3~", + "dc": "\x1b[3~", + "btab": "\x1b[Z", + "f1": "\x1bOP", + "f2": "\x1bOQ", + "f3": "\x1bOR", + "f4": "\x1bOS", + "f5": "\x1b[15~", + "f6": "\x1b[17~", + "f7": "\x1b[18~", + "f8": "\x1b[19~", + "f9": "\x1b[20~", + "f10": "\x1b[21~", + "f11": "\x1b[23~", + "f12": "\x1b[24~", +} + +// ss3KeysMap maps key names to SS3 escape sequences +var ss3KeysMap = map[string]string{ + "up": "\x1bOA", + "down": "\x1bOB", + "right": "\x1bOC", + "left": "\x1bOD", + "home": "\x1bOH", + "end": "\x1bOF", +} + +func detectPtyKeyMode(raw string) PtyKeyMode { + const SMKX = "\x1b[?1h" + const RMKX = "\x1b[?1l" + + lastSmkx := strings.LastIndex(raw, SMKX) + lastRmkx := strings.LastIndex(raw, RMKX) + + if lastSmkx == -1 && lastRmkx == -1 { + return PtyKeyModeNotFound + } + + if lastSmkx > lastRmkx { + return PtyKeyModeSS3 + } + return PtyKeyModeCSI +} + +// encodeKeyToken encodes a single key token into its escape sequence. +// Supports: +// - Named keys: "enter", "tab", "up", "ctrl-c", "alt-x", etc. +// - Ctrl modifier: "ctrl-c" or "c-c" (sends Ctrl+char) +// - Alt modifier: "alt-x" or "m-x" (sends ESC+char) +func encodeKeyToken(token string, ptyKeyMode PtyKeyMode) (string, error) { + token = strings.ToLower(strings.TrimSpace(token)) + if token == "" { + return "", nil + } + + // Handle ctrl-X format (c-x) + if strings.HasPrefix(token, "c-") { + char := token[2] + if char >= 'a' && char <= 'z' { + return string(rune(char) & 0x1f), nil // ctrl-a through ctrl-z + } + return "", fmt.Errorf("invalid ctrl key: %s", token) + } + + // Handle ctrl-X format (ctrl-x) + if strings.HasPrefix(token, "ctrl-") { + char := token[5] + if char >= 'a' && char <= 'z' { + return string(rune(char) & 0x1f), nil + } + return "", fmt.Errorf("invalid ctrl key: %s", token) + } + + // Handle alt-X format (m-x or alt-x) + if strings.HasPrefix(token, "m-") || strings.HasPrefix(token, "alt-") { + var char string + if strings.HasPrefix(token, "m-") { + char = token[2:] + } else { + char = token[4:] + } + if len(char) == 1 { + return "\x1b" + char, nil + } + return "", fmt.Errorf("invalid alt key: %s", token) + } + + // Handle shift modifier for special keys (shift-up, shift-down, etc.) + if strings.HasPrefix(token, "s-") || strings.HasPrefix(token, "shift-") { + var key string + if strings.HasPrefix(token, "s-") { + key = token[2:] + } else { + key = token[6:] + } + // Apply shift modifier: for single-char keys, return uppercase + if seq, ok := keyMap[key]; ok { + // For escape sequences, we can't easily add shift + // For single-char keys (letters), return uppercase + if len(seq) == 1 { + return strings.ToUpper(seq), nil + } + return seq, nil + } + return "", fmt.Errorf("unknown key with shift: %s", key) + } + + if ptyKeyMode == PtyKeyModeSS3 { + if seq, ok := ss3KeysMap[token]; ok { + return seq, nil + } + } + + if seq, ok := keyMap[token]; ok { + return seq, nil + } + + return "", fmt.Errorf("unknown key: %s (use write action for text input)", token) +} + +// encodeKeySequence encodes a slice of key tokens into a single string. +func encodeKeySequence(tokens []string, ptyKeyMode PtyKeyMode) (string, error) { + var result string + for _, token := range tokens { + seq, err := encodeKeyToken(token, ptyKeyMode) + if err != nil { + return "", err + } + result += seq + } + return result, nil +} + +func (t *ExecTool) executeSendKeys(args map[string]any) *ToolResult { + sessionID, ok := args["sessionId"].(string) + if !ok { + return ErrorResult("sessionId is required") + } + + keysStr, ok := args["keys"].(string) + if !ok { + return ErrorResult("keys must be a string") + } + + if keysStr == "" { + return ErrorResult("keys cannot be empty") + } + + // Parse comma-separated key names + keyNames := strings.Split(keysStr, ",") + var keys []string + for _, k := range keyNames { + k = strings.TrimSpace(k) + if k != "" { + keys = append(keys, k) + } + } + + if len(keys) == 0 { + return ErrorResult("keys cannot be empty") + } + + session, err := t.sessionManager.Get(sessionID) + if err != nil { + if errors.Is(err, ErrSessionNotFound) { + return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) + } + return ErrorResult(err.Error()) + } + + ptyKeyMode := session.GetPtyKeyMode() + + data, err := encodeKeySequence(keys, ptyKeyMode) + if err != nil { + return ErrorResult(fmt.Sprintf("invalid key: %v", err)) + } + + if session.IsDone() { + return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) + } + + if err := session.Write(data); err != nil { + if errors.Is(err, ErrSessionDone) { + return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) + } + return ErrorResult(fmt.Sprintf("failed to send keys: %v", err)) + } + + resp := ExecResponse{ + SessionID: sessionID, + Status: "running", + Output: fmt.Sprintf("Sent keys: %v", keys), + } + respData, _ := json.Marshal(resp) + return &ToolResult{ + ForLLM: string(respData), + IsError: false, + } +} + func (t *ExecTool) guardCommand(command, cwd string) string { cmd := strings.TrimSpace(command) lower := strings.ToLower(cmd) diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go index f8f83ea74..a8de2f4c9 100644 --- a/pkg/tools/shell_test.go +++ b/pkg/tools/shell_test.go @@ -2,12 +2,16 @@ package tools import ( "context" + "encoding/json" "os" "path/filepath" + "runtime" "strings" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/sipeed/picoclaw/pkg/config" ) @@ -20,6 +24,7 @@ func TestShellTool_Success(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "echo 'hello world'", } @@ -50,6 +55,7 @@ func TestShellTool_Failure(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "ls /nonexistent_directory_12345", } @@ -82,6 +88,7 @@ func TestShellTool_Timeout(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "sleep 10", } @@ -112,8 +119,9 @@ func TestShellTool_WorkingDir(t *testing.T) { ctx := context.Background() args := map[string]any{ - "command": "cat test.txt", - "working_dir": tmpDir, + "action": "run", + "command": "cat test.txt", + "cwd": tmpDir, } result := tool.Execute(ctx, args) @@ -136,6 +144,7 @@ func TestShellTool_DangerousCommand(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "rm -rf /", } @@ -159,6 +168,7 @@ func TestShellTool_DangerousCommand_KillBlocked(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "kill 12345", } @@ -198,6 +208,7 @@ func TestShellTool_StderrCapture(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "sh -c 'echo stdout; echo stderr >&2'", } @@ -222,6 +233,7 @@ func TestShellTool_OutputTruncation(t *testing.T) { ctx := context.Background() // Generate long output (>10000 chars) args := map[string]any{ + "action": "run", "command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000), } @@ -251,8 +263,9 @@ func TestShellTool_WorkingDir_OutsideWorkspace(t *testing.T) { } result := tool.Execute(context.Background(), map[string]any{ - "command": "pwd", - "working_dir": outsideDir, + "action": "run", + "command": "pwd", + "cwd": outsideDir, }) if !result.IsError { @@ -289,8 +302,9 @@ func TestShellTool_WorkingDir_SymlinkEscape(t *testing.T) { } result := tool.Execute(context.Background(), map[string]any{ - "command": "cat secret.txt", - "working_dir": link, + "action": "run", + "command": "cat secret.txt", + "cwd": link, }) if !result.IsError { @@ -312,7 +326,7 @@ func TestShellTool_RemoteChannelBlockedByDefault(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "telegram", "chat-1") - result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) if !result.IsError { t.Fatal("expected remote-channel exec to be blocked") @@ -333,7 +347,7 @@ func TestShellTool_InternalChannelAllowed(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "cli", "direct") - result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) if result.IsError { t.Fatalf("expected internal channel exec to succeed, got: %s", result.ForLLM) @@ -373,7 +387,7 @@ func TestShellTool_AllowRemoteBypassesChannelCheck(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "telegram", "chat-1") - result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) if result.IsError { t.Fatalf("expected allowRemote=true to permit remote channel, got: %s", result.ForLLM) @@ -392,6 +406,7 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "cat ../../etc/passwd", } @@ -429,7 +444,7 @@ func TestShellTool_DevNullAllowed(t *testing.T) { } for _, cmd := range commands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "blocked") { t.Errorf("command should not be blocked: %s\n error: %s", cmd, result.ForLLM) } @@ -458,7 +473,7 @@ func TestShellTool_BlockDevices(t *testing.T) { } for _, cmd := range blocked { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if !result.IsError { t.Errorf("expected block device write to be blocked: %s", cmd) } @@ -482,7 +497,7 @@ func TestShellTool_SafePathsInWorkspaceRestriction(t *testing.T) { } for _, cmd := range commands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("safe path should not be blocked by workspace check: %s\n error: %s", cmd, result.ForLLM) } @@ -498,6 +513,7 @@ func TestShellTool_ExitCodeDetails(t *testing.T) { ctx := context.Background() args := map[string]any{ + "action": "run", "command": "sh -c 'exit 42'", } @@ -534,6 +550,7 @@ func TestShellTool_TimeoutWithPartialOutput(t *testing.T) { ctx := context.Background() // Use a command that outputs immediately then sleeps args := map[string]any{ + "action": "run", "command": "echo 'partial output before timeout' && sleep 30", } @@ -608,7 +625,9 @@ func TestShellTool_URLsNotBlocked(t *testing.T) { } for _, cmd := range commands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + result := tool.Execute(ctx, map[string]any{"action": "run", "command": cmd}) + cancel() if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("command with URL should not be blocked by workspace check: %s\n error: %s", cmd, result.ForLLM) } @@ -633,7 +652,7 @@ func TestShellTool_FileURISandboxing(t *testing.T) { } for _, cmd := range blockedCommands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if !result.IsError || !strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("file:// URI outside workspace should be blocked: %s", cmd) } @@ -651,7 +670,7 @@ func TestShellTool_FileURISandboxing(t *testing.T) { } for _, cmd := range allowedCommands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("file:// URI inside workspace should be allowed: %s\n error: %s", cmd, result.ForLLM) } @@ -677,9 +696,920 @@ func TestShellTool_URLBypassPrevented(t *testing.T) { } for _, cmd := range blockedCommands { - result := tool.Execute(context.Background(), map[string]any{"command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) if !result.IsError || !strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("bypass attempt should be blocked: %q\n got: %s", cmd, result.ForLLM) } } } + +func TestShellTool_Background_ReturnsImmediately(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + ctx := context.Background() + args := map[string]any{ + "action": "run", + "command": "sleep 5", + "background": "true", + } + + start := time.Now() + result := tool.Execute(ctx, args) + elapsed := time.Since(start) + + require.False(t, result.IsError, "background run should not error: %s", result.ForLLM) + require.Less(t, elapsed, time.Second, "background run should return immediately") + require.Contains(t, result.ForLLM, "sessionId") +} + +func TestShellTool_List_Empty(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := context.Background() + args := map[string]any{"action": "list"} + + result := tool.Execute(ctx, args) + require.False(t, result.IsError) + require.Contains(t, result.ForUser, "0 active sessions") +} + +func TestShellTool_RunBackground_List(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 10", + "background": "true", + }) + require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &resp) + require.NoError(t, err) + require.NotEmpty(t, resp.SessionID) + + time.Sleep(100 * time.Millisecond) + + listResult := tool.Execute(ctx, map[string]any{"action": "list"}) + require.False(t, listResult.IsError) + + var listResp ExecResponse + err = json.Unmarshal([]byte(listResult.ForLLM), &listResp) + require.NoError(t, err) + require.Len(t, listResp.Sessions, 1) + require.Equal(t, resp.SessionID, listResp.Sessions[0].ID) + + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) +} + +func TestShellTool_Read_Output(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "echo hello", + "background": "true", + }) + require.False(t, runResult.IsError) + + var resp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &resp) + require.NoError(t, err) + + time.Sleep(200 * time.Millisecond) + + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": resp.SessionID, + }) + + if !readResult.IsError { + var readResp ExecResponse + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + } +} + +func TestShellTool_Kill(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 100", + "background": "true", + }) + require.False(t, runResult.IsError) + + var resp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &resp) + require.NoError(t, err) + + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) + + time.Sleep(100 * time.Millisecond) + + listResult := tool.Execute(ctx, map[string]any{"action": "list"}) + var listResp ExecResponse + err = json.Unmarshal([]byte(listResult.ForLLM), &listResp) + require.NoError(t, err) + require.Len(t, listResp.Sessions, 0) +} + +func TestShellTool_PTY_AllowedCommands(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Test that PTY is allowed for non-interpreter commands + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "cat", + "pty": "true", + "background": "true", + }) + require.False(t, result.IsError, "PTY with cat should succeed: %s", result.ForLLM) + require.Contains(t, result.ForLLM, "sessionId") + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + require.NotEmpty(t, resp.SessionID) + + // Clean up + tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) +} + +func TestShellTool_PTY_WriteRead(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a PTY session with a command that waits for input + // Using 'cat' which will wait for stdin + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "cat", + "pty": "true", + "background": "true", + }) + require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Write some input to cat + writeResult := tool.Execute(ctx, map[string]any{ + "action": "write", + "sessionId": resp.SessionID, + "data": "hello\n", + }) + require.False(t, writeResult.IsError, "write should succeed: %s", writeResult.ForLLM) + + // Give cat time to process and output + time.Sleep(200 * time.Millisecond) + + // Read the output + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": resp.SessionID, + }) + + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + + var readResp ExecResponse + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + // PTY output should contain "hello" + require.Contains(t, readResp.Output, "hello") + + // Clean up + tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) +} + +func TestShellTool_PTY_Poll(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a PTY session with a long-running command + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 2", + "pty": "true", + "background": "true", + }) + require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Poll should show running + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.False(t, pollResult.IsError, "poll should succeed: %s", pollResult.ForLLM) + + var pollResp ExecResponse + err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) + require.NoError(t, err) + require.Equal(t, "running", pollResp.Status) + + // Wait for sleep to complete + time.Sleep(2500 * time.Millisecond) + + // Poll should show done + pollResult = tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.False(t, pollResult.IsError) + + err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) + require.NoError(t, err) + require.Equal(t, "done", pollResp.Status) +} + +func TestShellTool_PTY_Kill(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a PTY session with a long-running command + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 10", + "pty": "true", + "background": "true", + }) + require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Kill the session + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) + + // Verify kill response shows done status + var killResp ExecResponse + err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) + require.NoError(t, err) + require.Equal(t, "done", killResp.Status) + + // Poll should return error since session is removed after kill + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + // Session is removed after kill, so poll returns error with "session not found" + require.True(t, pollResult.IsError, "poll should error after kill (session removed)") + require.Contains(t, pollResult.ForLLM, "session not found") +} + +func TestShellTool_Write_Read_NonPTY(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a background process that reads from stdin and outputs it + // Using 'cat' which echoes stdin to stdout + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "cat", + "pty": false, + "background": "true", + }) + require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Write some input to cat + writeResult := tool.Execute(ctx, map[string]any{ + "action": "write", + "sessionId": resp.SessionID, + "data": "hello world\n", + }) + require.False(t, writeResult.IsError, "write should succeed: %s", writeResult.ForLLM) + + // Give cat time to process and output + time.Sleep(200 * time.Millisecond) + + // Read the output + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": resp.SessionID, + }) + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + + var readResp ExecResponse + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + require.Contains(t, readResp.Output, "hello world") + + // Clean up + tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) +} + +func TestShellTool_Read_NonPTY_Running(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a long-running process that produces output over time + // Using sh -c with sleep at the end so process doesn't exit immediately + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sh -c 'echo line1; sleep 0.5; echo line2; sleep 0.5; echo line3; sleep 10'", + "pty": false, + "background": "true", + }) + require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Give time for first outputs to be produced + time.Sleep(300 * time.Millisecond) + + // Read output while process is running + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": resp.SessionID, + }) + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + + var readResp ExecResponse + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + // Should have at least line1 + require.Contains(t, readResp.Output, "line1") + + // Wait for line3 to be produced (line1=0s, line2=0.5s, line3=1s, then sleep 10) + time.Sleep(1200 * time.Millisecond) + + // Read again - should have line3 as well + readResult = tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": resp.SessionID, + }) + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + require.Contains(t, readResp.Output, "line3") + + // Clean up + tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) +} + +func TestShellTool_ProcessGroupKill(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Process group kill not supported on Windows") + } + + // Note: Testing process group kill with PTY is tricky because the command + // must be run through an interpreter (sh, bash) which is blocked for PTY. + // Instead, we test with non-PTY mode which also uses Setsid for background processes. + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a shell that spawns child processes (non-PTY mode) + // The sh -c command creates child sleep processes + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sh -c 'sleep 30 & sleep 30 & wait'", + "pty": false, + "background": "true", + }) + require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Give time for child processes to spawn + time.Sleep(500 * time.Millisecond) + + // Kill the session - should kill the entire process group + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) + + // Verify kill response shows done status + var killResp ExecResponse + err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) + require.NoError(t, err) + require.Equal(t, "done", killResp.Status) + + // Poll should return error since session is removed after kill + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.True(t, pollResult.IsError, "poll should error after kill (session removed)") + require.Contains(t, pollResult.ForLLM, "session not found") +} + +func TestShellTool_PTY_ProcessGroupKill(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY process group kill not supported on Windows") + } + + // This test binary creates 4 child sleep processes and waits for signals. + // It's not an interpreter, so it's allowed with PTY mode. + // The binary is created in /tmp/test_pgroup.c and compiled as part of test setup. + testBinary := "/tmp/test_pgroup" + if _, err := os.Stat(testBinary); os.IsNotExist(err) { + t.Skip("Test binary /tmp/test_pgroup not found - run: gcc -o /tmp/test_pgroup /tmp/test_pgroup.c") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start the test binary with PTY mode + // It forks 4 child sleep processes and waits for signals + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": testBinary, + "pty": "true", + "background": "true", + }) + require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) + + var resp ExecResponse + err = json.Unmarshal([]byte(result.ForLLM), &resp) + require.NoError(t, err) + + // Give time for child processes to spawn + time.Sleep(500 * time.Millisecond) + + // Kill the session - should kill the entire process group + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": resp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) + + // Verify kill response shows done status + var killResp ExecResponse + err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) + require.NoError(t, err) + require.Equal(t, "done", killResp.Status) + + // Poll should return error since session is removed after kill + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.True(t, pollResult.IsError, "poll should error after kill (session removed)") + require.Contains(t, pollResult.ForLLM, "session not found") +} + +func TestShellTool_PTY_Background_Read(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a fast command with PTY + background mode + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "echo hello", + "pty": "true", + "background": "true", + }) + require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) + + var runResp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &runResp) + require.NoError(t, err) + require.NotEmpty(t, runResp.SessionID) + require.Equal(t, "running", runResp.Status) + + // Wait for command to complete + time.Sleep(500 * time.Millisecond) + + // Read output - this is the key test: PTY + background mode should preserve output + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": runResp.SessionID, + }) + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + require.Contains(t, readResult.ForLLM, "hello", "output should contain 'hello'") +} + +func TestShellTool_PTY_Background_ReadNoBlock(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PTY not supported on Windows") + } + + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + // Start a long-running command with PTY + background mode + // This command produces no output, just sleeps + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 10", + "pty": "true", + "background": "true", + }) + require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) + + var runResp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &runResp) + require.NoError(t, err) + require.NotEmpty(t, runResp.SessionID) + + // Read immediately - should NOT block even though process is running and has no output + // This tests that Read() returns quickly (within 1 second) instead of blocking for 10 seconds + start := time.Now() + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": runResp.SessionID, + }) + elapsed := time.Since(start) + + require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) + require.Less(t, elapsed.Seconds(), 1.0, "read should not block, should return within 1 second") + + // Kill the session to clean up + killResult := tool.Execute(ctx, map[string]any{ + "action": "kill", + "sessionId": runResp.SessionID, + }) + require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) +} + +func TestShellTool_Poll_Status(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + sm := NewSessionManager() + tool.sessionManager = sm + + ctx := WithToolContext(context.Background(), "cli", "test") + + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "sleep 1", + "background": "true", + }) + require.False(t, runResult.IsError) + + var resp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &resp) + require.NoError(t, err) + + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.False(t, pollResult.IsError) + + var pollResp ExecResponse + err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) + require.NoError(t, err) + require.Equal(t, "running", pollResp.Status) + + time.Sleep(1200 * time.Millisecond) + + pollResult = tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": resp.SessionID, + }) + require.False(t, pollResult.IsError) + + err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) + require.NoError(t, err) + require.Equal(t, "done", pollResp.Status) +} + +func TestShellTool_Action_Run_Sync(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + ctx := context.Background() + + result := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "echo hello", + }) + + require.False(t, result.IsError) + require.Contains(t, result.ForLLM, "hello") +} + +// TestShellTool_Background_ReadAfterExit verifies that we can read +// buffered output even after the background process has exited. +func TestShellTool_Background_ReadAfterExit(t *testing.T) { + tool, err := NewExecTool("", false) + require.NoError(t, err) + + ctx := context.Background() + + // Start a background command that produces output and exits quickly + runResult := tool.Execute(ctx, map[string]any{ + "action": "run", + "command": "echo hello && sleep 1 && echo world", + "background": "true", + }) + require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForUser) + + // Parse session ID from response + var resp ExecResponse + err = json.Unmarshal([]byte(runResult.ForLLM), &resp) + require.NoError(t, err) + require.NotEmpty(t, resp.SessionID) + sessionID := resp.SessionID + + // Wait for process to exit (sleep 1 + some buffer) + time.Sleep(1500 * time.Millisecond) + + // Poll to verify process is done + pollResult := tool.Execute(ctx, map[string]any{ + "action": "poll", + "sessionId": sessionID, + }) + require.False(t, pollResult.IsError, "poll should succeed: %s", pollResult.ForLLM) + var pollResp ExecResponse + err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) + require.NoError(t, err) + require.Equal(t, "done", pollResp.Status, "process should be done") + + // Try to read output AFTER process has exited + readResult := tool.Execute(ctx, map[string]any{ + "action": "read", + "sessionId": sessionID, + }) + require.False(t, readResult.IsError, "read should succeed after exit: %s", readResult.ForLLM) + + var readResp ExecResponse + err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) + require.NoError(t, err) + + // Output should contain both "hello" and "world" + require.Contains(t, readResp.Output, "hello", "should contain hello") + require.Contains(t, readResp.Output, "world", "should contain world after sleep") +} + +func TestSendKeys_CtrlC(t *testing.T) { + // Note: Ctrl-C as a signal requires sending SIGINT to the process group, + // which requires elevated privileges. Writing "\x03" to PTY passes the byte + // to the process but doesn't generate SIGINT for processes that don't read stdin. + // For interrupting processes, use the kill action instead. + t.Skip("Ctrl-C as signal not supported - use kill action for interruption") +} + +func TestEncodeKeyToken(t *testing.T) { + tests := []struct { + token string + expected string + hasError bool + }{ + // Named keys + {"enter", "\r", false}, + {"return", "\r", false}, + {"tab", "\t", false}, + {"escape", "\x1b", false}, + {"esc", "\x1b", false}, + {"backspace", "\x7f", false}, + {"up", "\x1b[A", false}, + {"down", "\x1b[B", false}, + {"left", "\x1b[D", false}, + {"right", "\x1b[C", false}, + {"home", "\x1b[1~", false}, + {"end", "\x1b[4~", false}, + {"pageup", "\x1b[5~", false}, + {"pagedown", "\x1b[6~", false}, + {"delete", "\x1b[3~", false}, + {"f1", "\x1bOP", false}, + {"f12", "\x1b[24~", false}, + + // Ctrl keys + {"ctrl-c", "\x03", false}, + {"ctrl-d", "\x04", false}, + {"ctrl-a", "\x01", false}, + {"ctrl-z", "\x1a", false}, + {"c-c", "\x03", false}, + {"c-d", "\x04", false}, + + // Alt keys + {"alt-x", "\x1bx", false}, + {"m-x", "\x1bx", false}, + + // Case insensitive tests + {"ENTER", "\r", false}, + {"TAB", "\t", false}, + {"CTRL-C", "\x03", false}, + {"Ctrl-D", "\x04", false}, + {"ALT-X", "\x1bx", false}, + {"M-X", "\x1bx", false}, + {"UP", "\x1b[A", false}, + {"DOWN", "\x1b[B", false}, + + // Unknown keys should return error (use write action for text input) + {"unknown-key", "", true}, + } + + for _, tt := range tests { + t.Run(tt.token, func(t *testing.T) { + result, err := encodeKeyToken(tt.token, PtyKeyModeCSI) + if tt.hasError { + require.Error(t, err, "expected error for %s", tt.token) + } else { + require.NoError(t, err, "unexpected error for %s", tt.token) + require.Equal(t, tt.expected, result, "wrong encoding for %s", tt.token) + } + }) + } +} + +// TestDetectPtyKeyMode tests smkx/rmkx detection in PTY output +func TestDetectPtyKeyMode(t *testing.T) { + tests := []struct { + name string + raw string + expected PtyKeyMode + }{ + {"no toggle", "hello world", PtyKeyModeNotFound}, + {"smkx only", "\x1b[?1h\x1b=", PtyKeyModeSS3}, + {"rmkx only", "\x1b[?1l\x1b>", PtyKeyModeCSI}, + {"both smkx first", "\x1b[?1h\x1b=...\x1b[?1l\x1b>", PtyKeyModeCSI}, + {"both rmkx first", "\x1b[?1l\x1b>...\x1b[?1h\x1b=", PtyKeyModeSS3}, + {"multiple toggles smkx last", "\x1b[?1h\x1b=...\x1b[?1l\x1b>...\x1b[?1h\x1b=", PtyKeyModeSS3}, + {"multiple toggles rmkx last", "\x1b[?1l\x1b>...\x1b[?1h\x1b=...\x1b[?1l\x1b>", PtyKeyModeCSI}, + {"partial smkx", "\x1b[?1h", PtyKeyModeSS3}, + {"partial rmkx", "\x1b[?1l", PtyKeyModeCSI}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detectPtyKeyMode(tt.raw) + require.Equal(t, tt.expected, result, "wrong mode for %s", tt.name) + }) + } +} + +func TestEncodeKeyTokenWithPtyKeyMode(t *testing.T) { + tests := []struct { + name string + token string + mode PtyKeyMode + expected string + hasError bool + }{ + // CSI mode + {"up csi", "up", PtyKeyModeCSI, "\x1b[A", false}, + {"down csi", "down", PtyKeyModeCSI, "\x1b[B", false}, + {"left csi", "left", PtyKeyModeCSI, "\x1b[D", false}, + {"right csi", "right", PtyKeyModeCSI, "\x1b[C", false}, + + // SS3 mode + {"up ss3", "up", PtyKeyModeSS3, "\x1bOA", false}, + {"down ss3", "down", PtyKeyModeSS3, "\x1bOB", false}, + {"left ss3", "left", PtyKeyModeSS3, "\x1bOD", false}, + {"right ss3", "right", PtyKeyModeSS3, "\x1bOC", false}, + {"home ss3", "home", PtyKeyModeSS3, "\x1bOH", false}, + {"end ss3", "end", PtyKeyModeSS3, "\x1bOF", false}, + + // Other keys unaffected by mode + {"enter ss3", "enter", PtyKeyModeSS3, "\r", false}, + {"tab ss3", "tab", PtyKeyModeSS3, "\t", false}, + {"ctrl-c ss3", "ctrl-c", PtyKeyModeSS3, "\x03", false}, + + // NotFound behaves like CSI + {"up notfound", "up", PtyKeyModeNotFound, "\x1b[A", false}, + {"down notfound", "down", PtyKeyModeNotFound, "\x1b[B", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := encodeKeyToken(tt.token, tt.mode) + if tt.hasError { + require.Error(t, err, "expected error for %s", tt.name) + } else { + require.NoError(t, err, "unexpected error for %s", tt.name) + require.Equal(t, tt.expected, result, "wrong encoding for %s", tt.name) + } + }) + } +} diff --git a/pkg/tools/shell_timeout_unix_test.go b/pkg/tools/shell_timeout_unix_test.go index 357e1276e..dfd28454c 100644 --- a/pkg/tools/shell_timeout_unix_test.go +++ b/pkg/tools/shell_timeout_unix_test.go @@ -30,6 +30,7 @@ func TestShellTool_TimeoutKillsChildProcess(t *testing.T) { tool.SetTimeout(500 * time.Millisecond) args := map[string]any{ + "action": "run", // Spawn a child process that would outlive the shell unless process-group kill is used. "command": "sleep 60 & echo $! > child.pid; wait", } diff --git a/pkg/tools/types.go b/pkg/tools/types.go index a6015cde3..4d1a18d5a 100644 --- a/pkg/tools/types.go +++ b/pkg/tools/types.go @@ -56,3 +56,24 @@ type ToolFunctionDefinition struct { Description string `json:"description"` Parameters map[string]any `json:"parameters"` } + +type ExecRequest struct { + Action string `json:"action"` + Command string `json:"command,omitempty"` + PTY bool `json:"pty,omitempty"` + Background bool `json:"background,omitempty"` + Timeout int `json:"timeout,omitempty"` + Env map[string]string `json:"env,omitempty"` + Cwd string `json:"cwd,omitempty"` + SessionID string `json:"sessionId,omitempty"` + Data string `json:"data,omitempty"` +} + +type ExecResponse struct { + SessionID string `json:"sessionId,omitempty"` + Status string `json:"status,omitempty"` + ExitCode int `json:"exitCode,omitempty"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` + Sessions []SessionInfo `json:"sessions,omitempty"` +} From ebcd5645f1e0cebace2ec48f9ad3aa7b348c4970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?daming=E5=A4=A7=E9=93=AD?= Date: Sun, 22 Mar 2026 00:39:47 +0800 Subject: [PATCH 10/10] =?UTF-8?q?Revert=20"feat(tools):=20add=20exec=20too?= =?UTF-8?q?l=20enhancement=20with=20background=20execution=20and=20?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f901af8cbc2d331fb500d24bf8b68a746538c4d9. --- go.mod | 3 +- go.sum | 1 - pkg/agent/instance_test.go | 5 +- pkg/tools/session.go | 252 ------- pkg/tools/session_process_unix.go | 14 - pkg/tools/session_process_windows.go | 13 - pkg/tools/session_test.go | 99 --- pkg/tools/shell.go | 742 +-------------------- pkg/tools/shell_test.go | 962 +-------------------------- pkg/tools/shell_timeout_unix_test.go | 1 - pkg/tools/types.go | 21 - 11 files changed, 31 insertions(+), 2082 deletions(-) delete mode 100644 pkg/tools/session.go delete mode 100644 pkg/tools/session_process_unix.go delete mode 100644 pkg/tools/session_process_windows.go delete mode 100644 pkg/tools/session_test.go diff --git a/go.mod b/go.mod index 744e05e17..cfc930d37 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,12 @@ module github.com/sipeed/picoclaw go 1.25.8 require ( - fyne.io/systray v1.12.0 github.com/BurntSushi/toml v1.6.0 + fyne.io/systray v1.12.0 github.com/adhocore/gronx v1.19.6 github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 - github.com/creack/pty v1.1.9 github.com/ergochat/irc-go v0.6.0 github.com/ergochat/readline v0.1.3 github.com/gdamore/tcell/v2 v2.13.8 diff --git a/go.sum b/go.sum index dc82d46ef..f24b997d4 100644 --- a/go.sum +++ b/go.sum @@ -37,7 +37,6 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go index 84cfa81df..b3318ad1f 100644 --- a/pkg/agent/instance_test.go +++ b/pkg/agent/instance_test.go @@ -236,9 +236,8 @@ func TestNewAgentInstance_AllowsMediaTempDirForReadListAndExec(t *testing.T) { t.Fatal("exec tool not registered") } execResult := execTool.Execute(context.Background(), map[string]any{ - "action": "run", - "command": "cat " + filepath.Base(mediaPath), - "cwd": mediaDir, + "command": "cat " + filepath.Base(mediaPath), + "working_dir": mediaDir, }) if execResult.IsError { t.Fatalf("exec should allow media temp dir, got: %s", execResult.ForLLM) diff --git a/pkg/tools/session.go b/pkg/tools/session.go deleted file mode 100644 index e32bc3ddf..000000000 --- a/pkg/tools/session.go +++ /dev/null @@ -1,252 +0,0 @@ -package tools - -import ( - "bytes" - "errors" - "io" - "os" - "sync" - "time" - - "github.com/google/uuid" -) - -const maxOutputBufferSize = 100 * 1024 * 1024 // 100MB - -const outputTruncateMarker = "\n... [output truncated, exceeded 100MB]\n" - -// PtyKeyMode represents arrow key encoding mode for PTY sessions. -// Programs send smkx/rmkx sequences to switch between CSI and SS3 modes. -type PtyKeyMode uint8 - -const ( - PtyKeyModeCSI PtyKeyMode = iota // triggered by rmkx (\x1b[?1l) - PtyKeyModeSS3 // triggered by smkx (\x1b[?1h) -) - -const PtyKeyModeNotFound PtyKeyMode = 255 - -var ( - ErrSessionNotFound = errors.New("session not found") - ErrSessionDone = errors.New("session already completed") - ErrPTYNotSupported = errors.New("PTY is not supported on this platform") - ErrNoStdin = errors.New("no stdin available") -) - -type ProcessSession struct { - mu sync.Mutex - ID string - PID int - Command string - PTY bool - Background bool - StartTime int64 - ExitCode int - Status string - stdinWriter io.Writer - stdoutPipe io.Reader - outputBuffer *bytes.Buffer - outputTruncated bool - ptyMaster *os.File - - // ptyKeyMode tracks arrow key encoding mode (CSI vs SS3) - ptyKeyMode PtyKeyMode -} - -func (s *ProcessSession) IsDone() bool { - s.mu.Lock() - defer s.mu.Unlock() - return s.Status == "done" || s.Status == "exited" -} - -func (s *ProcessSession) GetPtyKeyMode() PtyKeyMode { - s.mu.Lock() - defer s.mu.Unlock() - return s.ptyKeyMode -} - -func (s *ProcessSession) SetPtyKeyMode(mode PtyKeyMode) { - s.mu.Lock() - defer s.mu.Unlock() - s.ptyKeyMode = mode -} - -func (s *ProcessSession) GetStatus() string { - s.mu.Lock() - defer s.mu.Unlock() - return s.Status -} - -func (s *ProcessSession) SetStatus(status string) { - s.mu.Lock() - defer s.mu.Unlock() - s.Status = status -} - -func (s *ProcessSession) GetExitCode() int { - s.mu.Lock() - defer s.mu.Unlock() - return s.ExitCode -} - -func (s *ProcessSession) SetExitCode(code int) { - s.mu.Lock() - defer s.mu.Unlock() - s.ExitCode = code -} - -func (s *ProcessSession) killProcess() error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.Status != "running" { - return ErrSessionDone - } - - pid := s.PID - if pid <= 0 { - return ErrSessionNotFound - } - - if err := killProcessGroup(pid); err != nil { - return err - } - - s.Status = "done" - s.ExitCode = -1 - return nil -} - -func (s *ProcessSession) Kill() error { - return s.killProcess() -} - -func (s *ProcessSession) Write(data string) error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.Status != "running" { - return ErrSessionDone - } - - var writer io.Writer - if s.PTY && s.ptyMaster != nil { - writer = s.ptyMaster - } else if s.stdinWriter != nil { - writer = s.stdinWriter - } else { - return ErrNoStdin - } - - _, err := writer.Write([]byte(data)) - return err -} - -func (s *ProcessSession) Read() string { - s.mu.Lock() - defer s.mu.Unlock() - - if s.outputBuffer.Len() == 0 { - return "" - } - - data := s.outputBuffer.String() - s.outputBuffer.Reset() - return data -} - -func (s *ProcessSession) ToSessionInfo() SessionInfo { - s.mu.Lock() - defer s.mu.Unlock() - - return SessionInfo{ - ID: s.ID, - Command: s.Command, - Status: s.Status, - PID: s.PID, - StartedAt: s.StartTime, - } -} - -type SessionManager struct { - mu sync.RWMutex - sessions map[string]*ProcessSession -} - -func NewSessionManager() *SessionManager { - sm := &SessionManager{ - sessions: make(map[string]*ProcessSession), - } - - // Start cleaner goroutine - runs every 5 minutes, cleans up sessions done for >30 minutes - go func() { - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - for range ticker.C { - sm.cleanupOldSessions() - } - }() - - return sm -} - -// cleanupOldSessions removes sessions that are done and older than 30 minutes -func (sm *SessionManager) cleanupOldSessions() { - sm.mu.Lock() - defer sm.mu.Unlock() - - cutoff := time.Now().Add(-30 * time.Minute) - for id, session := range sm.sessions { - if session.IsDone() && session.StartTime < cutoff.Unix() { - delete(sm.sessions, id) - } - } -} - -func (sm *SessionManager) Add(session *ProcessSession) { - sm.mu.Lock() - defer sm.mu.Unlock() - sm.sessions[session.ID] = session -} - -func (sm *SessionManager) Get(sessionID string) (*ProcessSession, error) { - sm.mu.RLock() - defer sm.mu.RUnlock() - - session, ok := sm.sessions[sessionID] - if !ok { - return nil, ErrSessionNotFound - } - - return session, nil -} - -func (sm *SessionManager) Remove(sessionID string) { - sm.mu.Lock() - defer sm.mu.Unlock() - delete(sm.sessions, sessionID) -} - -func (sm *SessionManager) List() []SessionInfo { - sm.mu.RLock() - defer sm.mu.RUnlock() - - result := make([]SessionInfo, 0, len(sm.sessions)) - for _, session := range sm.sessions { - result = append(result, session.ToSessionInfo()) - } - - return result -} - -func generateSessionID() string { - return uuid.New().String()[:8] -} - -type SessionInfo struct { - ID string `json:"id"` - Command string `json:"command"` - Status string `json:"status"` - PID int `json:"pid"` - StartedAt int64 `json:"startedAt"` -} diff --git a/pkg/tools/session_process_unix.go b/pkg/tools/session_process_unix.go deleted file mode 100644 index 2fe30166e..000000000 --- a/pkg/tools/session_process_unix.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !windows - -package tools - -import ( - "syscall" -) - -func killProcessGroup(pid int) error { - if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil { - _ = syscall.Kill(pid, syscall.SIGKILL) - } - return nil -} diff --git a/pkg/tools/session_process_windows.go b/pkg/tools/session_process_windows.go deleted file mode 100644 index 7cf558954..000000000 --- a/pkg/tools/session_process_windows.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build windows - -package tools - -import ( - "os/exec" - "strconv" -) - -func killProcessGroup(pid int) error { - _ = exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(pid)).Run() - return nil -} diff --git a/pkg/tools/session_test.go b/pkg/tools/session_test.go deleted file mode 100644 index 6cfe72a10..000000000 --- a/pkg/tools/session_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package tools - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestSessionManager_AddGet(t *testing.T) { - sm := NewSessionManager() - session := &ProcessSession{ - ID: "test-1", - Command: "echo hello", - Status: "running", - StartTime: 1000, - } - - sm.Add(session) - - got, err := sm.Get("test-1") - require.NoError(t, err) - require.Equal(t, "test-1", got.ID) -} - -func TestSessionManager_Remove(t *testing.T) { - sm := NewSessionManager() - session := &ProcessSession{ - ID: "test-1", - Command: "echo hello", - Status: "running", - StartTime: 1000, - } - sm.Add(session) - sm.Remove("test-1") - - _, err := sm.Get("test-1") - require.ErrorIs(t, err, ErrSessionNotFound) -} - -func TestSessionManager_List(t *testing.T) { - sm := NewSessionManager() - sm.Add(&ProcessSession{ - ID: "test-1", - Command: "echo hello", - Status: "running", - StartTime: 1000, - }) - sm.Add(&ProcessSession{ - ID: "test-2", - Command: "echo world", - Status: "running", - StartTime: 1001, - }) - sm.Add(&ProcessSession{ - ID: "test-3", - Command: "echo done", - Status: "done", - StartTime: 1002, - }) - - sessions := sm.List() - require.Len(t, sessions, 3) - - ids := make(map[string]bool) - for _, s := range sessions { - ids[s.ID] = true - } - require.True(t, ids["test-1"]) - require.True(t, ids["test-2"]) - require.True(t, ids["test-3"]) -} - -func TestProcessSession_IsDone(t *testing.T) { - session := &ProcessSession{Status: "running"} - require.False(t, session.IsDone()) - - session.Status = "done" - require.True(t, session.IsDone()) - - session.Status = "exited" - require.True(t, session.IsDone()) -} - -func TestProcessSession_ToSessionInfo(t *testing.T) { - session := &ProcessSession{ - ID: "test-1", - PID: 12345, - Command: "echo hello", - Status: "running", - StartTime: 1000, - } - - info := session.ToSessionInfo() - require.Equal(t, "test-1", info.ID) - require.Equal(t, "echo hello", info.Command) - require.Equal(t, "running", info.Status) - require.Equal(t, 12345, info.PID) - require.Equal(t, int64(1000), info.StartedAt) -} diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index f3869cc1c..78ad2b26d 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -3,37 +3,20 @@ package tools import ( "bytes" "context" - "encoding/json" "errors" "fmt" - "io" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" - "sync" - "syscall" "time" - "github.com/creack/pty" - "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/constants" ) -var ( - globalSessionManager = NewSessionManager() - sessionManagerMu sync.RWMutex -) - -func getSessionManager() *SessionManager { - sessionManagerMu.RLock() - defer sessionManagerMu.RUnlock() - return globalSessionManager -} - type ExecTool struct { workingDir string timeout time.Duration @@ -43,7 +26,6 @@ type ExecTool struct { allowedPathPatterns []*regexp.Regexp restrictToWorkspace bool allowRemote bool - sessionManager *SessionManager } var ( @@ -163,7 +145,7 @@ func NewExecToolWithConfig( denyPatterns = append(denyPatterns, defaultDenyPatterns...) } - var timeout time.Duration + timeout := 60 * time.Second if config != nil && config.Tools.Exec.TimeoutSeconds > 0 { timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second } @@ -177,7 +159,6 @@ func NewExecToolWithConfig( allowedPathPatterns: allowedPathPatterns, restrictToWorkspace: restrict, allowRemote: allowRemote, - sessionManager: getSessionManager(), }, nil } @@ -186,146 +167,27 @@ func (t *ExecTool) Name() string { } func (t *ExecTool) Description() string { - return `Execute shell commands. Use background=true for long-running commands (returns sessionId). Use pty=true for interactive commands (can combine with background=true). Use poll/read/write/send-keys/kill with sessionId to manage background sessions. Sessions auto-cleanup 30 minutes after process exits; use kill to terminate early. Output buffer limit: 100MB.` + return "Execute a shell command and return its output. Use with caution." } func (t *ExecTool) Parameters() map[string]any { return map[string]any{ - "oneOf": []map[string]any{ - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{"const": "run", "description": "Execute a shell command"}, - "command": map[string]any{"type": "string", "description": "Shell command to execute"}, - "background": map[string]any{ - "type": "string", - "description": "Run in background immediately", - }, - "pty": map[string]any{ - "type": "string", - "description": "Run in a pseudo-terminal (PTY) when available", - }, - "cwd": map[string]any{ - "type": "string", - "description": "Working directory for the command", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds (default: 0 = no timeout, kills process on expiry)", - }, - }, - "required": []string{"action", "command"}, + "type": "object", + "properties": map[string]any{ + "command": map[string]any{ + "type": "string", + "description": "The shell command to execute", }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{"const": "list", "description": "List all active sessions"}, - }, - "required": []string{"action"}, - }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{ - "const": "poll", - "description": "Check session status. Returns: {sessionId, status: running|done, exitCode}. exitCode only meaningful when status=done", - }, - "sessionId": map[string]any{ - "type": "string", - "description": "Session ID returned from background command", - }, - }, - "required": []string{"action", "sessionId"}, - }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{ - "const": "read", - "description": "Read output from session. Returns: {sessionId, output, status: running|done}", - }, - "sessionId": map[string]any{ - "type": "string", - "description": "Session ID returned from background command", - }, - }, - "required": []string{"action", "sessionId"}, - }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{ - "const": "write", - "description": "Send input to session stdin (only when status=running)", - }, - "sessionId": map[string]any{ - "type": "string", - "description": "Session ID returned from background command", - }, - "data": map[string]any{"type": "string", "description": "Data to write to session stdin."}, - }, - "required": []string{"action", "sessionId", "data"}, - }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{"const": "kill", "description": "Terminate session"}, - "sessionId": map[string]any{ - "type": "string", - "description": "Session ID returned from background command", - }, - }, - "required": []string{"action", "sessionId"}, - }, - { - "type": "object", - "properties": map[string]any{ - "action": map[string]any{ - "const": "send-keys", - "description": "Send special keys to PTY session. Keys: down/up/left/right/enter/escape/tab/backspace/ctrl-c/ctrl-d/ctrl-z. Multiple keys separated by comma", - }, - "sessionId": map[string]any{ - "type": "string", - "description": "Session ID returned from background command", - }, - "keys": map[string]any{ - "type": "string", - "description": "Comma-separated key names (optional spaces around comma). Valid keys: up, down, left, right, enter, tab, escape, backspace, ctrl-c, ctrl-d, home, end, pageup, pagedown, f1-f12.", - }, - }, - "required": []string{"action", "sessionId", "keys"}, + "working_dir": map[string]any{ + "type": "string", + "description": "Optional working directory for the command", }, }, + "required": []string{"command"}, } } func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult { - action, _ := args["action"].(string) - if action == "" { - return ErrorResult("action is required") - } - - switch action { - case "run": - return t.executeRun(ctx, args) - case "list": - return t.executeList() - case "poll": - return t.executePoll(args) - case "read": - return t.executeRead(args) - case "write": - return t.executeWrite(args) - case "kill": - return t.executeKill(args) - case "send-keys": - return t.executeSendKeys(args) - default: - return ErrorResult(fmt.Sprintf("unknown action: %s", action)) - } -} - -func (t *ExecTool) executeRun(ctx context.Context, args map[string]any) *ToolResult { command, ok := args["command"].(string) if !ok { return ErrorResult("command is required") @@ -344,26 +206,8 @@ func (t *ExecTool) executeRun(ctx context.Context, args map[string]any) *ToolRes } } - getBoolArg := func(key string) bool { - switch v := args[key].(type) { - case bool: - return v - case string: - return v == "true" - } - return false - } - isPty := getBoolArg("pty") - isBackground := getBoolArg("background") - - if isPty { - if runtime.GOOS == "windows" { - return ErrorResult("PTY is not supported on Windows. Use background=true without pty.") - } - } - cwd := t.workingDir - if wd, ok := args["cwd"].(string); ok && wd != "" { + if wd, ok := args["working_dir"].(string); ok && wd != "" { if t.restrictToWorkspace && t.workingDir != "" { resolvedWD, err := validatePathWithAllowPaths(wd, t.workingDir, true, t.allowedPathPatterns) if err != nil { @@ -409,14 +253,6 @@ func (t *ExecTool) executeRun(ctx context.Context, args map[string]any) *ToolRes } } - if isBackground { - return t.runBackground(ctx, command, cwd, isPty) - } - - return t.runSync(ctx, command, cwd) -} - -func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult { // timeout == 0 means no timeout var cmdCtx context.Context var cancel context.CancelFunc @@ -525,560 +361,6 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult } } -func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEnabled bool) *ToolResult { - sessionID := generateSessionID() - session := &ProcessSession{ - ID: sessionID, - Command: command, - PTY: ptyEnabled, - Background: true, - StartTime: time.Now().Unix(), - Status: "running", - ptyKeyMode: PtyKeyModeCSI, - } - - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command) - } else { - cmd = exec.Command("sh", "-c", command) - } - if cwd != "" { - cmd.Dir = cwd - } - - prepareCommandForTermination(cmd) - - var stdoutReader io.ReadCloser - var stderrReader io.ReadCloser - var stdinWriter io.WriteCloser - - if ptyEnabled { - ptmx, tty, err := pty.Open() - if err != nil { - return ErrorResult(fmt.Sprintf("failed to create PTY: %v", err)) - } - - cmd.Stdin = tty - cmd.Stdout = tty - cmd.Stderr = tty - - // For PTY, we need Setsid to create a new session. - // Note: Setsid and Setpgid conflict, so we must replace SysProcAttr entirely. - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} - - session.ptyMaster = ptmx - } else { - var err error - stdoutReader, err = cmd.StdoutPipe() - if err != nil { - return ErrorResult(fmt.Sprintf("failed to create stdout pipe: %v", err)) - } - stderrReader, err = cmd.StderrPipe() - if err != nil { - return ErrorResult(fmt.Sprintf("failed to create stderr pipe: %v", err)) - } - stdinWriter, err = cmd.StdinPipe() - if err != nil { - return ErrorResult(fmt.Sprintf("failed to create stdin pipe: %v", err)) - } - session.stdoutPipe = io.MultiReader(stdoutReader, stderrReader) - session.stdinWriter = stdinWriter - } - - if err := cmd.Start(); err != nil { - if session.ptyMaster != nil { - session.ptyMaster.Close() - } - return ErrorResult(fmt.Sprintf("failed to start command: %v", err)) - } - - session.PID = cmd.Process.Pid - t.sessionManager.Add(session) - - session.outputBuffer = &bytes.Buffer{} - - // PTY mode: read from ptyMaster and wait for process - // Note: On Linux, closing ptyMaster doesn't interrupt blocking Read() calls, - // so we need cmd.Wait() in a separate goroutine to detect process exit. - if session.PTY && session.ptyMaster != nil { - go func() { - cmd.Wait() // Wait for process to exit - session.mu.Lock() - if cmd.ProcessState != nil { - session.ExitCode = cmd.ProcessState.ExitCode() - } - session.Status = "done" - session.mu.Unlock() - }() - - go func() { - buf := make([]byte, 4096) - for { - n, err := session.ptyMaster.Read(buf) - if n > 0 { - raw := string(buf[:n]) - if mode := detectPtyKeyMode(raw); mode != PtyKeyModeNotFound && mode != session.GetPtyKeyMode() { - session.SetPtyKeyMode(mode) - } - - session.mu.Lock() - if session.outputBuffer.Len() >= maxOutputBufferSize { - if !session.outputTruncated { - session.outputBuffer.WriteString(outputTruncateMarker) - session.outputTruncated = true - } - } else { - session.outputBuffer.Write(buf[:n]) - } - session.mu.Unlock() - } - if err != nil { - break - } - } - }() - } else { - // Non-PTY mode: single goroutine reads pipes. - // When Read() returns EOF (pipe closed), we break. - // When process exits, OS closes pipe write end → Read() returns EOF → we exit. - go func() { - buf := make([]byte, 4096) - - // Read stdout - for { - n, err := stdoutReader.Read(buf) - if n > 0 { - session.mu.Lock() - if session.outputBuffer.Len() >= maxOutputBufferSize { - if !session.outputTruncated { - session.outputBuffer.WriteString(outputTruncateMarker) - session.outputTruncated = true - } - } else { - session.outputBuffer.Write(buf[:n]) - } - session.mu.Unlock() - } - if err != nil { - break - } - } - - // Read stderr - for { - n, err := stderrReader.Read(buf) - if n > 0 { - session.mu.Lock() - if session.outputBuffer.Len() >= maxOutputBufferSize { - if !session.outputTruncated { - session.outputBuffer.WriteString(outputTruncateMarker) - session.outputTruncated = true - } - } else { - session.outputBuffer.Write(buf[:n]) - } - session.mu.Unlock() - } - if err != nil { - break - } - } - - // All pipes closed, get exit status - if stdinWriter != nil { - stdinWriter.Close() - } - cmd.Wait() - - session.mu.Lock() - if cmd.ProcessState != nil { - session.ExitCode = cmd.ProcessState.ExitCode() - } - session.Status = "done" - session.mu.Unlock() - }() - } - - resp := ExecResponse{ - SessionID: sessionID, - Status: "running", - } - data, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(data), - ForUser: fmt.Sprintf("Session %s started", sessionID), - IsError: false, - } -} - -func (t *ExecTool) executeList() *ToolResult { - sessions := t.sessionManager.List() - resp := ExecResponse{ - Sessions: sessions, - } - data, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(data), - ForUser: fmt.Sprintf("%d active sessions", len(sessions)), - IsError: false, - } -} - -func (t *ExecTool) executePoll(args map[string]any) *ToolResult { - sessionID, ok := args["sessionId"].(string) - if !ok { - return ErrorResult("sessionId is required") - } - - session, err := t.sessionManager.Get(sessionID) - if err != nil { - if errors.Is(err, ErrSessionNotFound) { - return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) - } - return ErrorResult(err.Error()) - } - - resp := ExecResponse{ - SessionID: sessionID, - Status: session.GetStatus(), - ExitCode: session.GetExitCode(), - } - data, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(data), - IsError: false, - } -} - -func (t *ExecTool) executeRead(args map[string]any) *ToolResult { - sessionID, ok := args["sessionId"].(string) - if !ok { - return ErrorResult("sessionId is required") - } - - session, err := t.sessionManager.Get(sessionID) - if err != nil { - if errors.Is(err, ErrSessionNotFound) { - return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) - } - return ErrorResult(err.Error()) - } - - output := session.Read() - - resp := ExecResponse{ - SessionID: sessionID, - Output: output, - Status: session.GetStatus(), - } - data, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(data), - IsError: false, - } -} - -func (t *ExecTool) executeWrite(args map[string]any) *ToolResult { - sessionID, ok := args["sessionId"].(string) - if !ok { - return ErrorResult("sessionId is required") - } - - data, ok := args["data"].(string) - if !ok { - return ErrorResult("data is required") - } - - session, err := t.sessionManager.Get(sessionID) - if err != nil { - if errors.Is(err, ErrSessionNotFound) { - return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) - } - return ErrorResult(err.Error()) - } - - if session.IsDone() { - return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) - } - - if err := session.Write(data); err != nil { - if errors.Is(err, ErrSessionDone) { - return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) - } - return ErrorResult(fmt.Sprintf("failed to write to session: %v", err)) - } - - resp := ExecResponse{ - SessionID: sessionID, - Status: session.GetStatus(), - } - respData, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(respData), - IsError: false, - } -} - -func (t *ExecTool) executeKill(args map[string]any) *ToolResult { - sessionID, ok := args["sessionId"].(string) - if !ok { - return ErrorResult("sessionId is required") - } - - session, err := t.sessionManager.Get(sessionID) - if err != nil { - if errors.Is(err, ErrSessionNotFound) { - return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) - } - return ErrorResult(err.Error()) - } - - if session.IsDone() { - return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) - } - - if err := session.Kill(); err != nil { - return ErrorResult(fmt.Sprintf("failed to kill session: %v", err)) - } - - t.sessionManager.Remove(sessionID) - - resp := ExecResponse{ - SessionID: sessionID, - Status: "done", - } - data, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(data), - ForUser: fmt.Sprintf("Session %s killed", sessionID), - IsError: false, - } -} - -// keyMap maps key names to their escape sequences. -var keyMap = map[string]string{ - "enter": "\r", - "return": "\r", - "tab": "\t", - "escape": "\x1b", - "esc": "\x1b", - "space": " ", - "backspace": "\x7f", - "bspace": "\x7f", - "up": "\x1b[A", - "down": "\x1b[B", - "right": "\x1b[C", - "left": "\x1b[D", - "home": "\x1b[1~", - "end": "\x1b[4~", - "pageup": "\x1b[5~", - "pagedown": "\x1b[6~", - "pgup": "\x1b[5~", - "pgdn": "\x1b[6~", - "insert": "\x1b[2~", - "ic": "\x1b[2~", - "delete": "\x1b[3~", - "del": "\x1b[3~", - "dc": "\x1b[3~", - "btab": "\x1b[Z", - "f1": "\x1bOP", - "f2": "\x1bOQ", - "f3": "\x1bOR", - "f4": "\x1bOS", - "f5": "\x1b[15~", - "f6": "\x1b[17~", - "f7": "\x1b[18~", - "f8": "\x1b[19~", - "f9": "\x1b[20~", - "f10": "\x1b[21~", - "f11": "\x1b[23~", - "f12": "\x1b[24~", -} - -// ss3KeysMap maps key names to SS3 escape sequences -var ss3KeysMap = map[string]string{ - "up": "\x1bOA", - "down": "\x1bOB", - "right": "\x1bOC", - "left": "\x1bOD", - "home": "\x1bOH", - "end": "\x1bOF", -} - -func detectPtyKeyMode(raw string) PtyKeyMode { - const SMKX = "\x1b[?1h" - const RMKX = "\x1b[?1l" - - lastSmkx := strings.LastIndex(raw, SMKX) - lastRmkx := strings.LastIndex(raw, RMKX) - - if lastSmkx == -1 && lastRmkx == -1 { - return PtyKeyModeNotFound - } - - if lastSmkx > lastRmkx { - return PtyKeyModeSS3 - } - return PtyKeyModeCSI -} - -// encodeKeyToken encodes a single key token into its escape sequence. -// Supports: -// - Named keys: "enter", "tab", "up", "ctrl-c", "alt-x", etc. -// - Ctrl modifier: "ctrl-c" or "c-c" (sends Ctrl+char) -// - Alt modifier: "alt-x" or "m-x" (sends ESC+char) -func encodeKeyToken(token string, ptyKeyMode PtyKeyMode) (string, error) { - token = strings.ToLower(strings.TrimSpace(token)) - if token == "" { - return "", nil - } - - // Handle ctrl-X format (c-x) - if strings.HasPrefix(token, "c-") { - char := token[2] - if char >= 'a' && char <= 'z' { - return string(rune(char) & 0x1f), nil // ctrl-a through ctrl-z - } - return "", fmt.Errorf("invalid ctrl key: %s", token) - } - - // Handle ctrl-X format (ctrl-x) - if strings.HasPrefix(token, "ctrl-") { - char := token[5] - if char >= 'a' && char <= 'z' { - return string(rune(char) & 0x1f), nil - } - return "", fmt.Errorf("invalid ctrl key: %s", token) - } - - // Handle alt-X format (m-x or alt-x) - if strings.HasPrefix(token, "m-") || strings.HasPrefix(token, "alt-") { - var char string - if strings.HasPrefix(token, "m-") { - char = token[2:] - } else { - char = token[4:] - } - if len(char) == 1 { - return "\x1b" + char, nil - } - return "", fmt.Errorf("invalid alt key: %s", token) - } - - // Handle shift modifier for special keys (shift-up, shift-down, etc.) - if strings.HasPrefix(token, "s-") || strings.HasPrefix(token, "shift-") { - var key string - if strings.HasPrefix(token, "s-") { - key = token[2:] - } else { - key = token[6:] - } - // Apply shift modifier: for single-char keys, return uppercase - if seq, ok := keyMap[key]; ok { - // For escape sequences, we can't easily add shift - // For single-char keys (letters), return uppercase - if len(seq) == 1 { - return strings.ToUpper(seq), nil - } - return seq, nil - } - return "", fmt.Errorf("unknown key with shift: %s", key) - } - - if ptyKeyMode == PtyKeyModeSS3 { - if seq, ok := ss3KeysMap[token]; ok { - return seq, nil - } - } - - if seq, ok := keyMap[token]; ok { - return seq, nil - } - - return "", fmt.Errorf("unknown key: %s (use write action for text input)", token) -} - -// encodeKeySequence encodes a slice of key tokens into a single string. -func encodeKeySequence(tokens []string, ptyKeyMode PtyKeyMode) (string, error) { - var result string - for _, token := range tokens { - seq, err := encodeKeyToken(token, ptyKeyMode) - if err != nil { - return "", err - } - result += seq - } - return result, nil -} - -func (t *ExecTool) executeSendKeys(args map[string]any) *ToolResult { - sessionID, ok := args["sessionId"].(string) - if !ok { - return ErrorResult("sessionId is required") - } - - keysStr, ok := args["keys"].(string) - if !ok { - return ErrorResult("keys must be a string") - } - - if keysStr == "" { - return ErrorResult("keys cannot be empty") - } - - // Parse comma-separated key names - keyNames := strings.Split(keysStr, ",") - var keys []string - for _, k := range keyNames { - k = strings.TrimSpace(k) - if k != "" { - keys = append(keys, k) - } - } - - if len(keys) == 0 { - return ErrorResult("keys cannot be empty") - } - - session, err := t.sessionManager.Get(sessionID) - if err != nil { - if errors.Is(err, ErrSessionNotFound) { - return ErrorResult(fmt.Sprintf("session not found: %s", sessionID)) - } - return ErrorResult(err.Error()) - } - - ptyKeyMode := session.GetPtyKeyMode() - - data, err := encodeKeySequence(keys, ptyKeyMode) - if err != nil { - return ErrorResult(fmt.Sprintf("invalid key: %v", err)) - } - - if session.IsDone() { - return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) - } - - if err := session.Write(data); err != nil { - if errors.Is(err, ErrSessionDone) { - return ErrorResult(fmt.Sprintf("process already exited with code %d", session.GetExitCode())) - } - return ErrorResult(fmt.Sprintf("failed to send keys: %v", err)) - } - - resp := ExecResponse{ - SessionID: sessionID, - Status: "running", - Output: fmt.Sprintf("Sent keys: %v", keys), - } - respData, _ := json.Marshal(resp) - return &ToolResult{ - ForLLM: string(respData), - IsError: false, - } -} - func (t *ExecTool) guardCommand(command, cwd string) string { cmd := strings.TrimSpace(command) lower := strings.ToLower(cmd) diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go index a8de2f4c9..f8f83ea74 100644 --- a/pkg/tools/shell_test.go +++ b/pkg/tools/shell_test.go @@ -2,16 +2,12 @@ package tools import ( "context" - "encoding/json" "os" "path/filepath" - "runtime" "strings" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/sipeed/picoclaw/pkg/config" ) @@ -24,7 +20,6 @@ func TestShellTool_Success(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "echo 'hello world'", } @@ -55,7 +50,6 @@ func TestShellTool_Failure(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "ls /nonexistent_directory_12345", } @@ -88,7 +82,6 @@ func TestShellTool_Timeout(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "sleep 10", } @@ -119,9 +112,8 @@ func TestShellTool_WorkingDir(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", - "command": "cat test.txt", - "cwd": tmpDir, + "command": "cat test.txt", + "working_dir": tmpDir, } result := tool.Execute(ctx, args) @@ -144,7 +136,6 @@ func TestShellTool_DangerousCommand(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "rm -rf /", } @@ -168,7 +159,6 @@ func TestShellTool_DangerousCommand_KillBlocked(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "kill 12345", } @@ -208,7 +198,6 @@ func TestShellTool_StderrCapture(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "sh -c 'echo stdout; echo stderr >&2'", } @@ -233,7 +222,6 @@ func TestShellTool_OutputTruncation(t *testing.T) { ctx := context.Background() // Generate long output (>10000 chars) args := map[string]any{ - "action": "run", "command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000), } @@ -263,9 +251,8 @@ func TestShellTool_WorkingDir_OutsideWorkspace(t *testing.T) { } result := tool.Execute(context.Background(), map[string]any{ - "action": "run", - "command": "pwd", - "cwd": outsideDir, + "command": "pwd", + "working_dir": outsideDir, }) if !result.IsError { @@ -302,9 +289,8 @@ func TestShellTool_WorkingDir_SymlinkEscape(t *testing.T) { } result := tool.Execute(context.Background(), map[string]any{ - "action": "run", - "command": "cat secret.txt", - "cwd": link, + "command": "cat secret.txt", + "working_dir": link, }) if !result.IsError { @@ -326,7 +312,7 @@ func TestShellTool_RemoteChannelBlockedByDefault(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "telegram", "chat-1") - result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) if !result.IsError { t.Fatal("expected remote-channel exec to be blocked") @@ -347,7 +333,7 @@ func TestShellTool_InternalChannelAllowed(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "cli", "direct") - result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) if result.IsError { t.Fatalf("expected internal channel exec to succeed, got: %s", result.ForLLM) @@ -387,7 +373,7 @@ func TestShellTool_AllowRemoteBypassesChannelCheck(t *testing.T) { t.Fatalf("NewExecToolWithConfig() error: %v", err) } ctx := WithToolContext(context.Background(), "telegram", "chat-1") - result := tool.Execute(ctx, map[string]any{"action": "run", "command": "echo hi"}) + result := tool.Execute(ctx, map[string]any{"command": "echo hi"}) if result.IsError { t.Fatalf("expected allowRemote=true to permit remote channel, got: %s", result.ForLLM) @@ -406,7 +392,6 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "cat ../../etc/passwd", } @@ -444,7 +429,7 @@ func TestShellTool_DevNullAllowed(t *testing.T) { } for _, cmd := range commands { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "blocked") { t.Errorf("command should not be blocked: %s\n error: %s", cmd, result.ForLLM) } @@ -473,7 +458,7 @@ func TestShellTool_BlockDevices(t *testing.T) { } for _, cmd := range blocked { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if !result.IsError { t.Errorf("expected block device write to be blocked: %s", cmd) } @@ -497,7 +482,7 @@ func TestShellTool_SafePathsInWorkspaceRestriction(t *testing.T) { } for _, cmd := range commands { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("safe path should not be blocked by workspace check: %s\n error: %s", cmd, result.ForLLM) } @@ -513,7 +498,6 @@ func TestShellTool_ExitCodeDetails(t *testing.T) { ctx := context.Background() args := map[string]any{ - "action": "run", "command": "sh -c 'exit 42'", } @@ -550,7 +534,6 @@ func TestShellTool_TimeoutWithPartialOutput(t *testing.T) { ctx := context.Background() // Use a command that outputs immediately then sleeps args := map[string]any{ - "action": "run", "command": "echo 'partial output before timeout' && sleep 30", } @@ -625,9 +608,7 @@ func TestShellTool_URLsNotBlocked(t *testing.T) { } for _, cmd := range commands { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - result := tool.Execute(ctx, map[string]any{"action": "run", "command": cmd}) - cancel() + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("command with URL should not be blocked by workspace check: %s\n error: %s", cmd, result.ForLLM) } @@ -652,7 +633,7 @@ func TestShellTool_FileURISandboxing(t *testing.T) { } for _, cmd := range blockedCommands { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if !result.IsError || !strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("file:// URI outside workspace should be blocked: %s", cmd) } @@ -670,7 +651,7 @@ func TestShellTool_FileURISandboxing(t *testing.T) { } for _, cmd := range allowedCommands { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("file:// URI inside workspace should be allowed: %s\n error: %s", cmd, result.ForLLM) } @@ -696,920 +677,9 @@ func TestShellTool_URLBypassPrevented(t *testing.T) { } for _, cmd := range blockedCommands { - result := tool.Execute(context.Background(), map[string]any{"action": "run", "command": cmd}) + result := tool.Execute(context.Background(), map[string]any{"command": cmd}) if !result.IsError || !strings.Contains(result.ForLLM, "path outside working dir") { t.Errorf("bypass attempt should be blocked: %q\n got: %s", cmd, result.ForLLM) } } } - -func TestShellTool_Background_ReturnsImmediately(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - ctx := context.Background() - args := map[string]any{ - "action": "run", - "command": "sleep 5", - "background": "true", - } - - start := time.Now() - result := tool.Execute(ctx, args) - elapsed := time.Since(start) - - require.False(t, result.IsError, "background run should not error: %s", result.ForLLM) - require.Less(t, elapsed, time.Second, "background run should return immediately") - require.Contains(t, result.ForLLM, "sessionId") -} - -func TestShellTool_List_Empty(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := context.Background() - args := map[string]any{"action": "list"} - - result := tool.Execute(ctx, args) - require.False(t, result.IsError) - require.Contains(t, result.ForUser, "0 active sessions") -} - -func TestShellTool_RunBackground_List(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 10", - "background": "true", - }) - require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &resp) - require.NoError(t, err) - require.NotEmpty(t, resp.SessionID) - - time.Sleep(100 * time.Millisecond) - - listResult := tool.Execute(ctx, map[string]any{"action": "list"}) - require.False(t, listResult.IsError) - - var listResp ExecResponse - err = json.Unmarshal([]byte(listResult.ForLLM), &listResp) - require.NoError(t, err) - require.Len(t, listResp.Sessions, 1) - require.Equal(t, resp.SessionID, listResp.Sessions[0].ID) - - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) -} - -func TestShellTool_Read_Output(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "echo hello", - "background": "true", - }) - require.False(t, runResult.IsError) - - var resp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &resp) - require.NoError(t, err) - - time.Sleep(200 * time.Millisecond) - - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": resp.SessionID, - }) - - if !readResult.IsError { - var readResp ExecResponse - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - } -} - -func TestShellTool_Kill(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 100", - "background": "true", - }) - require.False(t, runResult.IsError) - - var resp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &resp) - require.NoError(t, err) - - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) - - time.Sleep(100 * time.Millisecond) - - listResult := tool.Execute(ctx, map[string]any{"action": "list"}) - var listResp ExecResponse - err = json.Unmarshal([]byte(listResult.ForLLM), &listResp) - require.NoError(t, err) - require.Len(t, listResp.Sessions, 0) -} - -func TestShellTool_PTY_AllowedCommands(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Test that PTY is allowed for non-interpreter commands - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "cat", - "pty": "true", - "background": "true", - }) - require.False(t, result.IsError, "PTY with cat should succeed: %s", result.ForLLM) - require.Contains(t, result.ForLLM, "sessionId") - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - require.NotEmpty(t, resp.SessionID) - - // Clean up - tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) -} - -func TestShellTool_PTY_WriteRead(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a PTY session with a command that waits for input - // Using 'cat' which will wait for stdin - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "cat", - "pty": "true", - "background": "true", - }) - require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Write some input to cat - writeResult := tool.Execute(ctx, map[string]any{ - "action": "write", - "sessionId": resp.SessionID, - "data": "hello\n", - }) - require.False(t, writeResult.IsError, "write should succeed: %s", writeResult.ForLLM) - - // Give cat time to process and output - time.Sleep(200 * time.Millisecond) - - // Read the output - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": resp.SessionID, - }) - - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - - var readResp ExecResponse - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - // PTY output should contain "hello" - require.Contains(t, readResp.Output, "hello") - - // Clean up - tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) -} - -func TestShellTool_PTY_Poll(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a PTY session with a long-running command - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 2", - "pty": "true", - "background": "true", - }) - require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Poll should show running - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.False(t, pollResult.IsError, "poll should succeed: %s", pollResult.ForLLM) - - var pollResp ExecResponse - err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) - require.NoError(t, err) - require.Equal(t, "running", pollResp.Status) - - // Wait for sleep to complete - time.Sleep(2500 * time.Millisecond) - - // Poll should show done - pollResult = tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.False(t, pollResult.IsError) - - err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) - require.NoError(t, err) - require.Equal(t, "done", pollResp.Status) -} - -func TestShellTool_PTY_Kill(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a PTY session with a long-running command - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 10", - "pty": "true", - "background": "true", - }) - require.False(t, result.IsError, "PTY run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Kill the session - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) - - // Verify kill response shows done status - var killResp ExecResponse - err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) - require.NoError(t, err) - require.Equal(t, "done", killResp.Status) - - // Poll should return error since session is removed after kill - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - // Session is removed after kill, so poll returns error with "session not found" - require.True(t, pollResult.IsError, "poll should error after kill (session removed)") - require.Contains(t, pollResult.ForLLM, "session not found") -} - -func TestShellTool_Write_Read_NonPTY(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a background process that reads from stdin and outputs it - // Using 'cat' which echoes stdin to stdout - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "cat", - "pty": false, - "background": "true", - }) - require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Write some input to cat - writeResult := tool.Execute(ctx, map[string]any{ - "action": "write", - "sessionId": resp.SessionID, - "data": "hello world\n", - }) - require.False(t, writeResult.IsError, "write should succeed: %s", writeResult.ForLLM) - - // Give cat time to process and output - time.Sleep(200 * time.Millisecond) - - // Read the output - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": resp.SessionID, - }) - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - - var readResp ExecResponse - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - require.Contains(t, readResp.Output, "hello world") - - // Clean up - tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) -} - -func TestShellTool_Read_NonPTY_Running(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a long-running process that produces output over time - // Using sh -c with sleep at the end so process doesn't exit immediately - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sh -c 'echo line1; sleep 0.5; echo line2; sleep 0.5; echo line3; sleep 10'", - "pty": false, - "background": "true", - }) - require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Give time for first outputs to be produced - time.Sleep(300 * time.Millisecond) - - // Read output while process is running - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": resp.SessionID, - }) - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - - var readResp ExecResponse - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - // Should have at least line1 - require.Contains(t, readResp.Output, "line1") - - // Wait for line3 to be produced (line1=0s, line2=0.5s, line3=1s, then sleep 10) - time.Sleep(1200 * time.Millisecond) - - // Read again - should have line3 as well - readResult = tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": resp.SessionID, - }) - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - require.Contains(t, readResp.Output, "line3") - - // Clean up - tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) -} - -func TestShellTool_ProcessGroupKill(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Process group kill not supported on Windows") - } - - // Note: Testing process group kill with PTY is tricky because the command - // must be run through an interpreter (sh, bash) which is blocked for PTY. - // Instead, we test with non-PTY mode which also uses Setsid for background processes. - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a shell that spawns child processes (non-PTY mode) - // The sh -c command creates child sleep processes - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sh -c 'sleep 30 & sleep 30 & wait'", - "pty": false, - "background": "true", - }) - require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Give time for child processes to spawn - time.Sleep(500 * time.Millisecond) - - // Kill the session - should kill the entire process group - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) - - // Verify kill response shows done status - var killResp ExecResponse - err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) - require.NoError(t, err) - require.Equal(t, "done", killResp.Status) - - // Poll should return error since session is removed after kill - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.True(t, pollResult.IsError, "poll should error after kill (session removed)") - require.Contains(t, pollResult.ForLLM, "session not found") -} - -func TestShellTool_PTY_ProcessGroupKill(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY process group kill not supported on Windows") - } - - // This test binary creates 4 child sleep processes and waits for signals. - // It's not an interpreter, so it's allowed with PTY mode. - // The binary is created in /tmp/test_pgroup.c and compiled as part of test setup. - testBinary := "/tmp/test_pgroup" - if _, err := os.Stat(testBinary); os.IsNotExist(err) { - t.Skip("Test binary /tmp/test_pgroup not found - run: gcc -o /tmp/test_pgroup /tmp/test_pgroup.c") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start the test binary with PTY mode - // It forks 4 child sleep processes and waits for signals - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": testBinary, - "pty": "true", - "background": "true", - }) - require.False(t, result.IsError, "run should succeed: %s", result.ForLLM) - - var resp ExecResponse - err = json.Unmarshal([]byte(result.ForLLM), &resp) - require.NoError(t, err) - - // Give time for child processes to spawn - time.Sleep(500 * time.Millisecond) - - // Kill the session - should kill the entire process group - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": resp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) - - // Verify kill response shows done status - var killResp ExecResponse - err = json.Unmarshal([]byte(killResult.ForLLM), &killResp) - require.NoError(t, err) - require.Equal(t, "done", killResp.Status) - - // Poll should return error since session is removed after kill - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.True(t, pollResult.IsError, "poll should error after kill (session removed)") - require.Contains(t, pollResult.ForLLM, "session not found") -} - -func TestShellTool_PTY_Background_Read(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a fast command with PTY + background mode - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "echo hello", - "pty": "true", - "background": "true", - }) - require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) - - var runResp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &runResp) - require.NoError(t, err) - require.NotEmpty(t, runResp.SessionID) - require.Equal(t, "running", runResp.Status) - - // Wait for command to complete - time.Sleep(500 * time.Millisecond) - - // Read output - this is the key test: PTY + background mode should preserve output - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": runResp.SessionID, - }) - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - require.Contains(t, readResult.ForLLM, "hello", "output should contain 'hello'") -} - -func TestShellTool_PTY_Background_ReadNoBlock(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("PTY not supported on Windows") - } - - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - // Start a long-running command with PTY + background mode - // This command produces no output, just sleeps - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 10", - "pty": "true", - "background": "true", - }) - require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForLLM) - - var runResp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &runResp) - require.NoError(t, err) - require.NotEmpty(t, runResp.SessionID) - - // Read immediately - should NOT block even though process is running and has no output - // This tests that Read() returns quickly (within 1 second) instead of blocking for 10 seconds - start := time.Now() - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": runResp.SessionID, - }) - elapsed := time.Since(start) - - require.False(t, readResult.IsError, "read should succeed: %s", readResult.ForLLM) - require.Less(t, elapsed.Seconds(), 1.0, "read should not block, should return within 1 second") - - // Kill the session to clean up - killResult := tool.Execute(ctx, map[string]any{ - "action": "kill", - "sessionId": runResp.SessionID, - }) - require.False(t, killResult.IsError, "kill should succeed: %s", killResult.ForLLM) -} - -func TestShellTool_Poll_Status(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - sm := NewSessionManager() - tool.sessionManager = sm - - ctx := WithToolContext(context.Background(), "cli", "test") - - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "sleep 1", - "background": "true", - }) - require.False(t, runResult.IsError) - - var resp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &resp) - require.NoError(t, err) - - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.False(t, pollResult.IsError) - - var pollResp ExecResponse - err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) - require.NoError(t, err) - require.Equal(t, "running", pollResp.Status) - - time.Sleep(1200 * time.Millisecond) - - pollResult = tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": resp.SessionID, - }) - require.False(t, pollResult.IsError) - - err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) - require.NoError(t, err) - require.Equal(t, "done", pollResp.Status) -} - -func TestShellTool_Action_Run_Sync(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - ctx := context.Background() - - result := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "echo hello", - }) - - require.False(t, result.IsError) - require.Contains(t, result.ForLLM, "hello") -} - -// TestShellTool_Background_ReadAfterExit verifies that we can read -// buffered output even after the background process has exited. -func TestShellTool_Background_ReadAfterExit(t *testing.T) { - tool, err := NewExecTool("", false) - require.NoError(t, err) - - ctx := context.Background() - - // Start a background command that produces output and exits quickly - runResult := tool.Execute(ctx, map[string]any{ - "action": "run", - "command": "echo hello && sleep 1 && echo world", - "background": "true", - }) - require.False(t, runResult.IsError, "run should succeed: %s", runResult.ForUser) - - // Parse session ID from response - var resp ExecResponse - err = json.Unmarshal([]byte(runResult.ForLLM), &resp) - require.NoError(t, err) - require.NotEmpty(t, resp.SessionID) - sessionID := resp.SessionID - - // Wait for process to exit (sleep 1 + some buffer) - time.Sleep(1500 * time.Millisecond) - - // Poll to verify process is done - pollResult := tool.Execute(ctx, map[string]any{ - "action": "poll", - "sessionId": sessionID, - }) - require.False(t, pollResult.IsError, "poll should succeed: %s", pollResult.ForLLM) - var pollResp ExecResponse - err = json.Unmarshal([]byte(pollResult.ForLLM), &pollResp) - require.NoError(t, err) - require.Equal(t, "done", pollResp.Status, "process should be done") - - // Try to read output AFTER process has exited - readResult := tool.Execute(ctx, map[string]any{ - "action": "read", - "sessionId": sessionID, - }) - require.False(t, readResult.IsError, "read should succeed after exit: %s", readResult.ForLLM) - - var readResp ExecResponse - err = json.Unmarshal([]byte(readResult.ForLLM), &readResp) - require.NoError(t, err) - - // Output should contain both "hello" and "world" - require.Contains(t, readResp.Output, "hello", "should contain hello") - require.Contains(t, readResp.Output, "world", "should contain world after sleep") -} - -func TestSendKeys_CtrlC(t *testing.T) { - // Note: Ctrl-C as a signal requires sending SIGINT to the process group, - // which requires elevated privileges. Writing "\x03" to PTY passes the byte - // to the process but doesn't generate SIGINT for processes that don't read stdin. - // For interrupting processes, use the kill action instead. - t.Skip("Ctrl-C as signal not supported - use kill action for interruption") -} - -func TestEncodeKeyToken(t *testing.T) { - tests := []struct { - token string - expected string - hasError bool - }{ - // Named keys - {"enter", "\r", false}, - {"return", "\r", false}, - {"tab", "\t", false}, - {"escape", "\x1b", false}, - {"esc", "\x1b", false}, - {"backspace", "\x7f", false}, - {"up", "\x1b[A", false}, - {"down", "\x1b[B", false}, - {"left", "\x1b[D", false}, - {"right", "\x1b[C", false}, - {"home", "\x1b[1~", false}, - {"end", "\x1b[4~", false}, - {"pageup", "\x1b[5~", false}, - {"pagedown", "\x1b[6~", false}, - {"delete", "\x1b[3~", false}, - {"f1", "\x1bOP", false}, - {"f12", "\x1b[24~", false}, - - // Ctrl keys - {"ctrl-c", "\x03", false}, - {"ctrl-d", "\x04", false}, - {"ctrl-a", "\x01", false}, - {"ctrl-z", "\x1a", false}, - {"c-c", "\x03", false}, - {"c-d", "\x04", false}, - - // Alt keys - {"alt-x", "\x1bx", false}, - {"m-x", "\x1bx", false}, - - // Case insensitive tests - {"ENTER", "\r", false}, - {"TAB", "\t", false}, - {"CTRL-C", "\x03", false}, - {"Ctrl-D", "\x04", false}, - {"ALT-X", "\x1bx", false}, - {"M-X", "\x1bx", false}, - {"UP", "\x1b[A", false}, - {"DOWN", "\x1b[B", false}, - - // Unknown keys should return error (use write action for text input) - {"unknown-key", "", true}, - } - - for _, tt := range tests { - t.Run(tt.token, func(t *testing.T) { - result, err := encodeKeyToken(tt.token, PtyKeyModeCSI) - if tt.hasError { - require.Error(t, err, "expected error for %s", tt.token) - } else { - require.NoError(t, err, "unexpected error for %s", tt.token) - require.Equal(t, tt.expected, result, "wrong encoding for %s", tt.token) - } - }) - } -} - -// TestDetectPtyKeyMode tests smkx/rmkx detection in PTY output -func TestDetectPtyKeyMode(t *testing.T) { - tests := []struct { - name string - raw string - expected PtyKeyMode - }{ - {"no toggle", "hello world", PtyKeyModeNotFound}, - {"smkx only", "\x1b[?1h\x1b=", PtyKeyModeSS3}, - {"rmkx only", "\x1b[?1l\x1b>", PtyKeyModeCSI}, - {"both smkx first", "\x1b[?1h\x1b=...\x1b[?1l\x1b>", PtyKeyModeCSI}, - {"both rmkx first", "\x1b[?1l\x1b>...\x1b[?1h\x1b=", PtyKeyModeSS3}, - {"multiple toggles smkx last", "\x1b[?1h\x1b=...\x1b[?1l\x1b>...\x1b[?1h\x1b=", PtyKeyModeSS3}, - {"multiple toggles rmkx last", "\x1b[?1l\x1b>...\x1b[?1h\x1b=...\x1b[?1l\x1b>", PtyKeyModeCSI}, - {"partial smkx", "\x1b[?1h", PtyKeyModeSS3}, - {"partial rmkx", "\x1b[?1l", PtyKeyModeCSI}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := detectPtyKeyMode(tt.raw) - require.Equal(t, tt.expected, result, "wrong mode for %s", tt.name) - }) - } -} - -func TestEncodeKeyTokenWithPtyKeyMode(t *testing.T) { - tests := []struct { - name string - token string - mode PtyKeyMode - expected string - hasError bool - }{ - // CSI mode - {"up csi", "up", PtyKeyModeCSI, "\x1b[A", false}, - {"down csi", "down", PtyKeyModeCSI, "\x1b[B", false}, - {"left csi", "left", PtyKeyModeCSI, "\x1b[D", false}, - {"right csi", "right", PtyKeyModeCSI, "\x1b[C", false}, - - // SS3 mode - {"up ss3", "up", PtyKeyModeSS3, "\x1bOA", false}, - {"down ss3", "down", PtyKeyModeSS3, "\x1bOB", false}, - {"left ss3", "left", PtyKeyModeSS3, "\x1bOD", false}, - {"right ss3", "right", PtyKeyModeSS3, "\x1bOC", false}, - {"home ss3", "home", PtyKeyModeSS3, "\x1bOH", false}, - {"end ss3", "end", PtyKeyModeSS3, "\x1bOF", false}, - - // Other keys unaffected by mode - {"enter ss3", "enter", PtyKeyModeSS3, "\r", false}, - {"tab ss3", "tab", PtyKeyModeSS3, "\t", false}, - {"ctrl-c ss3", "ctrl-c", PtyKeyModeSS3, "\x03", false}, - - // NotFound behaves like CSI - {"up notfound", "up", PtyKeyModeNotFound, "\x1b[A", false}, - {"down notfound", "down", PtyKeyModeNotFound, "\x1b[B", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := encodeKeyToken(tt.token, tt.mode) - if tt.hasError { - require.Error(t, err, "expected error for %s", tt.name) - } else { - require.NoError(t, err, "unexpected error for %s", tt.name) - require.Equal(t, tt.expected, result, "wrong encoding for %s", tt.name) - } - }) - } -} diff --git a/pkg/tools/shell_timeout_unix_test.go b/pkg/tools/shell_timeout_unix_test.go index dfd28454c..357e1276e 100644 --- a/pkg/tools/shell_timeout_unix_test.go +++ b/pkg/tools/shell_timeout_unix_test.go @@ -30,7 +30,6 @@ func TestShellTool_TimeoutKillsChildProcess(t *testing.T) { tool.SetTimeout(500 * time.Millisecond) args := map[string]any{ - "action": "run", // Spawn a child process that would outlive the shell unless process-group kill is used. "command": "sleep 60 & echo $! > child.pid; wait", } diff --git a/pkg/tools/types.go b/pkg/tools/types.go index 4d1a18d5a..a6015cde3 100644 --- a/pkg/tools/types.go +++ b/pkg/tools/types.go @@ -56,24 +56,3 @@ type ToolFunctionDefinition struct { Description string `json:"description"` Parameters map[string]any `json:"parameters"` } - -type ExecRequest struct { - Action string `json:"action"` - Command string `json:"command,omitempty"` - PTY bool `json:"pty,omitempty"` - Background bool `json:"background,omitempty"` - Timeout int `json:"timeout,omitempty"` - Env map[string]string `json:"env,omitempty"` - Cwd string `json:"cwd,omitempty"` - SessionID string `json:"sessionId,omitempty"` - Data string `json:"data,omitempty"` -} - -type ExecResponse struct { - SessionID string `json:"sessionId,omitempty"` - Status string `json:"status,omitempty"` - ExitCode int `json:"exitCode,omitempty"` - Output string `json:"output,omitempty"` - Error string `json:"error,omitempty"` - Sessions []SessionInfo `json:"sessions,omitempty"` -}