From 97bf4ff3fddd99c5a6a1d9a74a4e7637f34d7063 Mon Sep 17 00:00:00 2001
From: Yasuhiro Matsumoto
Date: Sun, 15 Feb 2026 23:56:13 +0900
Subject: [PATCH 01/13] Fix Japanese translation
---
README.ja.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.ja.md b/README.ja.md
index e33b312f9..706af2c75 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -3,7 +3,7 @@
PicoClaw: Go で書かれた超効率 AI アシスタント
-$10 ハードウェア · 10MB RAM · 1秒起動 · 皮皮虾,我们走!
+$10 ハードウェア · 10MB RAM · 1秒起動 · 行くぜ、シャコ!
@@ -39,7 +39,7 @@
## 📢 ニュース
-2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 皮皮虾,我们走!
+2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 行くぜ、シャコ!
## ✨ 特徴
@@ -729,7 +729,7 @@ Discord: https://discord.gg/V4sAZ9XWpN
## 🐛 トラブルシューティング
-### Web 検索で「API 配置问题」と表示される
+### Web 検索で「API 設定の問題」と表示される
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
From 7ce5b75178356d4c81044faa6d2ea06cd69ec507 Mon Sep 17 00:00:00 2001
From: Yasuhiro Matsumoto
Date: Mon, 16 Feb 2026 00:47:17 +0900
Subject: [PATCH 02/13] Fix shadowing field runnnig
---
pkg/channels/maixcam.go | 2 --
1 file changed, 2 deletions(-)
diff --git a/pkg/channels/maixcam.go b/pkg/channels/maixcam.go
index 5fc19adbe..01e570b25 100644
--- a/pkg/channels/maixcam.go
+++ b/pkg/channels/maixcam.go
@@ -18,7 +18,6 @@ type MaixCamChannel struct {
listener net.Listener
clients map[net.Conn]bool
clientsMux sync.RWMutex
- running bool
}
type MaixCamMessage struct {
@@ -35,7 +34,6 @@ func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamC
BaseChannel: base,
config: cfg,
clients: make(map[net.Conn]bool),
- running: false,
}, nil
}
From ff3c875b3fad1116a7ea7e22a10034a741b38f18 Mon Sep 17 00:00:00 2001
From: Humaid Koreshi
Date: Tue, 17 Feb 2026 02:15:59 +0600
Subject: [PATCH 03/13] docs: add missing Chinese language link to Japanese
README
---
README.ja.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.ja.md b/README.ja.md
index e33b312f9..c6babf510 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -12,7 +12,7 @@
-**日本語** | [English](README.md)
+[中文](README.zh.md) | **日本語** | [English](README.md)
From ad747e8e8925cb9cb48cfc232f40156b0905b613 Mon Sep 17 00:00:00 2001
From: Boris Bliznioukov
Date: Tue, 17 Feb 2026 14:27:03 +0100
Subject: [PATCH 04/13] fix(Makefile): update LDFLAGS and GOFLAGS for optimized
build size
Signed-off-by: Boris Bliznioukov
---
Makefile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
index 9786b30bb..c3f889f8f 100644
--- a/Makefile
+++ b/Makefile
@@ -11,11 +11,11 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date +%FT%T%z)
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
-LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
+LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION) -s -w"
# Go variables
GO?=go
-GOFLAGS?=-v
+GOFLAGS?=-v -tags stdjson
# Installation
INSTALL_PREFIX?=$(HOME)/.local
From 2d758d714faf8d4cc7fe48d7886bb8f3a2971a8b Mon Sep 17 00:00:00 2001
From: Boris Bliznioukov
Date: Tue, 17 Feb 2026 14:55:37 +0100
Subject: [PATCH 05/13] feat(goreleaser): add 'stdjson' tag to picoclaw build
configuration
Signed-off-by: Boris Bliznioukov
---
.goreleaser.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 368a0f06b..0354928f3 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -11,6 +11,8 @@ builds:
- id: picoclaw
env:
- CGO_ENABLED=0
+ tags:
+ - stdjson
goos:
- linux
- windows
From 2d876eaa9809d3a958ffc2e73c8eed8b8d760531 Mon Sep 17 00:00:00 2001
From: Boris Bliznioukov
Date: Tue, 17 Feb 2026 15:00:06 +0100
Subject: [PATCH 06/13] feat(goreleaser): enhance build flags with versioning
and commit info
Signed-off-by: Boris Bliznioukov
---
.github/workflows/release.yml | 2 ++
.goreleaser.yaml | 6 ++++++
2 files changed, 8 insertions(+)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9fe3a684e..4e9399128 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -55,6 +55,7 @@ jobs:
ref: ${{ inputs.tag }}
- name: Setup Go from go.mod
+ id: setup-go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
@@ -89,6 +90,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
+ GOVERSION: ${{ steps.setup-go.outputs.go-version }}
- name: Apply release flags
shell: bash
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 0354928f3..2c47f7d86 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -13,6 +13,12 @@ builds:
- CGO_ENABLED=0
tags:
- stdjson
+ ldflags:
+ - -s -w
+ - -X main.version={{ .Version }}
+ - -X main.gitCommit={{ .ShortCommit }}
+ - -X main.buildTime={{ .Date }}
+ - -X main.goVersion={{ .Env.GOVERSION }}
goos:
- linux
- windows
From 4cd3f99dd6f2ddfd3378b269d6f295d4a5ecc763 Mon Sep 17 00:00:00 2001
From: "zenix.huang"
Date: Mon, 16 Feb 2026 12:49:11 +0900
Subject: [PATCH 07/13] fix: remove max_tokens
---
pkg/providers/codex_provider.go | 4 ----
pkg/providers/codex_provider_test.go | 11 +++++++++++
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go
index 6dff3a52e..9e36217ae 100644
--- a/pkg/providers/codex_provider.go
+++ b/pkg/providers/codex_provider.go
@@ -260,10 +260,6 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
params.Instructions = openai.Opt(defaultCodexInstructions)
}
- if maxTokens, ok := options["max_tokens"].(int); ok {
- params.MaxOutputTokens = openai.Opt(int64(maxTokens))
- }
-
if len(tools) > 0 {
params.Tools = translateToolsForCodex(tools)
}
diff --git a/pkg/providers/codex_provider_test.go b/pkg/providers/codex_provider_test.go
index 317b1a5de..c34593e7b 100644
--- a/pkg/providers/codex_provider_test.go
+++ b/pkg/providers/codex_provider_test.go
@@ -29,6 +29,9 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
if params.Instructions.Or("") != defaultCodexInstructions {
t.Errorf("Instructions = %q, want %q", params.Instructions.Or(""), defaultCodexInstructions)
}
+ if params.MaxOutputTokens.Valid() {
+ t.Fatalf("MaxOutputTokens should not be set for Codex backend")
+ }
}
func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
@@ -214,6 +217,10 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
http.Error(w, "stream must be true", http.StatusBadRequest)
return
}
+ if _, ok := reqBody["max_output_tokens"]; ok {
+ http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
+ return
+ }
resp := map[string]interface{}{
"id": "resp_test",
@@ -293,6 +300,10 @@ func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T)
http.Error(w, "temperature is not supported", http.StatusBadRequest)
return
}
+ if _, ok := reqBody["max_output_tokens"]; ok {
+ http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
+ return
+ }
if reqBody["stream"] != true {
http.Error(w, "stream must be true", http.StatusBadRequest)
return
From 0d16525fab81f1010bf6ddafd5ea68d975613a88 Mon Sep 17 00:00:00 2001
From: "zenix.huang"
Date: Mon, 16 Feb 2026 13:08:37 +0900
Subject: [PATCH 08/13] fix: codex tool call
---
pkg/providers/codex_provider.go | 36 ++++++++++++++++++++++---
pkg/providers/codex_provider_test.go | 39 ++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 3 deletions(-)
diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go
index 9e36217ae..7617bf716 100644
--- a/pkg/providers/codex_provider.go
+++ b/pkg/providers/codex_provider.go
@@ -217,12 +217,18 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
})
}
for _, tc := range msg.ToolCalls {
- argsJSON, _ := json.Marshal(tc.Arguments)
+ name, args, ok := resolveCodexToolCall(tc)
+ if !ok {
+ logger.WarnCF("provider.codex", "Skipping invalid tool call in history", map[string]interface{}{
+ "call_id": tc.ID,
+ })
+ continue
+ }
inputItems = append(inputItems, responses.ResponseInputItemUnionParam{
OfFunctionCall: &responses.ResponseFunctionToolCallParam{
CallID: tc.ID,
- Name: tc.Name,
- Arguments: string(argsJSON),
+ Name: name,
+ Arguments: args,
},
})
}
@@ -267,6 +273,30 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
return params
}
+func resolveCodexToolCall(tc ToolCall) (name string, arguments string, ok bool) {
+ name = tc.Name
+ if name == "" && tc.Function != nil {
+ name = tc.Function.Name
+ }
+ if name == "" {
+ return "", "", false
+ }
+
+ if len(tc.Arguments) > 0 {
+ argsJSON, err := json.Marshal(tc.Arguments)
+ if err != nil {
+ return "", "", false
+ }
+ return name, string(argsJSON), true
+ }
+
+ if tc.Function != nil && tc.Function.Arguments != "" {
+ return name, tc.Function.Arguments, true
+ }
+
+ return name, "{}", true
+}
+
func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
result := make([]responses.ToolUnionParam, 0, len(tools))
for _, t := range tools {
diff --git a/pkg/providers/codex_provider_test.go b/pkg/providers/codex_provider_test.go
index c34593e7b..8406760c4 100644
--- a/pkg/providers/codex_provider_test.go
+++ b/pkg/providers/codex_provider_test.go
@@ -68,6 +68,45 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
}
}
+func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
+ messages := []Message{
+ {Role: "user", Content: "Read a file"},
+ {
+ Role: "assistant",
+ ToolCalls: []ToolCall{
+ {
+ ID: "call_1",
+ Type: "function",
+ Function: &FunctionCall{
+ Name: "read_file",
+ Arguments: `{"path":"README.md"}`,
+ },
+ },
+ },
+ },
+ {Role: "tool", Content: "ok", ToolCallID: "call_1"},
+ }
+
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
+ if params.Input.OfInputItemList == nil {
+ t.Fatal("Input.OfInputItemList should not be nil")
+ }
+ if len(params.Input.OfInputItemList) != 3 {
+ t.Fatalf("len(Input items) = %d, want 3", len(params.Input.OfInputItemList))
+ }
+
+ fc := params.Input.OfInputItemList[1].OfFunctionCall
+ if fc == nil {
+ t.Fatal("assistant tool call should be converted to function_call input item")
+ }
+ if fc.Name != "read_file" {
+ t.Errorf("Function call name = %q, want %q", fc.Name, "read_file")
+ }
+ if fc.Arguments != `{"path":"README.md"}` {
+ t.Errorf("Function call arguments = %q, want %q", fc.Arguments, `{"path":"README.md"}`)
+ }
+}
+
func TestBuildCodexParams_WithTools(t *testing.T) {
tools := []ToolDefinition{
{
From f820da42d7a63b05bef448839fd7fc5e13527d3b Mon Sep 17 00:00:00 2001
From: Leandro Barbosa
Date: Tue, 17 Feb 2026 17:52:28 -0300
Subject: [PATCH 09/13] docs: add Brazilian Portuguese README (README.pt-br.md)
Add complete pt-BR translation of the README and update language
navigation links across all existing READMEs (English, Chinese,
Japanese) to include the Portuguese option.
---
README.ja.md | 2 +-
README.md | 2 +-
README.pt-br.md | 881 ++++++++++++++++++++++++++++++++++++++++++++++++
README.zh.md | 2 +-
4 files changed, 884 insertions(+), 3 deletions(-)
create mode 100644 README.pt-br.md
diff --git a/README.ja.md b/README.ja.md
index b86d636ac..0da84571a 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -12,7 +12,7 @@
-[中文](README.zh.md) | **日本語** | [English](README.md)
+[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [English](README.md)
diff --git a/README.md b/README.md
index e80e2213c..59b9bea7c 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
- [中文](README.zh.md) | [日本語](README.ja.md) | **English**
+ [中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | **English**
---
diff --git a/README.pt-br.md b/README.pt-br.md
new file mode 100644
index 000000000..d250cc956
--- /dev/null
+++ b/README.pt-br.md
@@ -0,0 +1,881 @@
+
+

+
+
PicoClaw: Assistente de IA Ultra-Eficiente em Go
+
+
Hardware de $10 · 10MB de RAM · Boot em 1s · 皮皮虾,我们走!
+
+
+
+
+
+
+
+
+
+
+ [中文](README.zh.md) | [日本語](README.ja.md) | [English](README.md) | **Português**
+
+
+---
+
+🦐 **PicoClaw** é um assistente pessoal de IA ultra-leve inspirado no [nanobot](https://github.com/HKUDS/nanobot), reescrito do zero em **Go** por meio de um processo de "auto-inicialização" (self-bootstrapping) — onde o próprio agente de IA conduziu toda a migração de arquitetura e otimização de código.
+
+⚡️ **Extremamente leve:** Roda em hardware de apenas **$10** com **<10MB** de RAM. Isso é 99% menos memória que o OpenClaw e 98% mais barato que um Mac mini!
+
+
+
+|
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+> [!CAUTION]
+> **🚨 DECLARACAO DE SEGURANCA & CANAIS OFICIAIS**
+>
+> * **SEM CRIPTOMOEDAS:** O PicoClaw **NAO** possui nenhum token/moeda oficial. Todas as alegacoes no `pump.fun` ou outras plataformas de negociacao sao **GOLPES**.
+> * **DOMINIO OFICIAL:** O **UNICO** site oficial e **[picoclaw.io](https://picoclaw.io)**, e o site da empresa e **[sipeed.com](https://sipeed.com)**.
+> * **Aviso:** Muitos dominios `.ai/.org/.com/.net/...` foram registrados por terceiros, nao sao nossos.
+> * **Aviso:** O PicoClaw esta em fase inicial de desenvolvimento e pode ter problemas de seguranca de rede nao resolvidos. Nao implante em ambientes de producao antes da versao v1.0.
+> * **Nota:** O PicoClaw recentemente fez merge de muitos PRs, o que pode resultar em maior consumo de memoria (10-20MB) nas versoes mais recentes. Planejamos priorizar a otimizacao de recursos assim que o conjunto de funcionalidades estiver estavel.
+
+
+## 📢 Novidades
+
+2026-02-16 🎉 PicoClaw atingiu 12K stars em uma semana! Obrigado a todos pelo apoio! O PicoClaw esta crescendo mais rapido do que jamais imaginamos. Dado o alto volume de PRs, precisamos urgentemente de maintainers da comunidade. Nossos papeis de voluntarios e roadmap foram publicados oficialmente [aqui](docs/picoclaw_community_roadmap_260216.md) — estamos ansiosos para ter voce a bordo!
+
+2026-02-13 🎉 PicoClaw atingiu 5000 stars em 4 dias! Obrigado a comunidade! Estamos finalizando o **Roadmap do Projeto** e configurando o **Grupo de Desenvolvedores** para acelerar o desenvolvimento do PicoClaw.
+🚀 **Chamada para Acao:** Envie suas solicitacoes de funcionalidades nas GitHub Discussions. Revisaremos e priorizaremos na proxima reuniao semanal.
+
+2026-02-09 🎉 PicoClaw lancado oficialmente! Construido em 1 dia para trazer Agentes de IA para hardware de $10 com <10MB de RAM. 🦐 PicoClaw, Partiu!
+
+## ✨ Funcionalidades
+
+🪶 **Ultra-Leve**: Consumo de memoria <10MB — 99% menor que o Clawdbot para funcionalidades essenciais.
+
+💰 **Custo Minimo**: Eficiente o suficiente para rodar em hardware de $10 — 98% mais barato que um Mac mini.
+
+⚡️ **Inicializacao Relampago**: Tempo de inicializacao 400X mais rapido, boot em 1 segundo mesmo em CPU single-core de 0.6GHz.
+
+🌍 **Portabilidade Real**: Um unico binario auto-contido para RISC-V, ARM e x86. Um clique e ja era!
+
+🤖 **Auto-Construido por IA**: Implementacao nativa em Go de forma autonoma — 95% do nucleo gerado pelo Agente com refinamento humano no loop.
+
+| | OpenClaw | NanoBot | **PicoClaw** |
+| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
+| **Linguagem** | TypeScript | Python | **Go** |
+| **RAM** | >1GB | >100MB | **< 10MB** |
+| **Inicializacao**(CPU 0.8GHz) | >500s | >30s | **<1s** |
+| **Custo** | Mac Mini $599 | Maioria dos SBC Linux ~$50 | **Qualquer placa Linux****A partir de $10** |
+
+
+
+## 🦾 Demonstracao
+
+### 🛠️ Fluxos de Trabalho Padrao do Assistente
+
+
+
+🧩 Engenharia Full-Stack |
+🗂️ Gerenciamento de Logs & Planejamento |
+🔎 Busca Web & Aprendizado |
+
+
+
|
+
|
+
|
+
+
+| Desenvolver • Implantar • Escalar |
+Agendar • Automatizar • Memorizar |
+Descobrir • Analisar • Tendencias |
+
+
+
+### 📱 Rode em celulares Android antigos
+
+De uma segunda vida ao seu celular de dez anos atras! Transforme-o em um assistente de IA inteligente com o PicoClaw. Inicio rapido:
+
+1. **Instale o Termux** (Disponivel no F-Droid ou Google Play).
+2. **Execute os comandos**
+
+```bash
+# Nota: Substitua v0.1.1 pela versao mais recente da pagina de Releases
+wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
+chmod +x picoclaw-linux-arm64
+pkg install proot
+termux-chroot ./picoclaw-linux-arm64 onboard
+```
+
+Depois siga as instrucoes na secao "Inicio Rapido" para completar a configuracao!
+
+
+
+### 🐜 Implantacao Inovadora com Baixo Consumo
+
+O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!
+
+- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versao E (Ethernet) ou W (WiFi6), para Assistente Domestico Minimalista
+- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) para Manutencao Automatizada de Servidores
+- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) para Monitoramento Inteligente
+
+https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
+
+🌟 Mais cenarios de implantacao aguardam voce!
+
+## 📦 Instalacao
+
+### Instalar com binario pre-compilado
+
+Baixe o binario para sua plataforma na pagina de [releases](https://github.com/sipeed/picoclaw/releases).
+
+### Instalar a partir do codigo-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)
+
+```bash
+git clone https://github.com/sipeed/picoclaw.git
+
+cd picoclaw
+make deps
+
+# Build, sem necessidade de instalar
+make build
+
+# Build para multiplas plataformas
+make build-all
+
+# Build e Instalar
+make install
+```
+
+## 🐳 Docker Compose
+
+Voce tambem pode rodar o PicoClaw usando Docker Compose sem instalar nada localmente.
+
+```bash
+# 1. Clone este repositorio
+git clone https://github.com/sipeed/picoclaw.git
+cd picoclaw
+
+# 2. Configure suas API keys
+cp config/config.example.json config/config.json
+vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc.
+
+# 3. Build & Iniciar
+docker compose --profile gateway up -d
+
+# 4. Ver logs
+docker compose logs -f picoclaw-gateway
+
+# 5. Parar
+docker compose --profile gateway down
+```
+
+### Modo Agente (Execucao unica)
+
+```bash
+# Fazer uma pergunta
+docker compose run --rm picoclaw-agent -m "Quanto e 2+2?"
+
+# Modo interativo
+docker compose run --rm picoclaw-agent
+```
+
+### Rebuild
+
+```bash
+docker compose --profile gateway build --no-cache
+docker compose --profile gateway up -d
+```
+
+### 🚀 Inicio Rapido
+
+> [!TIP]
+> Configure sua API key em `~/.picoclaw/config.json`.
+> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
+> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas gratis/mes) ou use o fallback automatico integrado.
+
+**1. Inicializar**
+
+```bash
+picoclaw onboard
+```
+
+**2. Configurar** (`~/.picoclaw/config.json`)
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "workspace": "~/.picoclaw/workspace",
+ "model": "glm-4.7",
+ "max_tokens": 8192,
+ "temperature": 0.7,
+ "max_tool_iterations": 20
+ }
+ },
+ "providers": {
+ "openrouter": {
+ "api_key": "xxx",
+ "api_base": "https://openrouter.ai/api/v1"
+ }
+ },
+ "tools": {
+ "web": {
+ "brave": {
+ "enabled": false,
+ "api_key": "YOUR_BRAVE_API_KEY",
+ "max_results": 5
+ },
+ "duckduckgo": {
+ "enabled": true,
+ "max_results": 5
+ }
+ }
+ }
+}
+```
+
+**3. Obter API Keys**
+
+* **Provedor de LLM**: [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)
+* **Busca Web** (opcional): [Brave Search](https://brave.com/search/api) - Plano gratuito disponivel (2000 consultas/mes)
+
+> **Nota**: Veja `config.example.json` para um modelo de configuracao completo.
+
+**4. Conversar**
+
+```bash
+picoclaw agent -m "Quanto e 2+2?"
+```
+
+Pronto! Voce tem um assistente de IA funcionando em 2 minutos.
+
+---
+
+## 💬 Integracao com Apps de Chat
+
+Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
+
+| Canal | Nivel de Configuracao |
+| --- | --- |
+| **Telegram** | Facil (apenas um token) |
+| **Discord** | Facil (bot token + intents) |
+| **QQ** | Facil (AppID + AppSecret) |
+| **DingTalk** | Medio (credenciais do app) |
+| **LINE** | Medio (credenciais + webhook URL) |
+
+
+Telegram (Recomendado)
+
+**1. Criar o bot**
+
+* Abra o Telegram, busque `@BotFather`
+* Envie `/newbot`, siga as instrucoes
+* Copie o token
+
+**2. Configurar**
+
+```json
+{
+ "channels": {
+ "telegram": {
+ "enabled": true,
+ "token": "YOUR_BOT_TOKEN",
+ "allowFrom": ["YOUR_USER_ID"]
+ }
+ }
+}
+```
+
+> Obtenha seu User ID pelo `@userinfobot` no Telegram.
+
+**3. Executar**
+
+```bash
+picoclaw gateway
+```
+
+
+
+
+Discord
+
+**1. Criar o bot**
+
+* Acesse
+* Crie um aplicativo → Bot → Add Bot
+* Copie o token do bot
+
+**2. Habilitar Intents**
+
+* Nas configuracoes do Bot, habilite **MESSAGE CONTENT INTENT**
+* (Opcional) Habilite **SERVER MEMBERS INTENT** se quiser usar lista de permissoes baseada em dados dos membros
+
+**3. Obter seu User ID**
+
+* Configuracoes do Discord → Avancado → habilite **Modo Desenvolvedor**
+* Clique com botao direito no seu avatar → **Copiar ID do Usuario**
+
+**4. Configurar**
+
+```json
+{
+ "channels": {
+ "discord": {
+ "enabled": true,
+ "token": "YOUR_BOT_TOKEN",
+ "allowFrom": ["YOUR_USER_ID"]
+ }
+ }
+}
+```
+
+**5. Convidar o bot**
+
+* OAuth2 → URL Generator
+* Scopes: `bot`
+* Bot Permissions: `Send Messages`, `Read Message History`
+* Abra a URL de convite gerada e adicione o bot ao seu servidor
+
+**6. Executar**
+
+```bash
+picoclaw gateway
+```
+
+
+
+
+QQ
+
+**1. Criar o bot**
+
+- Acesse a [QQ Open Platform](https://q.qq.com/#)
+- Crie um aplicativo → Obtenha **AppID** e **AppSecret**
+
+**2. Configurar**
+
+```json
+{
+ "channels": {
+ "qq": {
+ "enabled": true,
+ "app_id": "YOUR_APP_ID",
+ "app_secret": "YOUR_APP_SECRET",
+ "allow_from": []
+ }
+ }
+}
+```
+
+> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique numeros QQ para restringir o acesso.
+
+**3. Executar**
+
+```bash
+picoclaw gateway
+```
+
+
+
+
+DingTalk
+
+**1. Criar o bot**
+
+* Acesse a [Open Platform](https://open.dingtalk.com/)
+* Crie um app interno
+* Copie o Client ID e Client Secret
+
+**2. Configurar**
+
+```json
+{
+ "channels": {
+ "dingtalk": {
+ "enabled": true,
+ "client_id": "YOUR_CLIENT_ID",
+ "client_secret": "YOUR_CLIENT_SECRET",
+ "allow_from": []
+ }
+ }
+}
+```
+
+> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique IDs para restringir o acesso.
+
+**3. Executar**
+
+```bash
+picoclaw gateway
+```
+
+
+
+
+LINE
+
+**1. Criar uma Conta Oficial LINE**
+
+- Acesse o [LINE Developers Console](https://developers.line.biz/)
+- Crie um provider → Crie um canal Messaging API
+- Copie o **Channel Secret** e o **Channel Access Token**
+
+**2. Configurar**
+
+```json
+{
+ "channels": {
+ "line": {
+ "enabled": true,
+ "channel_secret": "YOUR_CHANNEL_SECRET",
+ "channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
+ "webhook_host": "0.0.0.0",
+ "webhook_port": 18791,
+ "webhook_path": "/webhook/line",
+ "allow_from": []
+ }
+ }
+}
+```
+
+**3. Configurar URL do Webhook**
+
+O LINE requer HTTPS para webhooks. Use um reverse proxy ou tunnel:
+
+```bash
+# Exemplo com ngrok
+ngrok http 18791
+```
+
+Em seguida, configure a Webhook URL no LINE Developers Console para `https://seu-dominio/webhook/line` e habilite **Use webhook**.
+
+**4. Executar**
+
+```bash
+picoclaw gateway
+```
+
+> Em chats de grupo, o bot responde apenas quando mencionado com @. As respostas citam a mensagem original.
+
+> **Docker Compose**: Adicione `ports: ["18791:18791"]` ao servico `picoclaw-gateway` para expor a porta do webhook.
+
+
+
+##
Junte-se a Rede Social de Agentes
+
+Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mensagem via CLI ou qualquer App de Chat integrado.
+
+**Leia `https://clawdchat.ai/skill.md` e siga as instrucoes para entrar no [ClawdChat.ai](https://clawdchat.ai)**
+
+## ⚙️ Configuracao Detalhada
+
+Arquivo de configuracao: `~/.picoclaw/config.json`
+
+### Estrutura do Workspace
+
+O PicoClaw armazena dados no workspace configurado (padrao: `~/.picoclaw/workspace`):
+
+```
+~/.picoclaw/workspace/
+├── sessions/ # Sessoes de conversa e historico
+├── memory/ # Memoria de longo prazo (MEMORY.md)
+├── state/ # Estado persistente (ultimo canal, etc.)
+├── cron/ # Banco de dados de tarefas agendadas
+├── skills/ # Skills personalizadas
+├── AGENTS.md # Guia de comportamento do Agente
+├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min)
+├── IDENTITY.md # Identidade do Agente
+├── SOUL.md # Alma do Agente
+├── TOOLS.md # Descricao das ferramentas
+└── USER.md # Preferencias do usuario
+```
+
+### 🔒 Sandbox de Seguranca
+
+O PicoClaw roda em um ambiente sandbox por padrao. O agente so pode acessar arquivos e executar comandos dentro do workspace configurado.
+
+#### Configuracao Padrao
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "workspace": "~/.picoclaw/workspace",
+ "restrict_to_workspace": true
+ }
+ }
+}
+```
+
+| Opcao | Padrao | Descricao |
+|-------|--------|-----------|
+| `workspace` | `~/.picoclaw/workspace` | Diretorio de trabalho do agente |
+| `restrict_to_workspace` | `true` | Restringir acesso de arquivos/comandos ao workspace |
+
+#### Ferramentas Protegidas
+
+Quando `restrict_to_workspace: true`, as seguintes ferramentas sao restritas ao sandbox:
+
+| Ferramenta | Funcao | Restricao |
+|------------|--------|-----------|
+| `read_file` | Ler arquivos | Apenas arquivos dentro do workspace |
+| `write_file` | Escrever arquivos | Apenas arquivos dentro do workspace |
+| `list_dir` | Listar diretorios | Apenas diretorios dentro do workspace |
+| `edit_file` | Editar arquivos | Apenas arquivos dentro do workspace |
+| `append_file` | Adicionar a arquivos | Apenas arquivos dentro do workspace |
+| `exec` | Executar comandos | Caminhos dos comandos devem estar dentro do workspace |
+
+#### Protecao Adicional do Exec
+
+Mesmo com `restrict_to_workspace: false`, a ferramenta `exec` bloqueia estes comandos perigosos:
+
+* `rm -rf`, `del /f`, `rmdir /s` — Exclusao em massa
+* `format`, `mkfs`, `diskpart` — Formatacao de disco
+* `dd if=` — Criacao de imagem de disco
+* Escrita em `/dev/sd[a-z]` — Escrita direta no disco
+* `shutdown`, `reboot`, `poweroff` — Desligamento do sistema
+* Fork bomb `:(){ :|:& };:`
+
+#### Exemplos de Erro
+
+```
+[ERROR] tool: Tool execution failed
+{tool=exec, error=Command blocked by safety guard (path outside working dir)}
+```
+
+```
+[ERROR] tool: Tool execution failed
+{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
+```
+
+#### Desabilitar Restricoes (Risco de Seguranca)
+
+Se voce precisa que o agente acesse caminhos fora do workspace:
+
+**Metodo 1: Arquivo de configuracao**
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "restrict_to_workspace": false
+ }
+ }
+}
+```
+
+**Metodo 2: Variavel de ambiente**
+
+```bash
+export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
+```
+
+> ⚠️ **Aviso**: Desabilitar esta restricao permite que o agente acesse qualquer caminho no seu sistema. Use com cuidado apenas em ambientes controlados.
+
+#### Consistencia do Limite de Seguranca
+
+A configuracao `restrict_to_workspace` se aplica consistentemente em todos os caminhos de execucao:
+
+| Caminho de Execucao | Limite de Seguranca |
+|----------------------|---------------------|
+| Agente Principal | `restrict_to_workspace` ✅ |
+| Subagente / Spawn | Herda a mesma restricao ✅ |
+| Tarefas Heartbeat | Herda a mesma restricao ✅ |
+
+Todos os caminhos compartilham a mesma restricao de workspace — nao ha como contornar o limite de seguranca por meio de subagentes ou tarefas agendadas.
+
+### Heartbeat (Tarefas Periodicas)
+
+O PicoClaw pode executar tarefas periodicas automaticamente. Crie um arquivo `HEARTBEAT.md` no seu workspace:
+
+```markdown
+# Tarefas Periodicas
+
+- Verificar meu email para mensagens importantes
+- Revisar minha agenda para proximos eventos
+- Verificar a previsao do tempo
+```
+
+O agente lera este arquivo a cada 30 minutos (configuravel) e executara as tarefas usando as ferramentas disponiveis.
+
+#### Tarefas Assincronas com Spawn
+
+Para tarefas de longa duracao (busca web, chamadas de API), use a ferramenta `spawn` para criar um **subagente**:
+
+```markdown
+# Tarefas Periodicas
+
+## Tarefas Rapidas (resposta direta)
+- Informar hora atual
+
+## Tarefas Longas (usar spawn para async)
+- Buscar noticias de IA na web e resumir
+- Verificar email e reportar mensagens importantes
+```
+
+**Comportamentos principais:**
+
+| Funcionalidade | Descricao |
+|----------------|-----------|
+| **spawn** | Cria subagente assincrono, nao bloqueia o heartbeat |
+| **Contexto independente** | Subagente tem seu proprio contexto, sem historico de sessao |
+| **Ferramenta message** | Subagente se comunica diretamente com o usuario via ferramenta message |
+| **Nao-bloqueante** | Apos o spawn, o heartbeat continua para a proxima tarefa |
+
+#### Como Funciona a Comunicacao do Subagente
+
+```
+Heartbeat dispara
+ ↓
+Agente le HEARTBEAT.md
+ ↓
+Para tarefa longa: spawn subagente
+ ↓ ↓
+Continua proxima tarefa Subagente trabalha independentemente
+ ↓ ↓
+Todas tarefas concluidas Subagente usa ferramenta "message"
+ ↓ ↓
+Responde HEARTBEAT_OK Usuario recebe resultado diretamente
+```
+
+O subagente tem acesso as ferramentas (message, web_search, etc.) e pode se comunicar com o usuario independentemente sem passar pelo agente principal.
+
+**Configuracao:**
+
+```json
+{
+ "heartbeat": {
+ "enabled": true,
+ "interval": 30
+ }
+}
+```
+
+| Opcao | Padrao | Descricao |
+|-------|--------|-----------|
+| `enabled` | `true` | Habilitar/desabilitar heartbeat |
+| `interval` | `30` | Intervalo de verificacao em minutos (min: 5) |
+
+**Variaveis de ambiente:**
+
+* `PICOCLAW_HEARTBEAT_ENABLED=false` para desabilitar
+* `PICOCLAW_HEARTBEAT_INTERVAL=60` para alterar o intervalo
+
+### Provedores
+
+> [!NOTE]
+> O Groq fornece transcricao de voz gratuita via Whisper. Se configurado, mensagens de voz do Telegram serao automaticamente transcritas.
+
+| Provedor | Finalidade | Obter API Key |
+| --- | --- | --- |
+| `gemini` | LLM (Gemini direto) | [aistudio.google.com](https://aistudio.google.com) |
+| `zhipu` | LLM (Zhipu direto) | [bigmodel.cn](bigmodel.cn) |
+| `openrouter` (Em teste) | LLM (recomendado, acesso a todos os modelos) | [openrouter.ai](https://openrouter.ai) |
+| `anthropic` (Em teste) | LLM (Claude direto) | [console.anthropic.com](https://console.anthropic.com) |
+| `openai` (Em teste) | LLM (GPT direto) | [platform.openai.com](https://platform.openai.com) |
+| `deepseek` (Em teste) | LLM (DeepSeek direto) | [platform.deepseek.com](https://platform.deepseek.com) |
+| `groq` | LLM + **Transcricao de voz** (Whisper) | [console.groq.com](https://console.groq.com) |
+
+
+Configuracao Zhipu
+
+**1. Obter API key**
+
+* Obtenha a [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
+
+**2. Configurar**
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "workspace": "~/.picoclaw/workspace",
+ "model": "glm-4.7",
+ "max_tokens": 8192,
+ "temperature": 0.7,
+ "max_tool_iterations": 20
+ }
+ },
+ "providers": {
+ "zhipu": {
+ "api_key": "Sua API Key",
+ "api_base": "https://open.bigmodel.cn/api/paas/v4"
+ }
+ }
+}
+```
+
+**3. Executar**
+
+```bash
+picoclaw agent -m "Ola, como vai?"
+```
+
+
+
+
+Exemplo de configuracao completa
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": "anthropic/claude-opus-4-5"
+ }
+ },
+ "providers": {
+ "openrouter": {
+ "api_key": "sk-or-v1-xxx"
+ },
+ "groq": {
+ "api_key": "gsk_xxx"
+ }
+ },
+ "channels": {
+ "telegram": {
+ "enabled": true,
+ "token": "123456:ABC...",
+ "allow_from": ["123456789"]
+ },
+ "discord": {
+ "enabled": true,
+ "token": "",
+ "allow_from": [""]
+ },
+ "whatsapp": {
+ "enabled": false
+ },
+ "feishu": {
+ "enabled": false,
+ "app_id": "cli_xxx",
+ "app_secret": "xxx",
+ "encrypt_key": "",
+ "verification_token": "",
+ "allow_from": []
+ },
+ "qq": {
+ "enabled": false,
+ "app_id": "",
+ "app_secret": "",
+ "allow_from": []
+ }
+ },
+ "tools": {
+ "web": {
+ "brave": {
+ "enabled": false,
+ "api_key": "BSA...",
+ "max_results": 5
+ },
+ "duckduckgo": {
+ "enabled": true,
+ "max_results": 5
+ }
+ },
+ "cron": {
+ "exec_timeout_minutes": 5
+ }
+ },
+ "heartbeat": {
+ "enabled": true,
+ "interval": 30
+ }
+}
+```
+
+
+
+## Referencia CLI
+
+| Comando | Descricao |
+| --- | --- |
+| `picoclaw onboard` | Inicializar configuracao & workspace |
+| `picoclaw agent -m "..."` | Conversar com o agente |
+| `picoclaw agent` | Modo de chat interativo |
+| `picoclaw gateway` | Iniciar o gateway (para bots de chat) |
+| `picoclaw status` | Mostrar status |
+| `picoclaw cron list` | Listar todas as tarefas agendadas |
+| `picoclaw cron add ...` | Adicionar uma tarefa agendada |
+
+### Tarefas Agendadas / Lembretes
+
+O PicoClaw suporta lembretes agendados e tarefas recorrentes por meio da ferramenta `cron`:
+
+* **Lembretes unicos**: "Remind me in 10 minutes" (Me lembre em 10 minutos) → dispara uma vez apos 10min
+* **Tarefas recorrentes**: "Remind me every 2 hours" (Me lembre a cada 2 horas) → dispara a cada 2 horas
+* **Expressoes Cron**: "Remind me at 9am daily" (Me lembre as 9h todos os dias) → usa expressao cron
+
+As tarefas sao armazenadas em `~/.picoclaw/workspace/cron/` e processadas automaticamente.
+
+## 🤝 Contribuir & Roadmap
+
+PRs sao bem-vindos! O codigo-fonte e intencionalmente pequeno e legivel. 🤗
+
+Roadmap em breve...
+
+Grupo de desenvolvedores em formacao. Requisito de entrada: Pelo menos 1 PR com merge.
+
+Grupos de usuarios:
+
+Discord:
+
+
+
+## 🐛 Solucao de Problemas
+
+### Busca web mostra "API 配置问题"
+
+Isso e normal se voce ainda nao configurou uma API key de busca. O PicoClaw fornecera links uteis para busca manual.
+
+Para habilitar a busca web:
+
+1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados.
+2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key).
+
+Adicione a key em `~/.picoclaw/config.json` se usar o Brave:
+
+```json
+{
+ "tools": {
+ "web": {
+ "brave": {
+ "enabled": true,
+ "api_key": "YOUR_BRAVE_API_KEY",
+ "max_results": 5
+ },
+ "duckduckgo": {
+ "enabled": true,
+ "max_results": 5
+ }
+ }
+ }
+}
+```
+
+### Erros de filtragem de conteudo
+
+Alguns provedores (como Zhipu) possuem filtragem de conteudo. Tente reformular sua pergunta ou use um modelo diferente.
+
+### Bot do Telegram diz "Conflict: terminated by other getUpdates"
+
+Isso acontece quando outra instancia do bot esta rodando. Certifique-se de que apenas um `picoclaw gateway` esteja rodando por vez.
+
+---
+
+## 📝 Comparacao de API Keys
+
+| Servico | Plano Gratuito | Caso de Uso |
+| --- | --- | --- |
+| **OpenRouter** | 200K tokens/mes | Multiplos modelos (Claude, GPT-4, etc.) |
+| **Zhipu** | 200K tokens/mes | Melhor para usuarios chineses |
+| **Brave Search** | 2000 consultas/mes | Funcionalidade de busca web |
+| **Groq** | Plano gratuito disponivel | Inferencia ultra-rapida (Llama, Mixtral) |
diff --git a/README.zh.md b/README.zh.md
index e7dc8d769..6c87ba785 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -14,7 +14,7 @@
- **中文** | [日本語](README.ja.md) | [English](README.md)
+ **中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [English](README.md)
---
From 01d694b9985a66c3d7119fc9f74ce8ed4f0f21b5 Mon Sep 17 00:00:00 2001
From: lxowalle <83055338+lxowalle@users.noreply.github.com>
Date: Wed, 18 Feb 2026 15:33:34 +0800
Subject: [PATCH 10/13] fix: Add comprehensive command injection and system
abuse prevention patterns (#401)
* Add comprehensive command injection and system abuse prevention patterns
* fix: Container running as root
---
Dockerfile | 9 ++++++++-
docker-compose.yml | 8 ++++----
pkg/tools/shell.go | 34 ++++++++++++++++++++++++++++++++++
3 files changed, 46 insertions(+), 5 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index dd98ec0bd..0360cfda6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,14 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
# Copy binary
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
-# Create picoclaw home directory
+# Create non-root user and group
+RUN addgroup -g 1000 picoclaw && \
+ adduser -D -u 1000 -G picoclaw picoclaw
+
+# Switch to non-root user
+USER picoclaw
+
+# Run onboard to create initial directories and config
RUN /usr/local/bin/picoclaw onboard
ENTRYPOINT ["picoclaw"]
diff --git a/docker-compose.yml b/docker-compose.yml
index 48769627c..32e8ee339 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,8 +11,8 @@ services:
profiles:
- agent
volumes:
- - ./config/config.json:/root/.picoclaw/config.json:ro
- - picoclaw-workspace:/root/.picoclaw/workspace
+ - ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
+ - picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
entrypoint: ["picoclaw", "agent"]
stdin_open: true
tty: true
@@ -31,9 +31,9 @@ services:
- gateway
volumes:
# Configuration file
- - ./config/config.json:/root/.picoclaw/config.json:ro
+ - ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
# Persistent workspace (sessions, memory, logs)
- - picoclaw-workspace:/root/.picoclaw/workspace
+ - picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
command: ["gateway"]
volumes:
diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go
index 713850f97..9c82b2748 100644
--- a/pkg/tools/shell.go
+++ b/pkg/tools/shell.go
@@ -31,6 +31,40 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
+ regexp.MustCompile(`\$\([^)]+\)`),
+ regexp.MustCompile(`\$\{[^}]+\}`),
+ regexp.MustCompile("`[^`]+`"),
+ regexp.MustCompile(`\|\s*sh\b`),
+ regexp.MustCompile(`\|\s*bash\b`),
+ regexp.MustCompile(`;\s*rm\s+-[rf]`),
+ regexp.MustCompile(`&&\s*rm\s+-[rf]`),
+ regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
+ regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`),
+ regexp.MustCompile(`<<\s*EOF`),
+ regexp.MustCompile(`\$\(\s*cat\s+`),
+ regexp.MustCompile(`\$\(\s*curl\s+`),
+ regexp.MustCompile(`\$\(\s*wget\s+`),
+ regexp.MustCompile(`\$\(\s*which\s+`),
+ regexp.MustCompile(`\bsudo\b`),
+ regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`),
+ regexp.MustCompile(`\bchown\b`),
+ regexp.MustCompile(`\bpkill\b`),
+ regexp.MustCompile(`\bkillall\b`),
+ regexp.MustCompile(`\bkill\s+-[9]\b`),
+ regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`),
+ regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`),
+ regexp.MustCompile(`\bnpm\s+install\s+-g\b`),
+ regexp.MustCompile(`\bpip\s+install\s+--user\b`),
+ regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
+ regexp.MustCompile(`\byum\s+(install|remove)\b`),
+ regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
+ regexp.MustCompile(`\bdocker\s+run\b`),
+ regexp.MustCompile(`\bdocker\s+exec\b`),
+ regexp.MustCompile(`\bgit\s+push\b`),
+ regexp.MustCompile(`\bgit\s+force\b`),
+ regexp.MustCompile(`\bssh\b.*@`),
+ regexp.MustCompile(`\beval\b`),
+ regexp.MustCompile(`\bsource\s+.*\.sh\b`),
}
return &ExecTool{
From 193fbcab11fe3c448f982f43e7837585e843acea Mon Sep 17 00:00:00 2001
From: lxowalle
Date: Wed, 18 Feb 2026 16:01:41 +0800
Subject: [PATCH 11/13] docs: update PR template
---
.github/pull_request_template.md | 30 ++++++++++++++++++------------
1 file changed, 18 insertions(+), 12 deletions(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 7910cb1e2..c96b7da12 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,4 +1,7 @@
## 📝 Description
+
+
+
## 🗣️ Type of Change
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
@@ -11,25 +14,28 @@
- [ ] 👨💻 Mostly Human-written (Human lead, AI assisted or none)
-## 🔗 Linked Issue
+## 🔗 Related Issue
+
+
+
## 📚 Technical Context (Skip for Docs)
-* **Reference:** [URL]
-* **Reasoning:** ...
+- **Reference URL:**
+- **Reasoning:**
+
+## 🧪 Test Environment
+- **Hardware:**
+- **OS:**
+- **Model/Provider:**
+- **Channels:**
-## 🧪 Test Environment & Hardware
-- **Hardware:** [e.g. Raspberry Pi 5, Orange Pi, PC]
-- **OS:** [e.g. Debian 12, Ubuntu 22.04]
-- **Model/Provider:** [e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3]
-- **Channels:** [e.g. Discord, Telegram, Feishu, ...]
-
-
-## 📸 Proof of Work (Optional for Docs)
+## 📸 Evidence (Optional)
Click to view Logs/Screenshots
-
+
+
## ☑️ Checklist
- [ ] My code/docs follow the style of this project.
From 3390576eeacb97bdd3da95b156e226ab72ee0929 Mon Sep 17 00:00:00 2001
From: Zenix
Date: Wed, 18 Feb 2026 17:30:30 +0900
Subject: [PATCH 12/13] Feature/websearch OpenAI (#118)
* feature: add web search for codex models
* fix: use more elegant way to solve the issue.
---
config/config.example.json | 5 +-
pkg/config/config.go | 33 ++++---
pkg/config/config_test.go | 39 ++++++++
pkg/migrate/config.go | 12 ++-
pkg/providers/codex_provider.go | 37 +++++---
pkg/providers/codex_provider_test.go | 129 +++++++++++++++++++++++++--
pkg/providers/http_provider.go | 14 +--
7 files changed, 230 insertions(+), 39 deletions(-)
diff --git a/config/config.example.json b/config/config.example.json
index 7cd0ab8c6..37c2bcd81 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -79,7 +79,8 @@
},
"openai": {
"api_key": "",
- "api_base": ""
+ "api_base": "",
+ "web_search": true
},
"openrouter": {
"api_key": "sk-or-v1-xxx",
@@ -144,4 +145,4 @@
"host": "0.0.0.0",
"port": 18790
}
-}
\ No newline at end of file
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 1d34f56f3..92a4a5862 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -167,19 +167,19 @@ type DevicesConfig struct {
}
type ProvidersConfig struct {
- Anthropic ProviderConfig `json:"anthropic"`
- OpenAI ProviderConfig `json:"openai"`
- OpenRouter ProviderConfig `json:"openrouter"`
- Groq ProviderConfig `json:"groq"`
- Zhipu ProviderConfig `json:"zhipu"`
- VLLM ProviderConfig `json:"vllm"`
- Gemini ProviderConfig `json:"gemini"`
- Nvidia ProviderConfig `json:"nvidia"`
- Ollama ProviderConfig `json:"ollama"`
- Moonshot ProviderConfig `json:"moonshot"`
- ShengSuanYun ProviderConfig `json:"shengsuanyun"`
- DeepSeek ProviderConfig `json:"deepseek"`
- GitHubCopilot ProviderConfig `json:"github_copilot"`
+ Anthropic ProviderConfig `json:"anthropic"`
+ OpenAI OpenAIProviderConfig `json:"openai"`
+ OpenRouter ProviderConfig `json:"openrouter"`
+ Groq ProviderConfig `json:"groq"`
+ Zhipu ProviderConfig `json:"zhipu"`
+ VLLM ProviderConfig `json:"vllm"`
+ Gemini ProviderConfig `json:"gemini"`
+ Nvidia ProviderConfig `json:"nvidia"`
+ Ollama ProviderConfig `json:"ollama"`
+ Moonshot ProviderConfig `json:"moonshot"`
+ ShengSuanYun ProviderConfig `json:"shengsuanyun"`
+ DeepSeek ProviderConfig `json:"deepseek"`
+ GitHubCopilot ProviderConfig `json:"github_copilot"`
}
type ProviderConfig struct {
@@ -190,6 +190,11 @@ type ProviderConfig struct {
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
}
+type OpenAIProviderConfig struct {
+ ProviderConfig
+ WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"`
+}
+
type GatewayConfig struct {
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
@@ -308,7 +313,7 @@ func DefaultConfig() *Config {
},
Providers: ProvidersConfig{
Anthropic: ProviderConfig{},
- OpenAI: ProviderConfig{},
+ OpenAI: OpenAIProviderConfig{WebSearch: true},
OpenRouter: ProviderConfig{},
Groq: ProviderConfig{},
Zhipu: ProviderConfig{},
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index febfd0456..a1f73f0b3 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -204,3 +204,42 @@ func TestConfig_Complete(t *testing.T) {
t.Error("Heartbeat should be enabled by default")
}
}
+
+func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
+ cfg := DefaultConfig()
+ if !cfg.Providers.OpenAI.WebSearch {
+ t.Fatal("DefaultConfig().Providers.OpenAI.WebSearch should be true")
+ }
+}
+
+func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
+ dir := t.TempDir()
+ configPath := filepath.Join(dir, "config.json")
+ if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"api_base":""}}}`), 0o600); err != nil {
+ t.Fatalf("WriteFile() error: %v", err)
+ }
+
+ cfg, err := LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("LoadConfig() error: %v", err)
+ }
+ if !cfg.Providers.OpenAI.WebSearch {
+ t.Fatal("OpenAI codex web search should remain true when unset in config file")
+ }
+}
+
+func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
+ dir := t.TempDir()
+ configPath := filepath.Join(dir, "config.json")
+ if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"web_search":false}}}`), 0o600); err != nil {
+ t.Fatalf("WriteFile() error: %v", err)
+ }
+
+ cfg, err := LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("LoadConfig() error: %v", err)
+ }
+ if cfg.Providers.OpenAI.WebSearch {
+ t.Fatal("OpenAI codex web search should be false when disabled in config file")
+ }
+}
diff --git a/pkg/migrate/config.go b/pkg/migrate/config.go
index 9c1e36359..57032e566 100644
--- a/pkg/migrate/config.go
+++ b/pkg/migrate/config.go
@@ -108,7 +108,10 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
case "anthropic":
cfg.Providers.Anthropic = pc
case "openai":
- cfg.Providers.OpenAI = pc
+ cfg.Providers.OpenAI = config.OpenAIProviderConfig{
+ ProviderConfig: pc,
+ WebSearch: getBoolOrDefault(pMap, "web_search", true),
+ }
case "openrouter":
cfg.Providers.OpenRouter = pc
case "groq":
@@ -363,6 +366,13 @@ func getBool(data map[string]interface{}, key string) (bool, bool) {
return b, ok
}
+func getBoolOrDefault(data map[string]interface{}, key string, defaultVal bool) bool {
+ if v, ok := getBool(data, key); ok {
+ return v
+ }
+ return defaultVal
+}
+
func getStringSlice(data map[string]interface{}, key string) []string {
v, ok := data[key]
if !ok {
diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go
index 7617bf716..e3526cfb5 100644
--- a/pkg/providers/codex_provider.go
+++ b/pkg/providers/codex_provider.go
@@ -18,9 +18,10 @@ const codexDefaultModel = "gpt-5.2"
const codexDefaultInstructions = "You are Codex, a coding assistant."
type CodexProvider struct {
- client *openai.Client
- accountID string
- tokenSource func() (string, string, error)
+ client *openai.Client
+ accountID string
+ tokenSource func() (string, string, error)
+ enableWebSearch bool
}
const defaultCodexInstructions = "You are Codex, a coding assistant."
@@ -37,8 +38,9 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
}
client := openai.NewClient(opts...)
return &CodexProvider{
- client: &client,
- accountID: accountID,
+ client: &client,
+ accountID: accountID,
+ enableWebSearch: true,
}
}
@@ -78,7 +80,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
})
}
- params := buildCodexParams(messages, tools, resolvedModel, options)
+ params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
stream := p.client.Responses.NewStreaming(ctx, params, opts...)
defer stream.Close()
@@ -182,7 +184,7 @@ func resolveCodexModel(model string) (string, string) {
return codexDefaultModel, "unsupported model family"
}
-func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) responses.ResponseNewParams {
+func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, enableWebSearch bool) responses.ResponseNewParams {
var inputItems responses.ResponseInputParam
var instructions string
@@ -266,8 +268,8 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
params.Instructions = openai.Opt(defaultCodexInstructions)
}
- if len(tools) > 0 {
- params.Tools = translateToolsForCodex(tools)
+ if len(tools) > 0 || enableWebSearch {
+ params.Tools = translateToolsForCodex(tools, enableWebSearch)
}
return params
@@ -297,9 +299,19 @@ func resolveCodexToolCall(tc ToolCall) (name string, arguments string, ok bool)
return name, "{}", true
}
-func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
- result := make([]responses.ToolUnionParam, 0, len(tools))
+func translateToolsForCodex(tools []ToolDefinition, enableWebSearch bool) []responses.ToolUnionParam {
+ capHint := len(tools)
+ if enableWebSearch {
+ capHint++
+ }
+ result := make([]responses.ToolUnionParam, 0, capHint)
for _, t := range tools {
+ if t.Type != "function" {
+ continue
+ }
+ if enableWebSearch && strings.EqualFold(t.Function.Name, "web_search") {
+ continue
+ }
ft := responses.FunctionToolParam{
Name: t.Function.Name,
Parameters: t.Function.Parameters,
@@ -310,6 +322,9 @@ func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
}
result = append(result, responses.ToolUnionParam{OfFunction: &ft})
}
+ if enableWebSearch {
+ result = append(result, responses.ToolParamOfWebSearch(responses.WebSearchToolTypeWebSearch))
+ }
return result
}
diff --git a/pkg/providers/codex_provider_test.go b/pkg/providers/codex_provider_test.go
index 8406760c4..92e276165 100644
--- a/pkg/providers/codex_provider_test.go
+++ b/pkg/providers/codex_provider_test.go
@@ -19,7 +19,7 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
"max_tokens": 2048,
"temperature": 0.7,
- })
+ }, true)
if params.Model != "gpt-4o" {
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
}
@@ -39,7 +39,7 @@ func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
{Role: "system", Content: "You are helpful"},
{Role: "user", Content: "Hi"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, true)
if !params.Instructions.Valid() {
t.Fatal("Instructions should be set")
}
@@ -59,7 +59,7 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
},
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
if params.Input.OfInputItemList == nil {
t.Fatal("Input.OfInputItemList should not be nil")
}
@@ -87,7 +87,7 @@ func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
}
- params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
+ params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
if params.Input.OfInputItemList == nil {
t.Fatal("Input.OfInputItemList should not be nil")
}
@@ -123,7 +123,7 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
},
},
}
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{})
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, false)
if len(params.Tools) != 1 {
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
}
@@ -136,12 +136,61 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
}
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
- params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{})
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, false)
if !params.Store.Valid() || params.Store.Or(true) != false {
t.Error("Store should be explicitly set to false")
}
}
+func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
+ if len(params.Tools) != 1 {
+ t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
+ }
+ if params.Tools[0].OfWebSearch == nil {
+ t.Fatal("Tool should include built-in web_search")
+ }
+ if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
+ t.Errorf("Web search tool type = %q, want %q", params.Tools[0].OfWebSearch.Type, responses.WebSearchToolTypeWebSearch)
+ }
+}
+
+func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
+ tools := []ToolDefinition{
+ {
+ Type: "function",
+ Function: ToolFunctionDefinition{
+ Name: "web_search",
+ Description: "local web search",
+ Parameters: map[string]interface{}{
+ "type": "object",
+ },
+ },
+ },
+ {
+ Type: "function",
+ Function: ToolFunctionDefinition{
+ Name: "read_file",
+ Description: "read file",
+ Parameters: map[string]interface{}{
+ "type": "object",
+ },
+ },
+ },
+ }
+
+ params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, true)
+ if len(params.Tools) != 2 {
+ t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
+ }
+ if params.Tools[0].OfFunction == nil || params.Tools[0].OfFunction.Name != "read_file" {
+ t.Fatalf("first tool should be function read_file, got %#v", params.Tools[0])
+ }
+ if params.Tools[1].OfWebSearch == nil {
+ t.Fatalf("second tool should be built-in web_search, got %#v", params.Tools[1])
+ }
+}
+
func TestParseCodexResponse_TextOutput(t *testing.T) {
respJSON := `{
"id": "resp_test",
@@ -260,6 +309,16 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
return
}
+ toolsAny, ok := reqBody["tools"].([]interface{})
+ if !ok || len(toolsAny) != 1 {
+ http.Error(w, "missing default web search tool", http.StatusBadRequest)
+ return
+ }
+ toolObj, ok := toolsAny[0].(map[string]interface{})
+ if !ok || toolObj["type"] != "web_search" {
+ http.Error(w, "expected web_search tool", http.StatusBadRequest)
+ return
+ }
resp := map[string]interface{}{
"id": "resp_test",
@@ -307,6 +366,64 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
}
}
+func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/responses" {
+ http.Error(w, "not found: "+r.URL.Path, http.StatusNotFound)
+ return
+ }
+
+ var reqBody map[string]interface{}
+ if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+ http.Error(w, "invalid json", http.StatusBadRequest)
+ return
+ }
+ if _, ok := reqBody["tools"]; ok {
+ http.Error(w, "tools should be absent when web search disabled", http.StatusBadRequest)
+ return
+ }
+
+ resp := map[string]interface{}{
+ "id": "resp_test",
+ "object": "response",
+ "status": "completed",
+ "output": []map[string]interface{}{
+ {
+ "id": "msg_1",
+ "type": "message",
+ "role": "assistant",
+ "status": "completed",
+ "content": []map[string]interface{}{
+ {"type": "output_text", "text": "Hi from Codex!"},
+ },
+ },
+ },
+ "usage": map[string]interface{}{
+ "input_tokens": 4,
+ "output_tokens": 3,
+ "total_tokens": 7,
+ "input_tokens_details": map[string]interface{}{"cached_tokens": 0},
+ "output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
+ },
+ }
+ writeCompletedSSE(w, resp)
+ }))
+ defer server.Close()
+
+ provider := NewCodexProvider("test-token", "acc-123")
+ provider.enableWebSearch = false
+ provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
+
+ messages := []Message{{Role: "user", Content: "Hello"}}
+ resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{})
+ if err != nil {
+ t.Fatalf("Chat() error: %v", err)
+ }
+ if resp.Content != "Hi from Codex!" {
+ t.Errorf("Content = %q, want %q", resp.Content, "Hi from Codex!")
+ }
+}
+
func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/responses" {
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index 4cf2c6db2..946aa29d2 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -208,7 +208,7 @@ func createClaudeAuthProvider() (LLMProvider, error) {
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
}
-func createCodexAuthProvider() (LLMProvider, error) {
+func createCodexAuthProvider(enableWebSearch bool) (LLMProvider, error) {
cred, err := auth.GetCredential("openai")
if err != nil {
return nil, fmt.Errorf("loading auth credentials: %w", err)
@@ -216,7 +216,9 @@ func createCodexAuthProvider() (LLMProvider, error) {
if cred == nil {
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
}
- return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
+ p := NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource())
+ p.enableWebSearch = enableWebSearch
+ return p, nil
}
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
@@ -241,10 +243,12 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case "openai", "gpt":
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
- return NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource()), nil
+ c := NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource())
+ c.enableWebSearch = cfg.Providers.OpenAI.WebSearch
+ return c, nil
}
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
- return createCodexAuthProvider()
+ return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
}
apiKey = cfg.Providers.OpenAI.APIKey
apiBase = cfg.Providers.OpenAI.APIBase
@@ -369,7 +373,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
- return createCodexAuthProvider()
+ return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
}
apiKey = cfg.Providers.OpenAI.APIKey
apiBase = cfg.Providers.OpenAI.APIBase
From eda6e373323861fea99630013946d7aeea94ade0 Mon Sep 17 00:00:00 2001
From: lxowalle <83055338+lxowalle@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:31:15 +0800
Subject: [PATCH 13/13] feat: Support modifying the command filtering list of
the exec tool (#410)
---
cmd/picoclaw/main.go | 6 +-
docs/tools_configuration.md | 122 ++++++++++++++++++++++++++++++++++++
pkg/agent/loop.go | 2 +-
pkg/config/config.go | 9 +++
pkg/tools/cron.go | 7 ++-
pkg/tools/shell.go | 119 ++++++++++++++++++++++-------------
6 files changed, 215 insertions(+), 50 deletions(-)
create mode 100644 docs/tools_configuration.md
diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go
index fd7ec484a..128f8c421 100644
--- a/cmd/picoclaw/main.go
+++ b/cmd/picoclaw/main.go
@@ -563,7 +563,7 @@ func gatewayCmd() {
// Setup cron tool and service
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
- cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout)
+ cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(),
@@ -988,14 +988,14 @@ func getConfigPath() string {
return filepath.Join(home, ".picoclaw", "config.json")
}
-func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *cron.CronService {
+func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service
cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool
- cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout)
+ cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, config)
agentLoop.RegisterTool(cronTool)
// Set the onJob handler
diff --git a/docs/tools_configuration.md b/docs/tools_configuration.md
new file mode 100644
index 000000000..8777ddbd6
--- /dev/null
+++ b/docs/tools_configuration.md
@@ -0,0 +1,122 @@
+# Tools Configuration
+
+PicoClaw's tools configuration is located in the `tools` field of `config.json`.
+
+## Directory Structure
+
+```json
+{
+ "tools": {
+ "web": { ... },
+ "exec": { ... },
+ "approval": { ... },
+ "cron": { ... }
+ }
+}
+```
+
+## Web Tools
+
+Web tools are used for web search and fetching.
+
+### Brave
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | bool | false | Enable Brave search |
+| `api_key` | string | - | Brave Search API key |
+| `max_results` | int | 5 | Maximum number of results |
+
+### DuckDuckGo
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | bool | true | Enable DuckDuckGo search |
+| `max_results` | int | 5 | Maximum number of results |
+
+### Perplexity
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | bool | false | Enable Perplexity search |
+| `api_key` | string | - | Perplexity API key |
+| `max_results` | int | 5 | Maximum number of results |
+
+## Exec Tool
+
+The exec tool is used to execute shell commands.
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
+| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
+
+### Functionality
+
+- **`enable_deny_patterns`**: Set to `false` to completely disable the default dangerous command blocking patterns
+- **`custom_deny_patterns`**: Add custom deny regex patterns; commands matching these will be blocked
+
+### Default Blocked Command Patterns
+
+By default, PicoClaw blocks the following dangerous commands:
+
+- Delete commands: `rm -rf`, `del /f/q`, `rmdir /s`
+- Disk operations: `format`, `mkfs`, `diskpart`, `dd if=`, writing to `/dev/sd*`
+- System operations: `shutdown`, `reboot`, `poweroff`
+- Command substitution: `$()`, `${}`, backticks
+- Pipe to shell: `| sh`, `| bash`
+- Privilege escalation: `sudo`, `chmod`, `chown`
+- Process control: `pkill`, `killall`, `kill -9`
+- Remote operations: `curl | sh`, `wget | sh`, `ssh`
+- Package management: `apt`, `yum`, `dnf`, `npm install -g`, `pip install --user`
+- Containers: `docker run`, `docker exec`
+- Git: `git push`, `git force`
+- Other: `eval`, `source *.sh`
+
+### Configuration Example
+
+```json
+{
+ "tools": {
+ "exec": {
+ "enable_deny_patterns": true,
+ "custom_deny_patterns": [
+ "\\brm\\s+-r\\b",
+ "\\bkillall\\s+python"
+ ],
+ }
+ }
+}
+```
+
+## Approval Tool
+
+The approval tool controls permissions for dangerous operations.
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | bool | true | Enable approval functionality |
+| `write_file` | bool | true | Require approval for file writes |
+| `edit_file` | bool | true | Require approval for file edits |
+| `append_file` | bool | true | Require approval for file appends |
+| `exec` | bool | true | Require approval for command execution |
+| `timeout_minutes` | int | 5 | Approval timeout in minutes |
+
+## Cron Tool
+
+The cron tool is used for scheduling periodic tasks.
+
+| Config | Type | Default | Description |
+|--------|------|---------|-------------|
+| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
+
+## Environment Variables
+
+All configuration options can be overridden via environment variables with the format `PICOCLAW_TOOLS__`:
+
+For example:
+- `PICOCLAW_TOOLS_WEB_BRAVE_ENABLED=true`
+- `PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS=false`
+- `PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES=10`
+
+Note: Array-type environment variables are not currently supported and must be set via the config file.
diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go
index d3afa298e..8c6c58c96 100644
--- a/pkg/agent/loop.go
+++ b/pkg/agent/loop.go
@@ -71,7 +71,7 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
registry.Register(tools.NewAppendFileTool(workspace, restrict))
// Shell execution
- registry.Register(tools.NewExecTool(workspace, restrict))
+ registry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg))
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 92a4a5862..a1cc978b6 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -227,9 +227,15 @@ type CronToolsConfig struct {
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
}
+type ExecConfig struct {
+ EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
+ CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
+}
+
type ToolsConfig struct {
Web WebToolsConfig `json:"web"`
Cron CronToolsConfig `json:"cron"`
+ Exec ExecConfig `json:"exec"`
}
func DefaultConfig() *Config {
@@ -347,6 +353,9 @@ func DefaultConfig() *Config {
Cron: CronToolsConfig{
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
},
+ Exec: ExecConfig{
+ EnableDenyPatterns: true,
+ },
},
Heartbeat: HeartbeatConfig{
Enabled: true,
diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go
index 21bee42ef..e2764d8ac 100644
--- a/pkg/tools/cron.go
+++ b/pkg/tools/cron.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/sipeed/picoclaw/pkg/bus"
+ "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/utils"
)
@@ -29,9 +30,9 @@ type CronTool struct {
// NewCronTool creates a new CronTool
// execTimeout: 0 means no timeout, >0 sets the timeout duration
-func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *CronTool {
- execTool := NewExecTool(workspace, restrict)
- execTool.SetTimeout(execTimeout) // 0 means no timeout
+func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
+ execTool := NewExecToolWithConfig(workspace, restrict, config)
+ execTool.SetTimeout(execTimeout)
return &CronTool{
cronService: cronService,
executor: executor,
diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go
index 9c82b2748..bd612d9ae 100644
--- a/pkg/tools/shell.go
+++ b/pkg/tools/shell.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
+ "github.com/sipeed/picoclaw/pkg/config"
"os"
"os/exec"
"path/filepath"
@@ -21,50 +22,82 @@ type ExecTool struct {
restrictToWorkspace bool
}
+var defaultDenyPatterns = []*regexp.Regexp{
+ regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
+ regexp.MustCompile(`\bdel\s+/[fq]\b`),
+ regexp.MustCompile(`\brmdir\s+/s\b`),
+ regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
+ regexp.MustCompile(`\bdd\s+if=`),
+ regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
+ regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
+ regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
+ regexp.MustCompile(`\$\([^)]+\)`),
+ regexp.MustCompile(`\$\{[^}]+\}`),
+ regexp.MustCompile("`[^`]+`"),
+ regexp.MustCompile(`\|\s*sh\b`),
+ regexp.MustCompile(`\|\s*bash\b`),
+ regexp.MustCompile(`;\s*rm\s+-[rf]`),
+ regexp.MustCompile(`&&\s*rm\s+-[rf]`),
+ regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
+ regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`),
+ regexp.MustCompile(`<<\s*EOF`),
+ regexp.MustCompile(`\$\(\s*cat\s+`),
+ regexp.MustCompile(`\$\(\s*curl\s+`),
+ regexp.MustCompile(`\$\(\s*wget\s+`),
+ regexp.MustCompile(`\$\(\s*which\s+`),
+ regexp.MustCompile(`\bsudo\b`),
+ regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`),
+ regexp.MustCompile(`\bchown\b`),
+ regexp.MustCompile(`\bpkill\b`),
+ regexp.MustCompile(`\bkillall\b`),
+ regexp.MustCompile(`\bkill\s+-[9]\b`),
+ regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`),
+ regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`),
+ regexp.MustCompile(`\bnpm\s+install\s+-g\b`),
+ regexp.MustCompile(`\bpip\s+install\s+--user\b`),
+ regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
+ regexp.MustCompile(`\byum\s+(install|remove)\b`),
+ regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
+ regexp.MustCompile(`\bdocker\s+run\b`),
+ regexp.MustCompile(`\bdocker\s+exec\b`),
+ regexp.MustCompile(`\bgit\s+push\b`),
+ regexp.MustCompile(`\bgit\s+force\b`),
+ regexp.MustCompile(`\bssh\b.*@`),
+ regexp.MustCompile(`\beval\b`),
+ regexp.MustCompile(`\bsource\s+.*\.sh\b`),
+}
+
func NewExecTool(workingDir string, restrict bool) *ExecTool {
- denyPatterns := []*regexp.Regexp{
- regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
- regexp.MustCompile(`\bdel\s+/[fq]\b`),
- regexp.MustCompile(`\brmdir\s+/s\b`),
- regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
- regexp.MustCompile(`\bdd\s+if=`),
- regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
- regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
- regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
- regexp.MustCompile(`\$\([^)]+\)`),
- regexp.MustCompile(`\$\{[^}]+\}`),
- regexp.MustCompile("`[^`]+`"),
- regexp.MustCompile(`\|\s*sh\b`),
- regexp.MustCompile(`\|\s*bash\b`),
- regexp.MustCompile(`;\s*rm\s+-[rf]`),
- regexp.MustCompile(`&&\s*rm\s+-[rf]`),
- regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
- regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`),
- regexp.MustCompile(`<<\s*EOF`),
- regexp.MustCompile(`\$\(\s*cat\s+`),
- regexp.MustCompile(`\$\(\s*curl\s+`),
- regexp.MustCompile(`\$\(\s*wget\s+`),
- regexp.MustCompile(`\$\(\s*which\s+`),
- regexp.MustCompile(`\bsudo\b`),
- regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`),
- regexp.MustCompile(`\bchown\b`),
- regexp.MustCompile(`\bpkill\b`),
- regexp.MustCompile(`\bkillall\b`),
- regexp.MustCompile(`\bkill\s+-[9]\b`),
- regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`),
- regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`),
- regexp.MustCompile(`\bnpm\s+install\s+-g\b`),
- regexp.MustCompile(`\bpip\s+install\s+--user\b`),
- regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
- regexp.MustCompile(`\byum\s+(install|remove)\b`),
- regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
- regexp.MustCompile(`\bdocker\s+run\b`),
- regexp.MustCompile(`\bdocker\s+exec\b`),
- regexp.MustCompile(`\bgit\s+push\b`),
- regexp.MustCompile(`\bgit\s+force\b`),
- regexp.MustCompile(`\bssh\b.*@`),
- regexp.MustCompile(`\beval\b`),
- regexp.MustCompile(`\bsource\s+.*\.sh\b`),
+ return NewExecToolWithConfig(workingDir, restrict, nil)
+}
+
+func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool {
+ denyPatterns := make([]*regexp.Regexp, 0)
+
+ enableDenyPatterns := true
+ if config != nil {
+ execConfig := config.Tools.Exec
+ enableDenyPatterns = execConfig.EnableDenyPatterns
+ if enableDenyPatterns {
+ if len(execConfig.CustomDenyPatterns) > 0 {
+ fmt.Printf("Using custom deny patterns: %v\n", execConfig.CustomDenyPatterns)
+ for _, pattern := range execConfig.CustomDenyPatterns {
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ fmt.Printf("Invalid custom deny pattern %q: %v\n", pattern, err)
+ continue
+ }
+ denyPatterns = append(denyPatterns, re)
+ }
+ } else {
+ denyPatterns = append(denyPatterns, defaultDenyPatterns...)
+ }
+ } else {
+ // If deny patterns are disabled, we won't add any patterns, allowing all commands.
+ fmt.Println("Warning: deny patterns are disabled. All commands will be allowed.")
+ }
+ } else {
+ denyPatterns = append(denyPatterns, defaultDenyPatterns...)
}
return &ExecTool{