mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-08-04 01:19:36 +00:00
Compare commits
87 Commits
v0.3.0
..
52320f4875
| Author | SHA1 | Date | |
|---|---|---|---|
| 52320f4875 | |||
| c15aac21fe | |||
| 7ee4f41b7e | |||
| d65d592a40 | |||
| 06830c9b19 | |||
| 9bc8d15685 | |||
| 979f440af7 | |||
| 377c25e7c5 | |||
| 2cdfadaa8b | |||
| 0eb721fff7 | |||
| 14c9a1f1f8 | |||
| 500ce72436 | |||
| 583d9e1e14 | |||
| 1bd2e84527 | |||
| 36d72a0112 | |||
| 1260fd8e2f | |||
| 54f1ab75e2 | |||
| 7f90ce7a98 | |||
| 70b2364f39 | |||
| 124c529bf1 | |||
| 5feeb91c60 | |||
| 9287df415b | |||
| ae81ae93cf | |||
| e4bda8c4a1 | |||
| 3400221527 | |||
| 7ee4ee3b64 | |||
| 62a2b0015a | |||
| d6371fcb68 | |||
| dff16dbb91 | |||
| 0a47fc8eb1 | |||
| f53e6e68e2 | |||
| 288013ca0f | |||
| 5e3fd4cd0f | |||
| 540f81ff9e | |||
| a75b3d15bb | |||
| eb8b6ca7c8 | |||
| ec9c6cde8e | |||
| 646c2db1e9 | |||
| dc46c05aac | |||
| 43c671abdc | |||
| a63b44ac74 | |||
| 287853ab58 | |||
| 0d1ed99007 | |||
| 70afab59e6 | |||
| a3be5a6b25 | |||
| 62206befe1 | |||
| 38a1d823c3 | |||
| 35db389025 | |||
| aeab7b15a4 | |||
| 6d0ca80f21 | |||
| 4514d54ff5 | |||
| d4adbf9418 | |||
| 99291182e7 | |||
| 6c31f9ffdd | |||
| e3464f4388 | |||
| 08192ddf1b | |||
| 7b5fe01187 | |||
| 1420875921 | |||
| 234bd03018 | |||
| b2fbe83565 | |||
| a16a1e1535 | |||
| 910bfbe6a4 | |||
| 03b022c2ac | |||
| cc4055b6d4 | |||
| 86221d470b | |||
| e720be0dd7 | |||
| c9f5872e02 | |||
| 0c1e6b5279 | |||
| 981ab3affe | |||
| 3056a540bf | |||
| 5b41f18c3d | |||
| 7019143c8e | |||
| 083e68b49a | |||
| fcc2882ffa | |||
| b292defd95 | |||
| ed55715b6b | |||
| 2f8c462366 | |||
| d2156a982c | |||
| 9eadffda1e | |||
| e55ddf2e24 | |||
| 7de3d1f7e3 | |||
| d26e4eca7d | |||
| bbdf746bcb | |||
| 409cc051e7 | |||
| c5a98a5f47 | |||
| 351ecf0182 | |||
| f5add27d39 |
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run Docker-backed integration suites
|
- name: Run Docker-backed integration suites
|
||||||
run: bash ./scripts/run-integration-tests.sh
|
run: bash ./scripts/run-integration-tests.sh
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# ── Checkout ──────────────────────────────
|
# ── Checkout ──────────────────────────────
|
||||||
- name: 📥 Checkout repository
|
- name: 📥 Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
GOFLAGS: -tags=goolm,stdjson
|
GOFLAGS: -tags=goolm,stdjson
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run Docker-backed integration suites
|
- name: Run Docker-backed integration suites
|
||||||
run: bash ./scripts/run-integration-tests.sh
|
run: bash ./scripts/run-integration-tests.sh
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout tag
|
- name: Checkout tag
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](docs/project/README.zh.md) | [日本語](docs/project/README.ja.md) | [한국어](docs/project/README.ko.md) | [Português](docs/project/README.pt-br.md) | [Tiếng Việt](docs/project/README.vi.md) | [Français](docs/project/README.fr.md) | [Italiano](docs/project/README.it.md) | [Bahasa Indonesia](docs/project/README.id.md) | [Malay](docs/project/README.ms.md) | **English**
|
[中文](docs/project/README.zh.md) | [日本語](docs/project/README.ja.md) | [한국어](docs/project/README.ko.md) | [Português](docs/project/README.pt-br.md) | [Tiếng Việt](docs/project/README.vi.md) | [Français](docs/project/README.fr.md) | [Italiano](docs/project/README.it.md) | [Bahasa Indonesia](docs/project/README.id.md) | [Malay](docs/project/README.ms.md) | **English**
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -49,13 +60,15 @@
|
|||||||
> **Security Notice**
|
> **Security Notice**
|
||||||
>
|
>
|
||||||
> * **NO CRYPTO:** PicoClaw has **not** issued any official tokens or cryptocurrency. All claims on `pump.fun` or other trading platforms are **scams**.
|
> * **NO CRYPTO:** PicoClaw has **not** issued any official tokens or cryptocurrency. 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)**
|
> * **OFFICIAL DOMAIN:** The **ONLY** official PicoClaw website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)**
|
||||||
> * **BEWARE:** Many `.ai/.org/.com/.net/...` domains have been registered by third parties. Do not trust them.
|
> * **BEWARE:** Many lookalike `.ai/.org/.com/.net/...` domains have been registered by third parties. Only trust domains explicitly linked from this README.
|
||||||
> * **NOTE:** PicoClaw is in early rapid development. There may be unresolved security issues. Do not deploy to production before v1.0.
|
> * **NOTE:** PicoClaw is in early rapid development. There may be unresolved security issues. Do not deploy to production before v1.0.
|
||||||
> * **NOTE:** PicoClaw has recently merged many PRs. Recent builds may use 10-20MB RAM. Resource optimization is planned after feature stabilization.
|
> * **NOTE:** PicoClaw has recently merged many PRs. Recent builds may use 10-20MB RAM. Resource optimization is planned after feature stabilization.
|
||||||
|
|
||||||
## 📢 News
|
## 📢 News
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw on AliExpress!** You can now purchase LicheeRV-Claw from [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), making it easier to try PicoClaw on compact RISC-V hardware.
|
2026-05-11 🛒 **LicheeRV-Claw on AliExpress!** You can now purchase LicheeRV-Claw from [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), making it easier to try PicoClaw on compact RISC-V hardware.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -420,6 +433,7 @@ PicoClaw supports 30+ LLM providers through the `model_list` configuration. Use
|
|||||||
| [Mistral](https://console.mistral.ai/api-keys) | `mistral/` | Required | Mistral Large, Codestral |
|
| [Mistral](https://console.mistral.ai/api-keys) | `mistral/` | Required | Mistral Large, Codestral |
|
||||||
| [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models |
|
| [NVIDIA NIM](https://build.nvidia.com/) | `nvidia/` | Required | NVIDIA hosted models |
|
||||||
| [Cerebras](https://cloud.cerebras.ai/) | `cerebras/` | Required | Fast inference |
|
| [Cerebras](https://cloud.cerebras.ai/) | `cerebras/` | Required | Fast inference |
|
||||||
|
| [NEAR AI Cloud](https://near.ai/) | `nearai/` | Required | TEE inference, OpenAI-compatible |
|
||||||
| [Novita AI](https://novita.ai/) | `novita/` | Required | Various open models |
|
| [Novita AI](https://novita.ai/) | `novita/` | Required | Various open models |
|
||||||
| [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models |
|
| [Xiaomi MiMo](https://platform.xiaomimimo.com/) | `mimo/` | Required | MiMo models |
|
||||||
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |
|
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
@@ -155,7 +155,7 @@ func (c *LLMClient) Complete(ctx context.Context, systemPrompt, userPrompt strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
respBody, lastErr = io.ReadAll(resp.Body)
|
respBody, lastErr = io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -68,6 +69,21 @@ func TestFetchOpenAIModels_HTTPError(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "HTTP 401")
|
assert.Contains(t, err.Error(), "HTTP 401")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetchOpenAIModels_HTTPErrorReadFailure(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
body := "short"
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(body)+1))
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, _ = w.Write([]byte(body))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
_, err := fetchOpenAIModels(srv.URL, "bad")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "read error response")
|
||||||
|
assert.Contains(t, err.Error(), "unexpected EOF")
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetchOpenAIModels_EmptyDataEnvelope(t *testing.T) {
|
func TestFetchOpenAIModels_EmptyDataEnvelope(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = w.Write([]byte(`{"data":[]}`))
|
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ func fetchOpenAIModels(baseURL, apiKey string) ([]modelEntry, error) {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("read error response: %w", readErr)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,11 @@ func skillsSearchCmd(query string) {
|
|||||||
if result.Version != "" {
|
if result.Version != "" {
|
||||||
fmt.Printf(" Version: %s\n", result.Version)
|
fmt.Printf(" Version: %s\n", result.Version)
|
||||||
}
|
}
|
||||||
|
if result.RegistryName == "github" {
|
||||||
|
fmt.Printf(" Install: picoclaw skills install %s\n", result.Slug)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Install: picoclaw skills install --registry=%s %s\n", result.RegistryName, result.Slug)
|
||||||
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"model": "venice/venice-uncensored",
|
"model": "venice/venice-uncensored",
|
||||||
"api_keys": ["your-venice-api-key"]
|
"api_keys": ["your-venice-api-key"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model_name": "nearai-glm",
|
||||||
|
"model": "nearai/zai-org/GLM-5.1-FP8",
|
||||||
|
"api_keys": ["your-nearai-api-key"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model_name": "lmstudio-local",
|
"model_name": "lmstudio-local",
|
||||||
"model": "lmstudio/openai/gpt-oss-20b"
|
"model": "lmstudio/openai/gpt-oss-20b"
|
||||||
@@ -380,7 +385,9 @@
|
|||||||
},
|
},
|
||||||
"cron": {
|
"cron": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"exec_timeout_minutes": 5
|
"exec_timeout_minutes": 5,
|
||||||
|
"allow_command": true,
|
||||||
|
"command_allowed_remotes": []
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|||||||
@@ -1154,7 +1154,9 @@ PicoClaw supports cron-style scheduled tasks via the `cron` tool. The agent can
|
|||||||
"tools": {
|
"tools": {
|
||||||
"cron": {
|
"cron": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"exec_timeout_minutes": 5
|
"exec_timeout_minutes": 5,
|
||||||
|
"allow_command": true,
|
||||||
|
"command_allowed_remotes": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1162,6 +1164,8 @@ PicoClaw supports cron-style scheduled tasks via the `cron` tool. The agent can
|
|||||||
|
|
||||||
Scheduled tasks persist across restarts and are stored in `~/.picoclaw/workspace/cron/`.
|
Scheduled tasks persist across restarts and are stored in `~/.picoclaw/workspace/cron/`.
|
||||||
|
|
||||||
|
Command cron jobs can execute shell commands. By default, remote channels cannot schedule command jobs. To allow specific remote channels, set `command_allowed_remotes` to entries such as `"telegram"` or `"telegram:1234567890"`; use `"*"` only if every non-empty channel should be allowed. The `"*"` wildcard is potentially dangerous because any remote channel that can talk to PicoClaw can schedule shell commands. This does not bypass `allow_command`, `command_confirm`, or exec safety checks.
|
||||||
|
|
||||||
### Advanced Topics
|
### Advanced Topics
|
||||||
|
|
||||||
| Topic | Description |
|
| Topic | Description |
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
|
| `anthropic` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
|
||||||
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
|
| `openai` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
|
||||||
| `venice` | LLM (Venice AI direct) | [venice.ai](https://venice.ai) |
|
| `venice` | LLM (Venice AI direct) | [venice.ai](https://venice.ai) |
|
||||||
|
| `nearai` | LLM (NEAR AI Cloud TEE inference) | [near.ai](https://near.ai) |
|
||||||
| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
|
| `deepseek` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
|
||||||
| `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
|
| `qwen` | LLM (Qwen direct) | [dashscope.console.aliyun.com](https://dashscope.console.aliyun.com) |
|
||||||
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
|
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
|
||||||
@@ -50,6 +51,7 @@ This design also enables **multi-agent support** with flexible provider selectio
|
|||||||
| ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
|
| ------------------- | ----------------- |-----------------------------------------------------| --------- | ---------------------------------------------------------------- |
|
||||||
| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
|
| **OpenAI** | `openai` | `https://api.openai.com/v1` | OpenAI | [Get Key](https://platform.openai.com) |
|
||||||
| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
|
| **Venice AI** | `venice` | `https://api.venice.ai/api/v1` | OpenAI | [Get Key](https://venice.ai) |
|
||||||
|
| **NEAR AI Cloud** | `nearai` | `https://cloud-api.near.ai/v1` | OpenAI | [Get Key](https://near.ai) |
|
||||||
| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
|
| **Anthropic** | `anthropic` | `https://api.anthropic.com/v1` | Anthropic | [Get Key](https://console.anthropic.com) |
|
||||||
| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
| **智谱 AI (GLM)** | `zhipu` | `https://open.bigmodel.cn/api/paas/v4` | OpenAI | [Get Key](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||||
| **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
|
| **Z.AI Coding Plan** | `openai` | `https://api.z.ai/api/coding/paas/v4` | OpenAI | [Get Key](https://z.ai/manage-apikey/apikey-list) |
|
||||||
@@ -256,6 +258,17 @@ Notes that matter:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**NEAR AI Cloud**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "nearai-glm",
|
||||||
|
"provider": "nearai",
|
||||||
|
"model": "zai-org/GLM-5.1-FP8",
|
||||||
|
"api_keys": ["your-nearai-api-key"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**VolcEngine (Doubao)**
|
**VolcEngine (Doubao)**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -647,7 +660,9 @@ picoclaw agent -m "Hello"
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cron": {
|
"cron": {
|
||||||
"exec_timeout_minutes": 5
|
"exec_timeout_minutes": 5,
|
||||||
|
"allow_command": true,
|
||||||
|
"command_allowed_remotes": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"heartbeat": {
|
"heartbeat": {
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | **Français** | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | **Français** | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -57,6 +68,8 @@
|
|||||||
|
|
||||||
## 📢 Actualités
|
## 📢 Actualités
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw disponible sur AliExpress !** Vous pouvez désormais acheter le LicheeRV-Claw sur [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), ce qui facilite l'essai de PicoClaw sur du matériel RISC-V compact.
|
2026-05-11 🛒 **LicheeRV-Claw disponible sur AliExpress !** Vous pouvez désormais acheter le LicheeRV-Claw sur [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), ce qui facilite l'essai de PicoClaw sur du matériel RISC-V compact.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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** | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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** | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 Berita
|
## 📢 Berita
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Kini Anda dapat membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), sehingga lebih mudah mencoba PicoClaw di hardware RISC-V ringkas.
|
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Kini Anda dapat membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), sehingga lebih mudah mencoba PicoClaw di hardware RISC-V ringkas.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **Italiano** | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | **Italiano** | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 Novità
|
## 📢 Novità
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw disponibile su AliExpress!** Ora puoi acquistare LicheeRV-Claw su [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), rendendo più semplice provare PicoClaw su hardware RISC-V compatto.
|
2026-05-11 🛒 **LicheeRV-Claw disponibile su AliExpress!** Ora puoi acquistare LicheeRV-Claw su [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), rendendo più semplice provare PicoClaw su hardware RISC-V compatto.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | **日本語** | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | **日本語** | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 ニュース
|
## 📢 ニュース
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw が AliExpress で購入可能に!** [AliExpress](https://www.aliexpress.com/item/1005006519668532.html) から LicheeRV-Claw を購入できるようになり、コンパクトな RISC-V ハードウェアで PicoClaw を試しやすくなりました。
|
2026-05-11 🛒 **LicheeRV-Claw が AliExpress で購入可能に!** [AliExpress](https://www.aliexpress.com/item/1005006519668532.html) から LicheeRV-Claw を購入できるようになり、コンパクトな RISC-V ハードウェアで PicoClaw を試しやすくなりました。
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](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) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 뉴스
|
## 📢 뉴스
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw를 AliExpress에서 구매할 수 있습니다!** 이제 [AliExpress](https://www.aliexpress.com/item/1005006519668532.html)에서 LicheeRV-Claw를 구매해 소형 RISC-V 하드웨어에서 PicoClaw를 더 쉽게 사용해 볼 수 있습니다.
|
2026-05-11 🛒 **LicheeRV-Claw를 AliExpress에서 구매할 수 있습니다!** 이제 [AliExpress](https://www.aliexpress.com/item/1005006519668532.html)에서 LicheeRV-Claw를 구매해 소형 RISC-V 하드웨어에서 PicoClaw를 더 쉽게 사용해 볼 수 있습니다.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | **Malay** | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.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) | **Malay** | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 Berita
|
## 📢 Berita
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Anda kini boleh membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), menjadikannya lebih mudah untuk mencuba PicoClaw pada perkakasan RISC-V yang kompak.
|
2026-05-11 🛒 **LicheeRV-Claw tersedia di AliExpress!** Anda kini boleh membeli LicheeRV-Claw di [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), menjadikannya lebih mudah untuk mencuba PicoClaw pada perkakasan RISC-V yang kompak.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | **Português** | [Tiếng Việt](README.vi.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 Novidades
|
## 📢 Novidades
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw no AliExpress!** Agora você pode comprar o LicheeRV-Claw no [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), facilitando testar o PicoClaw em hardware RISC-V compacto.
|
2026-05-11 🛒 **LicheeRV-Claw no AliExpress!** Agora você pode comprar o LicheeRV-Claw no [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), facilitando testar o PicoClaw em hardware RISC-V compacto.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | **Tiếng Việt** | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
[中文](README.zh.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Português](README.pt-br.md) | **Tiếng Việt** | [Français](README.fr.md) | [Italiano](README.it.md) | [Bahasa Indonesia](README.id.md) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai">
|
||||||
|
<img src="../../assets/picopaw-banner-en.webp" alt="PicoPaw AI: Your AI Desktop Buddy" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI is now live at <a href="https://picopaw.ai">picopaw.ai</a>.</strong><br>
|
||||||
|
Create, preview, and share playful AI companions for the PicoClaw ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +67,8 @@
|
|||||||
|
|
||||||
## 📢 Tin tức
|
## 📢 Tin tức
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI is live!** Explore the new PicoPaw companion experience at [picopaw.ai](https://picopaw.ai), with animated AI pet previews and ecosystem updates for PicoClaw users.
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw đã có trên AliExpress!** Bạn hiện có thể mua LicheeRV-Claw trên [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), giúp việc thử PicoClaw trên phần cứng RISC-V nhỏ gọn dễ dàng hơn.
|
2026-05-11 🛒 **LicheeRV-Claw đã có trên AliExpress!** Bạn hiện có thể mua LicheeRV-Claw trên [AliExpress](https://www.aliexpress.com/item/1005006519668532.html), giúp việc thử PicoClaw trên phần cứng RISC-V nhỏ gọn dễ dàng hơn.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
|
|
||||||
**中文** | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
**中文** | [日本語](README.ja.md) | [한국어](README.ko.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) | [Malay](README.ms.md) | [English](../../README.md)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://picopaw.ai/zh">
|
||||||
|
<img src="../../assets/picopaw-banner-zh.webp" alt="PicoPaw AI:你的 AI 桌面伙伴" width="100%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>PicoPaw AI 已在 <a href="https://picopaw.ai/zh">picopaw.ai/zh</a> 上线。</strong><br>
|
||||||
|
创建、预览并分享面向 PicoClaw 生态的 AI 桌面伙伴。
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -49,13 +60,15 @@
|
|||||||
> **🚨 安全声明**
|
> **🚨 安全声明**
|
||||||
>
|
>
|
||||||
> - **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
|
> - **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**。
|
||||||
> - **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
|
> - **官方域名:** 唯一的 PicoClaw 官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
|
||||||
> - **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
|
> - **警惕:** 许多相似的 `.ai/.org/.com/.net/...` 后缀域名被第三方抢注。请只信任本 README 明确链接的域名。
|
||||||
> - **注意:** PicoClaw 正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在 1.0 正式版发布前,请不要将其部署到生产环境中。
|
> - **注意:** PicoClaw 正在初期的快速功能开发阶段,可能有尚未修复的网络安全问题,在 1.0 正式版发布前,请不要将其部署到生产环境中。
|
||||||
> - **注意:** PicoClaw 最近合并了大量 PR,近期版本可能内存占用较大 (10~20MB),我们将在功能较为收敛后进行资源占用优化。
|
> - **注意:** PicoClaw 最近合并了大量 PR,近期版本可能内存占用较大 (10~20MB),我们将在功能较为收敛后进行资源占用优化。
|
||||||
|
|
||||||
## 📢 新闻
|
## 📢 新闻
|
||||||
|
|
||||||
|
2026-06-11 🐾 **PicoPaw AI 上线!** 访问 [picopaw.ai/zh](https://picopaw.ai/zh),体验全新的 PicoPaw 桌面伙伴,预览会动的 AI 宠物,并关注 PicoClaw 生态更新。
|
||||||
|
|
||||||
2026-05-11 🛒 **LicheeRV-Claw 已上架淘宝!** 现在可以在 [淘宝](https://item.taobao.com/item.htm?abbucket=20&id=764939520376) 购买 LicheeRV-Claw,更方便地在小型 RISC-V 硬件上体验 PicoClaw。
|
2026-05-11 🛒 **LicheeRV-Claw 已上架淘宝!** 现在可以在 [淘宝](https://item.taobao.com/item.htm?abbucket=20&id=764939520376) 购买 LicheeRV-Claw,更方便地在小型 RISC-V 硬件上体验 PicoClaw。
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
+25
-5
@@ -43,7 +43,8 @@ the original prompt, delivery target, or command payload.
|
|||||||
Remote channel access is scoped to the current `channel/chat_id`: remote callers
|
Remote channel access is scoped to the current `channel/chat_id`: remote callers
|
||||||
can only list, get, or update jobs whose saved `payload.channel` and `payload.to`
|
can only list, get, or update jobs whose saved `payload.channel` and `payload.to`
|
||||||
match the current conversation. Command jobs include a shell command payload, so
|
match the current conversation. Command jobs include a shell command payload, so
|
||||||
they can only be listed, inspected, or updated from internal channels.
|
they can only be listed, inspected, or updated from internal channels or remote
|
||||||
|
channels allowed by `tools.cron.command_allowed_remotes`.
|
||||||
|
|
||||||
Example tool calls:
|
Example tool calls:
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ Example tool calls:
|
|||||||
(`at_seconds`, `every_seconds`, or `cron_expr`).
|
(`at_seconds`, `every_seconds`, or `cron_expr`).
|
||||||
Omit `command` to preserve it, set `command` to a non-empty string to replace
|
Omit `command` to preserve it, set `command` to a non-empty string to replace
|
||||||
it, or set `command` to `""` to clear it. Command updates require the same
|
it, or set `command` to `""` to clear it. Command updates require the same
|
||||||
internal channel and confirmation gates as command creation.
|
channel allowlist and confirmation gates as command creation.
|
||||||
|
|
||||||
## Execution Modes
|
## Execution Modes
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ If `tools.exec.enabled` is `false`:
|
|||||||
- new command jobs are rejected by the cron tool
|
- new command jobs are rejected by the cron tool
|
||||||
- existing command jobs publish a `command execution is disabled` error when they fire
|
- existing command jobs publish a `command execution is disabled` error when they fire
|
||||||
|
|
||||||
`tools.exec.allow_remote` is still enforced by the exec tool, but cron command scheduling already requires an internal channel when the job is created. In practice, reminder jobs can be scheduled from remote channels, while scheduled command jobs are limited to internal channels.
|
`tools.exec.allow_remote` is still enforced by the exec tool, but cron command scheduling has its own channel gate when the job is created. In practice, reminder jobs can be scheduled from remote channels, while scheduled command jobs are limited to internal channels and configured remote channels.
|
||||||
|
|
||||||
### `allow_command`
|
### `allow_command`
|
||||||
|
|
||||||
@@ -112,7 +113,23 @@ If `tools.exec.enabled` is `false`:
|
|||||||
|
|
||||||
This is not a hard disable switch. If you set `allow_command` to `false`, PicoClaw still allows a command job when the caller explicitly passes `command_confirm: true`.
|
This is not a hard disable switch. If you set `allow_command` to `false`, PicoClaw still allows a command job when the caller explicitly passes `command_confirm: true`.
|
||||||
|
|
||||||
Command jobs also require an internal channel. Non-command reminders do not have that restriction.
|
Command jobs also require either an internal channel or a remote channel allowed by `tools.cron.command_allowed_remotes`. Non-command reminders do not have that restriction.
|
||||||
|
|
||||||
|
### `command_allowed_remotes`
|
||||||
|
|
||||||
|
`tools.cron.command_allowed_remotes` defaults to an empty list. With the default empty list, remote channels cannot schedule command jobs.
|
||||||
|
|
||||||
|
Entries can be either a channel name or a channel plus chat id:
|
||||||
|
|
||||||
|
- `telegram` allows command jobs from any Telegram chat.
|
||||||
|
- `telegram:1234567890` allows command jobs only from that exact Telegram chat id.
|
||||||
|
- `*` allows command jobs from every non-empty channel.
|
||||||
|
|
||||||
|
Warning: `*` is potentially dangerous because any remote channel that can talk
|
||||||
|
to PicoClaw can schedule shell commands. Use it only when every enabled remote
|
||||||
|
channel and chat is trusted to request command execution.
|
||||||
|
|
||||||
|
This setting only controls the remote-channel gate. It does not bypass `tools.cron.allow_command`, `command_confirm`, `tools.exec.enabled`, or the exec tool's command safety checks.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -122,7 +139,10 @@ Example:
|
|||||||
"cron": {
|
"cron": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"exec_timeout_minutes": 5,
|
"exec_timeout_minutes": 5,
|
||||||
"allow_command": true
|
"allow_command": true,
|
||||||
|
"command_allowed_remotes": [
|
||||||
|
"telegram:1234567890"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"exec": {
|
"exec": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -312,9 +312,10 @@ as containers, VMs, or an approval flow around build-and-run commands.
|
|||||||
The cron tool is used for scheduling periodic tasks.
|
The cron tool is used for scheduling periodic tasks.
|
||||||
|
|
||||||
| Config | Type | Default | Description |
|
| Config | Type | Default | Description |
|
||||||
|------------------------|------|---------|------------------------------------------------|
|
|---------------------------|----------|---------|----------------------------------------------------------------|
|
||||||
| `enabled` | bool | true | Register the agent-facing cron tool |
|
| `enabled` | bool | true | Register the agent-facing cron tool |
|
||||||
| `allow_command` | bool | true | Allow command jobs without extra confirmation |
|
| `allow_command` | bool | true | Allow command jobs without extra confirmation |
|
||||||
|
| `command_allowed_remotes` | string[] | [] | Remote channels or `channel:chat_id` values allowed for command jobs; `*` allows every channel and is dangerous unless all remote channels are trusted |
|
||||||
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
|
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
|
||||||
|
|
||||||
For schedule types, execution modes (`deliver`, agent turn, and command jobs), persistence, and the current command-security gates, see [Scheduled Tasks and Cron Jobs](cron.md).
|
For schedule types, execution modes (`deliver`, agent turn, and command jobs), persistence, and the current command-security gates, see [Scheduled Tasks and Cron Jobs](cron.md).
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ module github.com/sipeed/picoclaw
|
|||||||
go 1.25.11
|
go 1.25.11
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/systray v1.12.1
|
fyne.io/systray v1.12.2
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
||||||
github.com/SevereCloud/vksdk/v3 v3.3.1
|
github.com/SevereCloud/vksdk/v3 v3.3.1
|
||||||
github.com/adhocore/gronx v1.20.0
|
github.com/adhocore/gronx v1.20.0
|
||||||
github.com/anthropics/anthropic-sdk-go v1.46.0
|
github.com/anthropics/anthropic-sdk-go v1.50.2
|
||||||
github.com/atc0005/go-teams-notify/v2 v2.14.0
|
github.com/atc0005/go-teams-notify/v2 v2.14.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.25
|
github.com/aws/aws-sdk-go-v2/config v1.32.25
|
||||||
@@ -26,12 +26,12 @@ require (
|
|||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62
|
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.9.4
|
github.com/larksuite/oapi-sdk-go/v3 v3.9.4
|
||||||
github.com/line/line-bot-sdk-go/v8 v8.20.0
|
github.com/line/line-bot-sdk-go/v8 v8.20.1
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/minio/selfupdate v0.6.0
|
github.com/minio/selfupdate v0.6.0
|
||||||
github.com/modelcontextprotocol/go-sdk v1.6.1
|
github.com/modelcontextprotocol/go-sdk v1.6.1
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/mymmrac/telego v1.9.0
|
github.com/mymmrac/telego v1.10.0
|
||||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
||||||
github.com/openai/openai-go/v3 v3.22.0
|
github.com/openai/openai-go/v3 v3.22.0
|
||||||
github.com/pion/rtp v1.10.2
|
github.com/pion/rtp v1.10.2
|
||||||
@@ -46,12 +46,12 @@ require (
|
|||||||
go.mau.fi/util v0.9.8
|
go.mau.fi/util v0.9.8
|
||||||
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4
|
go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.44.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mautrix v0.27.0
|
maunium.net/go/mautrix v0.27.0
|
||||||
modernc.org/sqlite v1.51.0
|
modernc.org/sqlite v1.53.0
|
||||||
rsc.io/qr v0.2.0
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ require (
|
|||||||
aead.dev/minisign v0.2.0 // indirect
|
aead.dev/minisign v0.2.0 // indirect
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
||||||
@@ -91,15 +91,15 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
github.com/invopop/jsonschema v0.14.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
@@ -111,16 +111,16 @@ require (
|
|||||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mau.fi/libsignal v0.2.1 // indirect
|
go.mau.fi/libsignal v0.2.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
modernc.org/libc v1.72.3 // indirect
|
modernc.org/libc v1.73.4 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.15.1 // indirect
|
github.com/bytedance/sonic v1.15.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/github/copilot-sdk/go v0.2.0
|
github.com/github/copilot-sdk/go v0.2.0
|
||||||
@@ -151,7 +151,7 @@ require (
|
|||||||
golang.org/x/crypto v0.51.0
|
golang.org/x/crypto v0.51.0
|
||||||
golang.org/x/net v0.55.0
|
golang.org/x/net v0.55.0
|
||||||
golang.org/x/sync v0.21.0
|
golang.org/x/sync v0.21.0
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.46.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/bwmarrin/discordgo => github.com/yeongaori/discordgo-fork v0.0.0-20260319072544-e8e546f5d532
|
replace github.com/bwmarrin/discordgo => github.com/yeongaori/discordgo-fork v0.0.0-20260319072544-e8e546f5d532
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
|||||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
|
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||||
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg=
|
github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg=
|
||||||
@@ -29,8 +29,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
|
|||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.46.0 h1:yl3n+el5ZfNgiCtQ7zQ7s/NXxB11YbrKXdc3uLPNWlU=
|
github.com/anthropics/anthropic-sdk-go v1.50.2 h1:K+YJWWzeN2h5MAbh9xeUWY8yAB2oOMp2xLLAODrVBXA=
|
||||||
github.com/anthropics/anthropic-sdk-go v1.46.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
|
github.com/anthropics/anthropic-sdk-go v1.50.2/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
|
||||||
github.com/atc0005/go-teams-notify/v2 v2.14.0 h1:7N+xw+COnYANLREaAveQ65rsNQ12nIZJED9nMLyscCo=
|
github.com/atc0005/go-teams-notify/v2 v2.14.0 h1:7N+xw+COnYANLREaAveQ65rsNQ12nIZJED9nMLyscCo=
|
||||||
github.com/atc0005/go-teams-notify/v2 v2.14.0/go.mod h1:EECsWM2b0Hvoz7O+QdlsvyN2KCUOFQCGj8bUBXv3A3Q=
|
github.com/atc0005/go-teams-notify/v2 v2.14.0/go.mod h1:EECsWM2b0Hvoz7O+QdlsvyN2KCUOFQCGj8bUBXv3A3Q=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
||||||
@@ -75,8 +75,8 @@ github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJ
|
|||||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
|
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||||
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
|
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
|
||||||
@@ -180,9 +180,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
|
|||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
|
||||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
|
||||||
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62 h1:nyUi7Wel3KlVSa5ArgX/snlizqfaxU48qtvXS/JK5GE=
|
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62 h1:nyUi7Wel3KlVSa5ArgX/snlizqfaxU48qtvXS/JK5GE=
|
||||||
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62/go.mod h1:vONkS+clG730HSKOw3nZVa22TjB21r6csKYzYt0a9zI=
|
github.com/kagisearch/kagi-openapi-golang v0.0.0-20260526215348-96575e864d62/go.mod h1:vONkS+clG730HSKOw3nZVa22TjB21r6csKYzYt0a9zI=
|
||||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
@@ -206,12 +205,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.9.4 h1:oMgcY7NBjJv1QXJqFAfcoN/TbScCkCuRZfbb1mCwZmI=
|
github.com/larksuite/oapi-sdk-go/v3 v3.9.4 h1:oMgcY7NBjJv1QXJqFAfcoN/TbScCkCuRZfbb1mCwZmI=
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.9.4/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
github.com/larksuite/oapi-sdk-go/v3 v3.9.4/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||||
github.com/line/line-bot-sdk-go/v8 v8.20.0 h1:Jv22DV3JuQ5qZvniqUbg504bJrVzffXs2CMpyoiuIZU=
|
github.com/line/line-bot-sdk-go/v8 v8.20.1 h1:OE7qwJbuZPz9MGOHHNI0E6rQNtIU7ENHyFXpvbVLwII=
|
||||||
github.com/line/line-bot-sdk-go/v8 v8.20.0/go.mod h1:QMXJwPka2ysSeVQKWXkBp8DzBFs+CFAXFNo75KJtWho=
|
github.com/line/line-bot-sdk-go/v8 v8.20.1/go.mod h1:QMXJwPka2ysSeVQKWXkBp8DzBFs+CFAXFNo75KJtWho=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -228,8 +225,8 @@ github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/u
|
|||||||
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/mymmrac/telego v1.9.0 h1:ZUJxZaPx/1IgRvVb5lXnUB8FgW5rNYfRe6Q2EJ4OJ+Y=
|
github.com/mymmrac/telego v1.10.0 h1:Upe0TqYyiK+yE5RFXXuQWVHGfLZnqvUfj4KZVjTcgWE=
|
||||||
github.com/mymmrac/telego v1.9.0/go.mod h1:tVEB7OqiOPx8elRk9+ETkwiDQrUhWSB2XmAKIY9KmWY=
|
github.com/mymmrac/telego v1.10.0/go.mod h1:LsQKDA6EwssPP9XkORPXwwOFUGIRf/Wf2Wb8y3YyJdE=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
@@ -244,6 +241,8 @@ github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv
|
|||||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
|
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
|
||||||
github.com/openai/openai-go/v3 v3.22.0 h1:6MEoNoV8sbjOVmXdvhmuX3BjVbVdcExbVyGixiyJ8ys=
|
github.com/openai/openai-go/v3 v3.22.0 h1:6MEoNoV8sbjOVmXdvhmuX3BjVbVdcExbVyGixiyJ8ys=
|
||||||
github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||||
|
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
|
||||||
|
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
@@ -324,8 +323,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
@@ -354,6 +351,8 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
|
|||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@@ -371,8 +370,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -422,16 +421,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -450,8 +449,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -483,20 +482,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
|
maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
|
||||||
maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
|
maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
|
||||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -505,8 +504,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
|||||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||||
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -172,14 +172,22 @@ func encodeImageToDataURL(localPath, mime string, info os.FileInfo, maxSize int)
|
|||||||
buf.WriteString(prefix)
|
buf.WriteString(prefix)
|
||||||
|
|
||||||
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||||
if _, err := io.Copy(encoder, f); err != nil {
|
_, copyErr := io.Copy(encoder, f)
|
||||||
|
closeErr := encoder.Close()
|
||||||
|
if copyErr != nil {
|
||||||
logger.WarnCF("agent", "Failed to encode media file", map[string]any{
|
logger.WarnCF("agent", "Failed to encode media file", map[string]any{
|
||||||
"path": localPath,
|
"path": localPath,
|
||||||
"error": err.Error(),
|
"error": copyErr.Error(),
|
||||||
|
})
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
logger.WarnCF("agent", "Failed to close base64 encoder", map[string]any{
|
||||||
|
"path": localPath,
|
||||||
|
"error": closeErr.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
encoder.Close()
|
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,9 @@ func (b *evolutionBridge) handleTurnEndAsync(meta EventMeta, payload TurnEndPayl
|
|||||||
FinalSuccessfulPath: append([]string(nil), payload.FinalSuccessfulPath...),
|
FinalSuccessfulPath: append([]string(nil), payload.FinalSuccessfulPath...),
|
||||||
SkillContextSnapshots: toEvolutionSkillContextSnapshots(payload.SkillContextSnapshots),
|
SkillContextSnapshots: toEvolutionSkillContextSnapshots(payload.SkillContextSnapshots),
|
||||||
}
|
}
|
||||||
|
if isEvolutionHeartbeatInput(input) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
b.rememberScheduledColdPathWorkspace(input.Workspace)
|
b.rememberScheduledColdPathWorkspace(input.Workspace)
|
||||||
|
|
||||||
b.closeMu.Lock()
|
b.closeMu.Lock()
|
||||||
@@ -228,6 +231,10 @@ func (b *evolutionBridge) handleTurnEndAsync(meta EventMeta, payload TurnEndPayl
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isEvolutionHeartbeatInput(input evolution.TurnCaseInput) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(input.SessionKey), "heartbeat")
|
||||||
|
}
|
||||||
|
|
||||||
func (b *evolutionBridge) subscribeRuntimeEvents(ch runtimeevents.EventChannel) error {
|
func (b *evolutionBridge) subscribeRuntimeEvents(ch runtimeevents.EventChannel) error {
|
||||||
if b == nil || ch == nil {
|
if b == nil || ch == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -513,6 +513,31 @@ func TestEvolutionBridge_DraftModeAutomaticallyRunsColdPathAndCreatesDraftFile(t
|
|||||||
waitForDrafts(t, filepath.Join(tmpDir, "state", "evolution", "skill-drafts.json"), 1)
|
waitForDrafts(t, filepath.Join(tmpDir, "state", "evolution", "skill-drafts.json"), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEvolutionBridge_DraftModeDoesNotRunColdPathForHeartbeat(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
seedReadyRule(t, tmpDir)
|
||||||
|
|
||||||
|
al := newEvolutionTestLoop(t, tmpDir, config.EvolutionConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Mode: "draft",
|
||||||
|
}, &simpleMockProvider{
|
||||||
|
response: `{"target_skill_name":"weather","draft_type":"shortcut","change_kind":"append","human_summary":"unexpected heartbeat draft","body_or_patch":"## Unexpected"}`,
|
||||||
|
})
|
||||||
|
defer al.Close()
|
||||||
|
|
||||||
|
resp, err := al.ProcessHeartbeat(context.Background(), "check heartbeat tasks", "telegram", "chat-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessHeartbeat failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp == "" {
|
||||||
|
t.Fatal("expected non-empty heartbeat response")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
assertNotExists(t, filepath.Join(tmpDir, "state", "evolution", "task-records.jsonl"))
|
||||||
|
assertNotExists(t, filepath.Join(tmpDir, "state", "evolution", "skill-drafts.json"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestEvolutionBridge_ScheduledModeDoesNotRunColdPathAfterTurn(t *testing.T) {
|
func TestEvolutionBridge_ScheduledModeDoesNotRunColdPathAfterTurn(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
seedReadyRule(t, tmpDir)
|
seedReadyRule(t, tmpDir)
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func SynthesizeAndStore(
|
|||||||
|
|
||||||
_, err = io.Copy(file, stream)
|
_, err = io.Copy(file, stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
file.Close()
|
_ = file.Close()
|
||||||
return "", fmt.Errorf("failed to write tts audio: %w", err)
|
return "", fmt.Errorf("failed to write tts audio: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -1279,6 +1280,16 @@ func (m *Manager) StartAll(ctx context.Context) error {
|
|||||||
for _, listener := range m.httpListeners {
|
for _, listener := range m.httpListeners {
|
||||||
ln := listener
|
ln := listener
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("channels", "HTTP server goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"addr": ln.Addr().String(),
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
|
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
|
||||||
"addr": ln.Addr().String(),
|
"addr": ln.Addr().String(),
|
||||||
})
|
})
|
||||||
@@ -1292,6 +1303,16 @@ func (m *Manager) StartAll(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("channels", "HTTP server goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"addr": m.httpServer.Addr,
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
|
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
|
||||||
"addr": m.httpServer.Addr,
|
"addr": m.httpServer.Addr,
|
||||||
})
|
})
|
||||||
@@ -1943,6 +1964,15 @@ func (m *Manager) Reload(ctx context.Context, cfg *config.Config) error {
|
|||||||
// Commit hashes only on full success.
|
// Commit hashes only on full success.
|
||||||
m.channelHashes = list
|
m.channelHashes = list
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("channels", "channel registration goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
for _, f := range deferFuncs {
|
for _, f := range deferFuncs {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
type OneBotChannel struct {
|
type OneBotChannel struct {
|
||||||
*channels.BaseChannel
|
*channels.BaseChannel
|
||||||
config *config.OneBotSettings
|
config *config.OneBotSettings
|
||||||
|
downloadFn func(urlStr, filename string) string
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -194,7 +195,7 @@ func (c *OneBotChannel) connect() error {
|
|||||||
|
|
||||||
conn, resp, err := dialer.Dial(c.config.WSUrl, header)
|
conn, resp, err := dialer.Dial(c.config.WSUrl, header)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -795,9 +796,7 @@ func (c *OneBotChannel) parseMessageSegments(
|
|||||||
} else if n, ok := data["name"].(string); ok && n != "" {
|
} else if n, ok := data["name"].(string); ok && n != "" {
|
||||||
filename = n
|
filename = n
|
||||||
}
|
}
|
||||||
localPath := utils.DownloadFile(url, filename, utils.DownloadOptions{
|
localPath := c.downloadInboundFile(url, filename)
|
||||||
LoggerPrefix: "onebot",
|
|
||||||
})
|
|
||||||
if localPath != "" {
|
if localPath != "" {
|
||||||
mediaRefs = append(mediaRefs, storeFile(localPath, filename))
|
mediaRefs = append(mediaRefs, storeFile(localPath, filename))
|
||||||
textParts = append(textParts, fmt.Sprintf("[%s]", segType))
|
textParts = append(textParts, fmt.Sprintf("[%s]", segType))
|
||||||
@@ -809,9 +808,7 @@ func (c *OneBotChannel) parseMessageSegments(
|
|||||||
if data != nil {
|
if data != nil {
|
||||||
url, _ := data["url"].(string)
|
url, _ := data["url"].(string)
|
||||||
if url != "" {
|
if url != "" {
|
||||||
localPath := utils.DownloadFile(url, "voice.amr", utils.DownloadOptions{
|
localPath := c.downloadInboundFile(url, "voice.amr")
|
||||||
LoggerPrefix: "onebot",
|
|
||||||
})
|
|
||||||
if localPath != "" {
|
if localPath != "" {
|
||||||
textParts = append(textParts, "[voice]")
|
textParts = append(textParts, "[voice]")
|
||||||
mediaRefs = append(mediaRefs, storeFile(localPath, "voice.amr"))
|
mediaRefs = append(mediaRefs, storeFile(localPath, "voice.amr"))
|
||||||
@@ -847,6 +844,16 @@ func (c *OneBotChannel) parseMessageSegments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) downloadInboundFile(urlStr, filename string) string {
|
||||||
|
if c.downloadFn != nil {
|
||||||
|
return c.downloadFn(urlStr, filename)
|
||||||
|
}
|
||||||
|
return utils.DownloadFile(urlStr, filename, utils.DownloadOptions{
|
||||||
|
LoggerPrefix: "onebot",
|
||||||
|
BlockPrivateTargets: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
||||||
switch raw.PostType {
|
switch raw.PostType {
|
||||||
case "message":
|
case "message":
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package onebot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMessageSegments_BlocksLoopbackInboundMediaURL(t *testing.T) {
|
||||||
|
ch := &OneBotChannel{}
|
||||||
|
store := media.NewFileMediaStore()
|
||||||
|
|
||||||
|
raw := json.RawMessage(`[
|
||||||
|
{"type":"text","data":{"text":"see attachment"}},
|
||||||
|
{"type":"image","data":{"url":"http://127.0.0.1:8080/evil.png","file":"evil.png"}}
|
||||||
|
]`)
|
||||||
|
|
||||||
|
result := ch.parseMessageSegments(raw, 0, store, "onebot:test:msg1")
|
||||||
|
|
||||||
|
if got := result.Text; got != "see attachment" {
|
||||||
|
t.Fatalf("Text = %q, want %q", got, "see attachment")
|
||||||
|
}
|
||||||
|
if len(result.Media) != 0 {
|
||||||
|
t.Fatalf("Media count = %d, want 0", len(result.Media))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessageSegments_BlocksInboundMediaRedirectToLoopback(t *testing.T) {
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("secret"))
|
||||||
|
}))
|
||||||
|
defer target.Close()
|
||||||
|
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.URL.String(), "http://example.com/evil.png"; got != want {
|
||||||
|
t.Fatalf("proxy request URL = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, target.URL, http.StatusFound)
|
||||||
|
}))
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
t.Setenv("HTTP_PROXY", proxy.URL)
|
||||||
|
t.Setenv("http_proxy", proxy.URL)
|
||||||
|
t.Setenv("HTTPS_PROXY", "")
|
||||||
|
t.Setenv("https_proxy", "")
|
||||||
|
t.Setenv("ALL_PROXY", "")
|
||||||
|
t.Setenv("all_proxy", "")
|
||||||
|
t.Setenv("NO_PROXY", "")
|
||||||
|
t.Setenv("no_proxy", "")
|
||||||
|
|
||||||
|
ch := &OneBotChannel{}
|
||||||
|
store := media.NewFileMediaStore()
|
||||||
|
|
||||||
|
raw := json.RawMessage(`[
|
||||||
|
{"type":"text","data":{"text":"see attachment"}},
|
||||||
|
{"type":"image","data":{"url":"http://example.com/evil.png","file":"evil.png"}}
|
||||||
|
]`)
|
||||||
|
|
||||||
|
result := ch.parseMessageSegments(raw, 0, store, "onebot:test:msg-redirect")
|
||||||
|
|
||||||
|
if got := result.Text; got != "see attachment" {
|
||||||
|
t.Fatalf("Text = %q, want %q", got, "see attachment")
|
||||||
|
}
|
||||||
|
if len(result.Media) != 0 {
|
||||||
|
t.Fatalf("Media count = %d, want 0", len(result.Media))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMessageSegments_StoresDownloadedMediaRef(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
localPath := filepath.Join(tmpDir, "image.png")
|
||||||
|
if err := os.WriteFile(localPath, []byte("fake-image"), 0o600); err != nil {
|
||||||
|
t.Fatalf("os.WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := &OneBotChannel{
|
||||||
|
downloadFn: func(urlStr, filename string) string {
|
||||||
|
if urlStr != "https://cdn.example.com/image.png" {
|
||||||
|
t.Fatalf("download url = %q, want %q", urlStr, "https://cdn.example.com/image.png")
|
||||||
|
}
|
||||||
|
if filename != "image.png" {
|
||||||
|
t.Fatalf("download filename = %q, want %q", filename, "image.png")
|
||||||
|
}
|
||||||
|
return localPath
|
||||||
|
},
|
||||||
|
}
|
||||||
|
store := media.NewFileMediaStore()
|
||||||
|
|
||||||
|
raw := json.RawMessage(`[
|
||||||
|
{"type":"text","data":{"text":"see attachment"}},
|
||||||
|
{"type":"image","data":{"url":"https://cdn.example.com/image.png","file":"image.png"}}
|
||||||
|
]`)
|
||||||
|
|
||||||
|
result := ch.parseMessageSegments(raw, 0, store, "onebot:test:msg2")
|
||||||
|
|
||||||
|
if got := result.Text; got != "see attachment[image]" {
|
||||||
|
t.Fatalf("Text = %q, want %q", got, "see attachment[image]")
|
||||||
|
}
|
||||||
|
if len(result.Media) != 1 {
|
||||||
|
t.Fatalf("Media count = %d, want 1", len(result.Media))
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(result.Media[0], "media://") {
|
||||||
|
t.Fatalf("media ref = %q, want media:// prefix", result.Media[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedPath, meta, err := store.ResolveWithMeta(result.Media[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveWithMeta() error = %v", err)
|
||||||
|
}
|
||||||
|
if resolvedPath != localPath {
|
||||||
|
t.Fatalf("resolved path = %q, want %q", resolvedPath, localPath)
|
||||||
|
}
|
||||||
|
if meta.Source != "onebot" {
|
||||||
|
t.Fatalf("meta.Source = %q, want %q", meta.Source, "onebot")
|
||||||
|
}
|
||||||
|
if meta.Filename != "image.png" {
|
||||||
|
t.Fatalf("meta.Filename = %q, want %q", meta.Filename, "image.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ func (c *PicoClientChannel) dial() error {
|
|||||||
|
|
||||||
ws, resp, err := websocket.DefaultDialer.DialContext(c.ctx, c.config.URL, header)
|
ws, resp, err := websocket.DefaultDialer.DialContext(c.ctx, c.config.URL, header)
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1185,7 +1185,7 @@ func (c *TelegramChannel) handleMessages(ctx context.Context, messages []*telego
|
|||||||
|
|
||||||
inboundCtx := bus.InboundContext{
|
inboundCtx := bus.InboundContext{
|
||||||
Channel: c.Name(),
|
Channel: c.Name(),
|
||||||
ChatID: fmt.Sprintf("%d", chatID),
|
ChatID: compositeChatID,
|
||||||
ChatType: peerKind,
|
ChatType: peerKind,
|
||||||
SenderID: platformID,
|
SenderID: platformID,
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -213,6 +215,33 @@ func TestSendMedia_ImageFallbacksToDocumentOnInvalidDimensions(t *testing.T) {
|
|||||||
assert.Equal(t, "caption", constructor.calls[1].Parameters["caption"])
|
assert.Equal(t, "caption", constructor.calls[1].Parameters["caption"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloadFileWithInfo_AllowsLocalConfiguredBaseURL(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.URL.Path, "/file/bot"+testToken+"/photos/image"; got != want {
|
||||||
|
t.Fatalf("request path = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("telegram-local-bot-api"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
ch, err := NewTelegramChannel(
|
||||||
|
&config.Channel{Type: config.ChannelTelegram, Enabled: true},
|
||||||
|
&config.TelegramSettings{
|
||||||
|
Token: *config.NewSecureString(testToken),
|
||||||
|
BaseURL: server.URL,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
path := ch.downloadFileWithInfo(&telego.File{FilePath: "photos/image"}, "")
|
||||||
|
if path == "" {
|
||||||
|
t.Fatal("expected local base_url download to succeed")
|
||||||
|
}
|
||||||
|
defer os.Remove(path)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSendMedia_ImageNonDimensionErrorDoesNotFallback(t *testing.T) {
|
func TestSendMedia_ImageNonDimensionErrorDoesNotFallback(t *testing.T) {
|
||||||
constructor := &multipartRecordingConstructor{}
|
constructor := &multipartRecordingConstructor{}
|
||||||
caller := &stubCaller{
|
caller := &stubCaller{
|
||||||
@@ -1212,8 +1241,9 @@ func TestHandleMessage_ForumTopic_SetsMetadata(t *testing.T) {
|
|||||||
inbound, ok := <-messageBus.InboundChan()
|
inbound, ok := <-messageBus.InboundChan()
|
||||||
require.True(t, ok, "expected inbound message")
|
require.True(t, ok, "expected inbound message")
|
||||||
|
|
||||||
// ChatID remains the parent chat; TopicID isolates the sub-conversation.
|
// ChatID includes the thread ID for forum topics so outbound
|
||||||
assert.Equal(t, "-1001234567890", inbound.ChatID)
|
// delivery resolves the correct topic without relying solely on TopicID fallback.
|
||||||
|
assert.Equal(t, "-1001234567890/42", inbound.ChatID)
|
||||||
assert.Equal(t, "group", inbound.Context.ChatType)
|
assert.Equal(t, "group", inbound.Context.ChatType)
|
||||||
assert.Equal(t, "42", inbound.Context.TopicID)
|
assert.Equal(t, "42", inbound.Context.TopicID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (c *WhatsAppChannel) Start(ctx context.Context) error {
|
|||||||
|
|
||||||
conn, resp, err := dialer.Dial(c.url, nil)
|
conn, resp, err := dialer.Dial(c.url, nil)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.cancel()
|
c.cancel()
|
||||||
|
|||||||
@@ -1028,9 +1028,11 @@ type WebToolsConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CronToolsConfig struct {
|
type CronToolsConfig struct {
|
||||||
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_CRON_"`
|
ToolConfig `envPrefix:"PICOCLAW_TOOLS_CRON_"`
|
||||||
ExecTimeoutMinutes int ` json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
|
// 0 means no timeout.
|
||||||
AllowCommand bool ` json:"allow_command" env:"PICOCLAW_TOOLS_CRON_ALLOW_COMMAND"`
|
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"`
|
||||||
|
AllowCommand bool `json:"allow_command" env:"PICOCLAW_TOOLS_CRON_ALLOW_COMMAND"`
|
||||||
|
CommandAllowedRemotes []string `json:"command_allowed_remotes" env:"PICOCLAW_TOOLS_CRON_COMMAND_ALLOWED_REMOTES"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecConfig struct {
|
type ExecConfig struct {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -655,6 +656,8 @@ func filterSecureFields(r RawNode, secureFields map[string]struct{}) RawNode {
|
|||||||
// channelSettingsFactory maps channel type to a zero-value prototype of the
|
// channelSettingsFactory maps channel type to a zero-value prototype of the
|
||||||
// corresponding Settings struct. InitChannelList uses reflect.New to create
|
// corresponding Settings struct. InitChannelList uses reflect.New to create
|
||||||
// fresh instances, avoiding repeated closure boilerplate.
|
// fresh instances, avoiding repeated closure boilerplate.
|
||||||
|
var channelSettingsMu sync.RWMutex
|
||||||
|
|
||||||
var channelSettingsFactory = map[string]any{
|
var channelSettingsFactory = map[string]any{
|
||||||
ChannelPico: (PicoSettings{}),
|
ChannelPico: (PicoSettings{}),
|
||||||
ChannelPicoClient: (PicoClientSettings{}),
|
ChannelPicoClient: (PicoClientSettings{}),
|
||||||
@@ -679,10 +682,24 @@ var channelSettingsFactory = map[string]any{
|
|||||||
ChannelSlackWebHook: (SlackWebhookSettings{}),
|
ChannelSlackWebHook: (SlackWebhookSettings{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterChannelSettings registers a settings struct prototype for a custom
|
||||||
|
// channel type. External packages (out-of-tree channels registered via
|
||||||
|
// channels.RegisterFactory) call this from an init() so their channel type
|
||||||
|
// passes config validation (isValidChannelType) and its settings block decodes
|
||||||
|
// into the right struct (newChannelSettings). The prototype must be a struct
|
||||||
|
// value, e.g. RegisterChannelSettings("my_channel", MyChannelSettings{}).
|
||||||
|
func RegisterChannelSettings(channelType string, prototype any) {
|
||||||
|
channelSettingsMu.Lock()
|
||||||
|
defer channelSettingsMu.Unlock()
|
||||||
|
channelSettingsFactory[channelType] = prototype
|
||||||
|
}
|
||||||
|
|
||||||
// newChannelSettings creates a fresh zero-value pointer for the given channel type.
|
// newChannelSettings creates a fresh zero-value pointer for the given channel type.
|
||||||
// Returns nil if the type is not registered.
|
// Returns nil if the type is not registered.
|
||||||
func newChannelSettings(channelType string) any {
|
func newChannelSettings(channelType string) any {
|
||||||
|
channelSettingsMu.RLock()
|
||||||
proto, ok := channelSettingsFactory[channelType]
|
proto, ok := channelSettingsFactory[channelType]
|
||||||
|
channelSettingsMu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -691,7 +708,9 @@ func newChannelSettings(channelType string) any {
|
|||||||
|
|
||||||
// isValidChannelType returns true if the channel type is a known, registered type.
|
// isValidChannelType returns true if the channel type is a known, registered type.
|
||||||
func isValidChannelType(channelType string) bool {
|
func isValidChannelType(channelType string) bool {
|
||||||
|
channelSettingsMu.RLock()
|
||||||
_, ok := channelSettingsFactory[channelType]
|
_, ok := channelSettingsFactory[channelType]
|
||||||
|
channelSettingsMu.RUnlock()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1540,6 +1540,16 @@ func TestDefaultConfig_CronAllowCommandEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultConfig_CronCommandAllowedRemotesEmpty(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if len(cfg.Tools.Cron.CommandAllowedRemotes) != 0 {
|
||||||
|
t.Fatalf(
|
||||||
|
"DefaultConfig().Tools.Cron.CommandAllowedRemotes = %#v, want empty",
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefaultConfig_HooksDefaults(t *testing.T) {
|
func TestDefaultConfig_HooksDefaults(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
if !cfg.Hooks.Enabled {
|
if !cfg.Hooks.Enabled {
|
||||||
@@ -1600,6 +1610,32 @@ func TestLoadConfig_CronAllowCommandDefaultsTrueWhenUnset(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_CronCommandAllowedRemotes(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(
|
||||||
|
configPath,
|
||||||
|
[]byte(`{"version":1,"tools":{"cron":{"command_allowed_remotes":["telegram:1234567890","discord"]}}}`),
|
||||||
|
0o600,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error: %v", err)
|
||||||
|
}
|
||||||
|
want := []string{"telegram:1234567890", "discord"}
|
||||||
|
if len(cfg.Tools.Cron.CommandAllowedRemotes) != len(want) {
|
||||||
|
t.Fatalf("CommandAllowedRemotes = %#v, want %#v", cfg.Tools.Cron.CommandAllowedRemotes, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if cfg.Tools.Cron.CommandAllowedRemotes[i] != want[i] {
|
||||||
|
t.Fatalf("CommandAllowedRemotes = %#v, want %#v", cfg.Tools.Cron.CommandAllowedRemotes, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadConfig_WebToolsProxy(t *testing.T) {
|
func TestLoadConfig_WebToolsProxy(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "config.json")
|
configPath := filepath.Join(tmpDir, "config.json")
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ func DefaultConfig() *Config {
|
|||||||
APIBase: "https://api.venice.ai/api/v1",
|
APIBase: "https://api.venice.ai/api/v1",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// NEAR AI Cloud TEE inference - https://near.ai
|
||||||
|
{
|
||||||
|
ModelName: "nearai-glm",
|
||||||
|
Provider: "nearai",
|
||||||
|
Model: "zai-org/GLM-5.1-FP8",
|
||||||
|
APIBase: "https://cloud-api.near.ai/v1",
|
||||||
|
},
|
||||||
|
|
||||||
// Google Gemini - https://ai.google.dev/
|
// Google Gemini - https://ai.google.dev/
|
||||||
{
|
{
|
||||||
ModelName: "gemini-2.0-flash",
|
ModelName: "gemini-2.0-flash",
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// customChannelSettings stands in for an out-of-tree channel's settings struct.
|
||||||
|
type customChannelSettings struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterChannelSettings verifies the public registration hook makes a
|
||||||
|
// previously-unknown channel type valid and decodable — the behavior out-of-tree
|
||||||
|
// channels rely on (they call RegisterChannelSettings from init()).
|
||||||
|
func TestRegisterChannelSettings(t *testing.T) {
|
||||||
|
const typ = "custom_test_channel"
|
||||||
|
|
||||||
|
assert.False(t, isValidChannelType(typ), "type should be unknown before registration")
|
||||||
|
|
||||||
|
RegisterChannelSettings(typ, customChannelSettings{})
|
||||||
|
|
||||||
|
assert.True(t, isValidChannelType(typ), "type should be valid after registration")
|
||||||
|
|
||||||
|
got := newChannelSettings(typ)
|
||||||
|
_, ok := got.(*customChannelSettings)
|
||||||
|
assert.Truef(t, ok, "newChannelSettings(%q) = %T, want *customChannelSettings", typ, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterChannelSettings_InitChannelList verifies that a config carrying a
|
||||||
|
// registered out-of-tree channel type passes InitChannelList and decodes its
|
||||||
|
// settings — the full path that previously errored with "unknown type".
|
||||||
|
func TestRegisterChannelSettings_InitChannelList(t *testing.T) {
|
||||||
|
const typ = "custom_initlist_channel"
|
||||||
|
RegisterChannelSettings(typ, customChannelSettings{})
|
||||||
|
|
||||||
|
channels := ChannelsConfig{
|
||||||
|
"mychan": {
|
||||||
|
Type: typ,
|
||||||
|
Enabled: true,
|
||||||
|
Settings: RawNode(`{"token":"secret-123"}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, InitChannelList(channels))
|
||||||
|
|
||||||
|
decoded, err := channels["mychan"].GetDecoded()
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg, ok := decoded.(*customChannelSettings)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "secret-123", cfg.Token)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -227,6 +228,11 @@ func (s *eventSubscription) dispatch(ctx context.Context, evt Event) {
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("events: subscriber %q goroutine panic recovered: %v\n%s", s.name, r, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
s.handle(ctx, evt)
|
s.handle(ctx, evt)
|
||||||
}()
|
}()
|
||||||
case Keyed:
|
case Keyed:
|
||||||
@@ -253,6 +259,15 @@ func (s *eventSubscription) handle(ctx context.Context, evt Event) {
|
|||||||
|
|
||||||
done := make(chan handlerResult, 1)
|
done := make(chan handlerResult, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf(
|
||||||
|
"events: subscriber %q timeout-handler goroutine panic recovered: %v\n%s",
|
||||||
|
s.name, r, debug.Stack(),
|
||||||
|
)
|
||||||
|
done <- handlerResult{panicked: true}
|
||||||
|
}
|
||||||
|
}()
|
||||||
done <- s.invokeHandler(ctx, evt)
|
done <- s.invokeHandler(ctx, evt)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -302,6 +317,14 @@ func (s *eventSubscription) watchContext(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf(
|
||||||
|
"events: subscriber %q watchContext goroutine panic recovered: %v\n%s",
|
||||||
|
s.name, r, debug.Stack(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
_ = s.Close()
|
_ = s.Close()
|
||||||
|
|||||||
+11
-1
@@ -558,10 +558,20 @@ func isInvalidJSON(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lockStoreFile(path string) func() {
|
func lockStoreFile(path string) func() {
|
||||||
|
for {
|
||||||
actual, _ := storeFileLocks.LoadOrStore(path, &sync.Mutex{})
|
actual, _ := storeFileLocks.LoadOrStore(path, &sync.Mutex{})
|
||||||
mu := actual.(*sync.Mutex)
|
mu, ok := actual.(*sync.Mutex)
|
||||||
|
if !ok || mu == nil {
|
||||||
|
// Corrupted entry (wrong type or nil *sync.Mutex).
|
||||||
|
// Atomically swap in a fresh mutex via CompareAndSwap.
|
||||||
|
// If CAS fails, another goroutine already replaced it —
|
||||||
|
// just retry the loop to pick up the valid entry.
|
||||||
|
storeFileLocks.CompareAndSwap(path, actual, &sync.Mutex{})
|
||||||
|
continue
|
||||||
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
return mu.Unlock
|
return mu.Unlock
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) profilePath(workspaceID, skillName string) (string, error) {
|
func (s *Store) profilePath(workspaceID, skillName string) (string, error) {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func WriteFileAtomic(path string, data []byte, perm os.FileMode) error {
|
|||||||
// This prevents the renamed file from disappearing after a crash
|
// This prevents the renamed file from disappearing after a crash
|
||||||
if dirFile, err := os.Open(dir); err == nil {
|
if dirFile, err := os.Open(dir); err == nil {
|
||||||
_ = dirFile.Sync()
|
_ = dirFile.Sync()
|
||||||
dirFile.Close()
|
_ = dirFile.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success: skip cleanup (file was renamed, no temp to remove)
|
// Success: skip cleanup (file was renamed, no temp to remove)
|
||||||
|
|||||||
+52
-11
@@ -205,18 +205,11 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
|
|||||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayStart, startedAt, nil)
|
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayStart, startedAt, nil)
|
||||||
|
|
||||||
fmt.Println("\n📦 Agent Status:")
|
fmt.Println("\n📦 Agent Status:")
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupStatus := collectGatewayStartupStatus(agentLoop.GetStartupInfo())
|
||||||
toolsInfo := startupInfo["tools"].(map[string]any)
|
fmt.Printf(" • Tools: %d loaded\n", startupStatus.toolsCount)
|
||||||
skillsInfo := startupInfo["skills"].(map[string]any)
|
fmt.Printf(" • Skills: %d/%d available\n", startupStatus.skillsAvailable, startupStatus.skillsTotal)
|
||||||
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
|
|
||||||
fmt.Printf(" • Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"])
|
|
||||||
|
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
logger.InfoCF("agent", "Agent initialized", startupStatus.logFields)
|
||||||
map[string]any{
|
|
||||||
"tools_count": toolsInfo["count"],
|
|
||||||
"skills_total": skillsInfo["total"],
|
|
||||||
"skills_available": skillsInfo["available"],
|
|
||||||
})
|
|
||||||
|
|
||||||
runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult)
|
runningServices, err := setupAndStartServices(cfg, agentLoop, msgBus, pidData.Token, listenResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -315,6 +308,54 @@ func preCheckConfig(cfg *config.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gatewayStartupStatus struct {
|
||||||
|
toolsCount int
|
||||||
|
skillsAvailable int
|
||||||
|
skillsTotal int
|
||||||
|
logFields map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectGatewayStartupStatus(startupInfo map[string]any) gatewayStartupStatus {
|
||||||
|
status := gatewayStartupStatus{logFields: map[string]any{}}
|
||||||
|
|
||||||
|
if toolsInfo, ok := startupInfo["tools"].(map[string]any); ok {
|
||||||
|
if count, ok := startupInfoInt(toolsInfo["count"]); ok {
|
||||||
|
status.toolsCount = count
|
||||||
|
status.logFields["tools_count"] = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if skillsInfo, ok := startupInfo["skills"].(map[string]any); ok {
|
||||||
|
if total, ok := startupInfoInt(skillsInfo["total"]); ok {
|
||||||
|
status.skillsTotal = total
|
||||||
|
status.logFields["skills_total"] = total
|
||||||
|
}
|
||||||
|
if available, ok := startupInfoInt(skillsInfo["available"]); ok {
|
||||||
|
status.skillsAvailable = available
|
||||||
|
status.logFields["skills_available"] = available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func startupInfoInt(value any) (int, bool) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int:
|
||||||
|
return v, true
|
||||||
|
case int32:
|
||||||
|
return int(v), true
|
||||||
|
case int64:
|
||||||
|
return int(v), true
|
||||||
|
case float32:
|
||||||
|
return int(v), true
|
||||||
|
case float64:
|
||||||
|
return int(v), true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func executeReload(
|
func executeReload(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
agentLoop *agent.AgentLoop,
|
agentLoop *agent.AgentLoop,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -113,6 +114,93 @@ func TestGatewayRunStartupFailureHelper(t *testing.T) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCollectGatewayStartupStatusHandlesMalformedInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
startupInfo map[string]any
|
||||||
|
wantToolsCount int
|
||||||
|
wantSkillsAvailable int
|
||||||
|
wantSkillsTotal int
|
||||||
|
wantLogFields map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing info",
|
||||||
|
startupInfo: map[string]any{},
|
||||||
|
wantLogFields: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong map shapes",
|
||||||
|
startupInfo: map[string]any{
|
||||||
|
"tools": "unexpected",
|
||||||
|
"skills": []any{"unexpected"},
|
||||||
|
},
|
||||||
|
wantLogFields: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid startup info",
|
||||||
|
startupInfo: map[string]any{
|
||||||
|
"tools": map[string]any{
|
||||||
|
"count": 3,
|
||||||
|
},
|
||||||
|
"skills": map[string]any{
|
||||||
|
"available": 2,
|
||||||
|
"total": 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantToolsCount: 3,
|
||||||
|
wantSkillsAvailable: 2,
|
||||||
|
wantSkillsTotal: 5,
|
||||||
|
wantLogFields: map[string]any{
|
||||||
|
"tools_count": 3,
|
||||||
|
"skills_available": 2,
|
||||||
|
"skills_total": 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json number startup info",
|
||||||
|
startupInfo: map[string]any{
|
||||||
|
"tools": map[string]any{
|
||||||
|
"count": float64(4),
|
||||||
|
},
|
||||||
|
"skills": map[string]any{
|
||||||
|
"available": float64(1),
|
||||||
|
"total": float64(6),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantToolsCount: 4,
|
||||||
|
wantSkillsAvailable: 1,
|
||||||
|
wantSkillsTotal: 6,
|
||||||
|
wantLogFields: map[string]any{
|
||||||
|
"tools_count": 4,
|
||||||
|
"skills_available": 1,
|
||||||
|
"skills_total": 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := collectGatewayStartupStatus(tt.startupInfo)
|
||||||
|
if got.toolsCount != tt.wantToolsCount {
|
||||||
|
t.Fatalf("toolsCount = %d, want %d", got.toolsCount, tt.wantToolsCount)
|
||||||
|
}
|
||||||
|
if got.skillsAvailable != tt.wantSkillsAvailable {
|
||||||
|
t.Fatalf("skillsAvailable = %d, want %d", got.skillsAvailable, tt.wantSkillsAvailable)
|
||||||
|
}
|
||||||
|
if got.skillsTotal != tt.wantSkillsTotal {
|
||||||
|
t.Fatalf("skillsTotal = %d, want %d", got.skillsTotal, tt.wantSkillsTotal)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got.logFields, tt.wantLogFields) {
|
||||||
|
t.Fatalf("logFields = %#v, want %#v", got.logFields, tt.wantLogFields)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishGatewayEvent(t *testing.T) {
|
func TestPublishGatewayEvent(t *testing.T) {
|
||||||
eventBus := runtimeevents.NewBus()
|
eventBus := runtimeevents.NewBus()
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (s *Server) reloadHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "reload triggered"})
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "reload triggered"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -176,7 +176,7 @@ func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
PID: os.Getpid(),
|
PID: os.Getpid(),
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(resp)
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -190,7 +190,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if !ready {
|
if !ready {
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
json.NewEncoder(w).Encode(StatusResponse{
|
_ = json.NewEncoder(w).Encode(StatusResponse{
|
||||||
Status: "not ready",
|
Status: "not ready",
|
||||||
Checks: checks,
|
Checks: checks,
|
||||||
})
|
})
|
||||||
@@ -200,7 +200,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, check := range checks {
|
for _, check := range checks {
|
||||||
if check.Status == "fail" {
|
if check.Status == "fail" {
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
json.NewEncoder(w).Encode(StatusResponse{
|
_ = json.NewEncoder(w).Encode(StatusResponse{
|
||||||
Status: "not ready",
|
Status: "not ready",
|
||||||
Checks: checks,
|
Checks: checks,
|
||||||
})
|
})
|
||||||
@@ -210,7 +210,7 @@ func (s *Server) readyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
uptime := time.Since(s.startTime)
|
uptime := time.Since(s.startTime)
|
||||||
json.NewEncoder(w).Encode(StatusResponse{
|
_ = json.NewEncoder(w).Encode(StatusResponse{
|
||||||
Status: "ready",
|
Status: "ready",
|
||||||
Uptime: uptime.String(),
|
Uptime: uptime.String(),
|
||||||
Checks: checks,
|
Checks: checks,
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||||||
return finalizeProviderFromConfig(provider, modelID, cfg)
|
return finalizeProviderFromConfig(provider, modelID, cfg)
|
||||||
|
|
||||||
case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice",
|
case "litellm", "lmstudio", "gpt4free", "openrouter", "groq", "zhipu", "nvidia", "venice",
|
||||||
"ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
|
"nearai", "ollama", "moonshot", "shengsuanyun", "siliconflow", "deepseek", "cerebras",
|
||||||
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
|
"vivgrid", "volcengine", "vllm", "qwen-portal", "qwen-intl", "qwen-us", "mistral",
|
||||||
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
|
"avian", "longcat", "modelscope", "novita", "alibaba-coding", "zai", "mimo":
|
||||||
// All other OpenAI-compatible HTTP providers
|
// All other OpenAI-compatible HTTP providers
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ func TestCreateProviderFromConfig_DefaultAPIBase(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"openai", "openai"},
|
{"openai", "openai"},
|
||||||
{"venice", "venice"},
|
{"venice", "venice"},
|
||||||
|
{"nearai", "nearai"},
|
||||||
{"groq", "groq"},
|
{"groq", "groq"},
|
||||||
{"novita", "novita"},
|
{"novita", "novita"},
|
||||||
{"openrouter", "openrouter"},
|
{"openrouter", "openrouter"},
|
||||||
@@ -288,6 +289,15 @@ func TestGetDefaultAPIBase_Venice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetDefaultAPIBase_NearAI(t *testing.T) {
|
||||||
|
if got := getDefaultAPIBase("nearai"); got != "https://cloud-api.near.ai/v1" {
|
||||||
|
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "nearai", got, "https://cloud-api.near.ai/v1")
|
||||||
|
}
|
||||||
|
if got := getDefaultAPIBase("near-ai"); got != "https://cloud-api.near.ai/v1" {
|
||||||
|
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "near-ai", got, "https://cloud-api.near.ai/v1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) {
|
func TestGetDefaultAPIBase_SiliconFlow(t *testing.T) {
|
||||||
if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" {
|
if got := getDefaultAPIBase("siliconflow"); got != "https://api.siliconflow.cn/v1" {
|
||||||
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1")
|
t.Fatalf("getDefaultAPIBase(%q) = %q, want %q", "siliconflow", got, "https://api.siliconflow.cn/v1")
|
||||||
@@ -525,6 +535,28 @@ func TestCreateProviderFromConfig_Venice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateProviderFromConfig_NearAI(t *testing.T) {
|
||||||
|
cfg := &config.ModelConfig{
|
||||||
|
ModelName: "test-nearai",
|
||||||
|
Model: "nearai/zai-org/GLM-5.1-FP8",
|
||||||
|
}
|
||||||
|
cfg.SetAPIKey("test-key")
|
||||||
|
|
||||||
|
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if provider == nil {
|
||||||
|
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||||
|
}
|
||||||
|
if modelID != "zai-org/GLM-5.1-FP8" {
|
||||||
|
t.Errorf("modelID = %q, want %q", modelID, "zai-org/GLM-5.1-FP8")
|
||||||
|
}
|
||||||
|
if _, ok := provider.(*HTTPProvider); !ok {
|
||||||
|
t.Fatalf("expected *HTTPProvider, got %T", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) {
|
func TestCreateProviderFromConfig_SiliconFlow(t *testing.T) {
|
||||||
cfg := &config.ModelConfig{
|
cfg := &config.ModelConfig{
|
||||||
ModelName: "test-siliconflow",
|
ModelName: "test-siliconflow",
|
||||||
@@ -1076,6 +1108,22 @@ func TestModelProviderOptions(t *testing.T) {
|
|||||||
"https://api.siliconflow.cn/v1",
|
"https://api.siliconflow.cn/v1",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if option, ok := seen["nearai"]; !ok {
|
||||||
|
t.Fatal("nearai option missing")
|
||||||
|
} else {
|
||||||
|
if option.DisplayName != "NEAR AI Cloud" {
|
||||||
|
t.Fatalf("nearai display_name = %q, want %q", option.DisplayName, "NEAR AI Cloud")
|
||||||
|
}
|
||||||
|
if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
|
||||||
|
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
|
||||||
|
}
|
||||||
|
if !option.SupportsFetch {
|
||||||
|
t.Fatal("nearai should support upstream model listing")
|
||||||
|
}
|
||||||
|
if len(option.CommonModels) == 0 {
|
||||||
|
t.Fatal("nearai common_models should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
if option, ok := seen["anthropic"]; !ok {
|
if option, ok := seen["anthropic"]; !ok {
|
||||||
t.Fatal("anthropic option missing")
|
t.Fatal("anthropic option missing")
|
||||||
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {
|
} else if option.DefaultAPIBase != "https://api.anthropic.com/v1" {
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ func (p *GeminiProvider) buildRequestBody(
|
|||||||
}
|
}
|
||||||
if thoughtSignature != "" {
|
if thoughtSignature != "" {
|
||||||
part.ThoughtSignature = thoughtSignature
|
part.ThoughtSignature = thoughtSignature
|
||||||
|
part.ThoughtSignatureSnake = thoughtSignature
|
||||||
}
|
}
|
||||||
content.Parts = append(content.Parts, part)
|
content.Parts = append(content.Parts, part)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ func TestGeminiProvider_ChatStreamReturnsErrorOnInvalidDataFrame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGeminiProvider_BuildRequestBody_UsesCamelCaseThoughtSignatureOnly(t *testing.T) {
|
func TestGeminiProvider_BuildRequestBody_SetsBothThoughtSignatureFormats(t *testing.T) {
|
||||||
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
||||||
|
|
||||||
body := provider.buildRequestBody(
|
body := provider.buildRequestBody(
|
||||||
@@ -484,8 +484,12 @@ func TestGeminiProvider_BuildRequestBody_UsesCamelCaseThoughtSignatureOnly(t *te
|
|||||||
if !strings.Contains(jsonBody, `"thoughtSignature":"sig-1"`) {
|
if !strings.Contains(jsonBody, `"thoughtSignature":"sig-1"`) {
|
||||||
t.Fatalf("request body = %s, expected camelCase thoughtSignature", jsonBody)
|
t.Fatalf("request body = %s, expected camelCase thoughtSignature", jsonBody)
|
||||||
}
|
}
|
||||||
if strings.Contains(jsonBody, `"thought_signature"`) {
|
if !strings.Contains(jsonBody, `"thought_signature":"sig-1"`) {
|
||||||
t.Fatalf("request body = %s, unexpected snake_case thought_signature", jsonBody)
|
t.Fatalf(
|
||||||
|
"request body = %s, expected snake_case thought_signature for "+
|
||||||
|
"Gemini 3.5 Flash Agentic compatibility",
|
||||||
|
jsonBody,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const (
|
|||||||
|
|
||||||
var stripModelPrefixProviders = map[string]struct{}{
|
var stripModelPrefixProviders = map[string]struct{}{
|
||||||
"litellm": {},
|
"litellm": {},
|
||||||
|
"nearai": {},
|
||||||
"venice": {},
|
"venice": {},
|
||||||
"moonshot": {},
|
"moonshot": {},
|
||||||
"nvidia": {},
|
"nvidia": {},
|
||||||
@@ -153,7 +154,21 @@ func (p *Provider) buildRequestBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// When fallback uses a different provider (e.g. DeepSeek), that provider must not inject web_search_preview.
|
// When fallback uses a different provider (e.g. DeepSeek), that provider must not inject web_search_preview.
|
||||||
nativeSearch, _ := options["native_search"].(bool)
|
nativeSearch, ok := options["native_search"].(bool)
|
||||||
|
if !ok {
|
||||||
|
// If the option is present but not a bool, log a warning and
|
||||||
|
// treat it as false — web_search_preview must not be injected
|
||||||
|
// when the caller cannot express a well-typed intent.
|
||||||
|
if _, present := options["native_search"]; present {
|
||||||
|
logger.WarnCF(
|
||||||
|
"provider.openai_compat",
|
||||||
|
"native_search option has unexpected type, ignoring",
|
||||||
|
map[string]any{
|
||||||
|
"type": fmt.Sprintf("%T", options["native_search"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
nativeSearch = nativeSearch && isNativeSearchHost(p.apiBase)
|
nativeSearch = nativeSearch && isNativeSearchHost(p.apiBase)
|
||||||
if len(tools) > 0 || nativeSearch {
|
if len(tools) > 0 || nativeSearch {
|
||||||
requestBody["tools"] = buildToolsList(tools, nativeSearch)
|
requestBody["tools"] = buildToolsList(tools, nativeSearch)
|
||||||
|
|||||||
@@ -398,6 +398,24 @@ var modelProviderOptionsByName = map[string]ModelProviderOption{
|
|||||||
Priority: 45,
|
Priority: 45,
|
||||||
httpAPI: true,
|
httpAPI: true,
|
||||||
},
|
},
|
||||||
|
"nearai": {
|
||||||
|
ID: "nearai",
|
||||||
|
DisplayName: "NEAR AI Cloud",
|
||||||
|
Domain: "near.ai",
|
||||||
|
DefaultAPIBase: "https://cloud-api.near.ai/v1",
|
||||||
|
CreateAllowed: true,
|
||||||
|
DefaultModelAllowed: true,
|
||||||
|
SupportsFetch: true,
|
||||||
|
Priority: 44.5,
|
||||||
|
CommonModels: []string{
|
||||||
|
"zai-org/GLM-5.1-FP8",
|
||||||
|
"openai/gpt-oss-120b",
|
||||||
|
"Qwen/Qwen3-30B-A3B-Instruct-2507",
|
||||||
|
"Qwen/Qwen3.6-35B-A3B-FP8",
|
||||||
|
},
|
||||||
|
Aliases: []string{"near-ai", "near-ai-cloud"},
|
||||||
|
httpAPI: true,
|
||||||
|
},
|
||||||
"shengsuanyun": {
|
"shengsuanyun": {
|
||||||
ID: "shengsuanyun",
|
ID: "shengsuanyun",
|
||||||
DisplayName: "ShengsuanYun",
|
DisplayName: "ShengsuanYun",
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ func (t *ExpandTool) Execute(ctx context.Context, args map[string]any) *tools.To
|
|||||||
"tokenCount": result.TokenCount,
|
"tokenCount": result.TokenCount,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
}
|
}
|
||||||
data, _ := json.Marshal(output)
|
data, err := json.Marshal(output)
|
||||||
|
if err != nil {
|
||||||
|
return tools.ErrorResult(fmt.Sprintf("failed to marshal expand result: %v", err))
|
||||||
|
}
|
||||||
return tools.NewToolResult(string(data))
|
return tools.NewToolResult(string(data))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ func (t *GrepTool) Execute(ctx context.Context, args map[string]any) *tools.Tool
|
|||||||
output["hint"] = result.Hint
|
output["hint"] = result.Hint
|
||||||
}
|
}
|
||||||
|
|
||||||
data, _ := json.Marshal(output)
|
data, err := json.Marshal(output)
|
||||||
|
if err != nil {
|
||||||
|
return tools.ErrorResult(fmt.Sprintf("failed to marshal grep result: %v", err))
|
||||||
|
}
|
||||||
return tools.NewToolResult(string(data))
|
return tools.NewToolResult(string(data))
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-14
@@ -32,6 +32,7 @@ type CronTool struct {
|
|||||||
execTool *ExecTool
|
execTool *ExecTool
|
||||||
allowCommand bool
|
allowCommand bool
|
||||||
execEnabled bool
|
execEnabled bool
|
||||||
|
commandAllowedRemotes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCronTool creates a new CronTool
|
// NewCronTool creates a new CronTool
|
||||||
@@ -42,9 +43,11 @@ func NewCronTool(
|
|||||||
) (*CronTool, error) {
|
) (*CronTool, error) {
|
||||||
allowCommand := true
|
allowCommand := true
|
||||||
execEnabled := true
|
execEnabled := true
|
||||||
|
var commandAllowedRemotes []string
|
||||||
if config != nil {
|
if config != nil {
|
||||||
allowCommand = config.Tools.Cron.AllowCommand
|
allowCommand = config.Tools.Cron.AllowCommand
|
||||||
execEnabled = config.Tools.Exec.Enabled
|
execEnabled = config.Tools.Exec.Enabled
|
||||||
|
commandAllowedRemotes = config.Tools.Cron.CommandAllowedRemotes
|
||||||
}
|
}
|
||||||
|
|
||||||
var execTool *ExecTool
|
var execTool *ExecTool
|
||||||
@@ -66,6 +69,7 @@ func NewCronTool(
|
|||||||
execTool: execTool,
|
execTool: execTool,
|
||||||
allowCommand: allowCommand,
|
allowCommand: allowCommand,
|
||||||
execEnabled: execEnabled,
|
execEnabled: execEnabled,
|
||||||
|
commandAllowedRemotes: commandAllowedRemotes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +154,11 @@ func (t *CronTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
|||||||
case "update":
|
case "update":
|
||||||
return t.updateJob(ctx, args)
|
return t.updateJob(ctx, args)
|
||||||
case "remove":
|
case "remove":
|
||||||
return t.removeJob(args)
|
return t.removeJob(ctx, args)
|
||||||
case "enable":
|
case "enable":
|
||||||
return t.enableJob(args, true)
|
return t.enableJob(ctx, args, true)
|
||||||
case "disable":
|
case "disable":
|
||||||
return t.enableJob(args, false)
|
return t.enableJob(ctx, args, false)
|
||||||
default:
|
default:
|
||||||
return ErrorResult(fmt.Sprintf("unknown action: %s", action))
|
return ErrorResult(fmt.Sprintf("unknown action: %s", action))
|
||||||
}
|
}
|
||||||
@@ -217,8 +221,10 @@ func (t *CronTool) addJob(ctx context.Context, args map[string]any) *ToolResult
|
|||||||
if !t.execEnabled {
|
if !t.execEnabled {
|
||||||
return ErrorResult("command execution is disabled")
|
return ErrorResult("command execution is disabled")
|
||||||
}
|
}
|
||||||
if !constants.IsInternalChannel(channel) {
|
if !constants.IsInternalChannel(channel) && !isCommandAllowedRemote(channel, chatID, t.commandAllowedRemotes) {
|
||||||
return ErrorResult("scheduling command execution is restricted to internal channels")
|
return ErrorResult(
|
||||||
|
"scheduling command execution is restricted to internal channels or configured remote channels",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if !t.allowCommand && !commandConfirm {
|
if !t.allowCommand && !commandConfirm {
|
||||||
return ErrorResult("command_confirm=true is required when allow_command is disabled")
|
return ErrorResult("command_confirm=true is required when allow_command is disabled")
|
||||||
@@ -371,12 +377,20 @@ func (t *CronTool) updateJob(ctx context.Context, args map[string]any) *ToolResu
|
|||||||
return SilentResult(fmt.Sprintf("Cron job updated:\n%s", formatCronJobJSON(updated)))
|
return SilentResult(fmt.Sprintf("Cron job updated:\n%s", formatCronJobJSON(updated)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) removeJob(args map[string]any) *ToolResult {
|
func (t *CronTool) removeJob(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
jobID, ok := args["job_id"].(string)
|
jobID, ok := args["job_id"].(string)
|
||||||
if !ok || jobID == "" {
|
if !ok || jobID == "" {
|
||||||
return ErrorResult("job_id is required for remove")
|
return ErrorResult("job_id is required for remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
job, ok := t.cronService.GetJob(jobID)
|
||||||
|
if !ok {
|
||||||
|
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
||||||
|
}
|
||||||
|
if !t.canAccessJob(ctx, job) {
|
||||||
|
return ErrorResult(fmt.Sprintf("Job %s is not accessible from this channel", jobID))
|
||||||
|
}
|
||||||
|
|
||||||
if t.cronService.RemoveJob(jobID) {
|
if t.cronService.RemoveJob(jobID) {
|
||||||
return SilentResult(fmt.Sprintf("Cron job removed: %s", jobID))
|
return SilentResult(fmt.Sprintf("Cron job removed: %s", jobID))
|
||||||
}
|
}
|
||||||
@@ -483,11 +497,15 @@ func positiveSeconds(args map[string]any, key string) (int64, *ToolResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) validateCommandMutation(ctx context.Context, args map[string]any) *ToolResult {
|
func (t *CronTool) validateCommandMutation(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
channel := ToolChannel(ctx)
|
||||||
|
chatID := ToolChatID(ctx)
|
||||||
if !t.execEnabled {
|
if !t.execEnabled {
|
||||||
return ErrorResult("command execution is disabled")
|
return ErrorResult("command execution is disabled")
|
||||||
}
|
}
|
||||||
if !constants.IsInternalChannel(ToolChannel(ctx)) {
|
if !constants.IsInternalChannel(channel) && !isCommandAllowedRemote(channel, chatID, t.commandAllowedRemotes) {
|
||||||
return ErrorResult("updating command execution is restricted to internal channels")
|
return ErrorResult(
|
||||||
|
"updating command execution is restricted to internal channels or configured remote channels",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
commandConfirm, _ := args["command_confirm"].(bool)
|
commandConfirm, _ := args["command_confirm"].(bool)
|
||||||
if !t.allowCommand && !commandConfirm {
|
if !t.allowCommand && !commandConfirm {
|
||||||
@@ -496,19 +514,46 @@ func (t *CronTool) validateCommandMutation(ctx context.Context, args map[string]
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCommandAllowedRemote(channel, chatID string, allowed []string) bool {
|
||||||
|
if channel == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
target := channel
|
||||||
|
if chatID != "" {
|
||||||
|
target = channel + ":" + chatID
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range allowed {
|
||||||
|
entry = strings.TrimSpace(entry)
|
||||||
|
if entry == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry == "*" || entry == channel || entry == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (t *CronTool) canAccessJob(ctx context.Context, job *cron.CronJob) bool {
|
func (t *CronTool) canAccessJob(ctx context.Context, job *cron.CronJob) bool {
|
||||||
channel := ToolChannel(ctx)
|
channel := ToolChannel(ctx)
|
||||||
if constants.IsInternalChannel(channel) {
|
if constants.IsInternalChannel(channel) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
chatID := ToolChatID(ctx)
|
chatID := ToolChatID(ctx)
|
||||||
if channel == "" || chatID == "" {
|
if channel == "" || chatID == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if job.Payload.Command != "" {
|
if job.Payload.Channel != channel || job.Payload.To != chatID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return job.Payload.Channel == channel && job.Payload.To == chatID
|
if job.Payload.Command != "" {
|
||||||
|
return isCommandAllowedRemote(channel, chatID, t.commandAllowedRemotes)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCronJobJSON(job *cron.CronJob) string {
|
func formatCronJobJSON(job *cron.CronJob) string {
|
||||||
@@ -519,14 +564,22 @@ func formatCronJobJSON(job *cron.CronJob) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
|
func (t *CronTool) enableJob(ctx context.Context, args map[string]any, enable bool) *ToolResult {
|
||||||
jobID, ok := args["job_id"].(string)
|
jobID, ok := args["job_id"].(string)
|
||||||
if !ok || jobID == "" {
|
if !ok || jobID == "" {
|
||||||
return ErrorResult("job_id is required for enable/disable")
|
return ErrorResult("job_id is required for enable/disable")
|
||||||
}
|
}
|
||||||
|
|
||||||
job := t.cronService.EnableJob(jobID, enable)
|
job, ok := t.cronService.GetJob(jobID)
|
||||||
if job == nil {
|
if !ok {
|
||||||
|
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
||||||
|
}
|
||||||
|
if !t.canAccessJob(ctx, job) {
|
||||||
|
return ErrorResult(fmt.Sprintf("Job %s is not accessible from this channel", jobID))
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedJob := t.cronService.EnableJob(jobID, enable)
|
||||||
|
if updatedJob == nil {
|
||||||
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +587,7 @@ func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
|
|||||||
if !enable {
|
if !enable {
|
||||||
status = "disabled"
|
status = "disabled"
|
||||||
}
|
}
|
||||||
return SilentResult(fmt.Sprintf("Cron job '%s' %s", job.Name, status))
|
return SilentResult(fmt.Sprintf("Cron job '%s' %s", updatedJob.Name, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteJob executes a cron job through the agent
|
// ExecuteJob executes a cron job through the agent
|
||||||
|
|||||||
+455
-3
@@ -87,7 +87,29 @@ func parseCronJobResult(t *testing.T, result *ToolResult) cron.CronJob {
|
|||||||
return job
|
return job
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCronTool_CommandBlockedFromRemoteChannel verifies command scheduling is restricted to internal channels
|
func addTestCronJob(t *testing.T, tool *CronTool, name, channel, chatID, command string) *cron.CronJob {
|
||||||
|
t.Helper()
|
||||||
|
everyMS := int64(60_000)
|
||||||
|
job, err := tool.cronService.AddJob(
|
||||||
|
name,
|
||||||
|
cron.CronSchedule{Kind: "every", EveryMS: &everyMS},
|
||||||
|
name+" message",
|
||||||
|
channel,
|
||||||
|
chatID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddJob() error: %v", err)
|
||||||
|
}
|
||||||
|
if command != "" {
|
||||||
|
job.Payload.Command = command
|
||||||
|
if err := tool.cronService.UpdateJob(job); err != nil {
|
||||||
|
t.Fatalf("UpdateJob() error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCronTool_CommandBlockedFromRemoteChannel verifies command scheduling is restricted by default.
|
||||||
func TestCronTool_CommandBlockedFromRemoteChannel(t *testing.T) {
|
func TestCronTool_CommandBlockedFromRemoteChannel(t *testing.T) {
|
||||||
tool := newTestCronTool(t)
|
tool := newTestCronTool(t)
|
||||||
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
@@ -102,8 +124,148 @@ func TestCronTool_CommandBlockedFromRemoteChannel(t *testing.T) {
|
|||||||
if !result.IsError {
|
if !result.IsError {
|
||||||
t.Fatal("expected command scheduling to be blocked from remote channel")
|
t.Fatal("expected command scheduling to be blocked from remote channel")
|
||||||
}
|
}
|
||||||
if !strings.Contains(result.ForLLM, "restricted to internal channels") {
|
if !strings.Contains(result.ForLLM, "restricted to internal channels or configured remote channels") {
|
||||||
t.Errorf("expected 'restricted to internal channels', got: %s", result.ForLLM)
|
t.Errorf("expected remote restriction message, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandAllowedFromRemoteChannelAllowlist(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram"}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected command scheduling from allowed remote channel to succeed, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandAllowedFromRemoteChatIDAllowlist(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{" telegram:1234567890 "}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "1234567890")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected command scheduling from allowed remote chat to succeed, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandAllowedFromRemoteWildcardAllowlist(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"*"}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected wildcard allowlist to allow remote command scheduling, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandAllowedRemoteWildcardRequiresNonEmptyChannel(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"*"}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected missing channel to remain blocked even with wildcard allowlist")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "no session context") {
|
||||||
|
t.Errorf("expected session context error, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandBlockedFromDifferentRemoteChatID(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram:1234567890"}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "other-chat")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"command_confirm": true,
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected command scheduling from non-allowlisted remote chat to fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "restricted to internal channels or configured remote channels") {
|
||||||
|
t.Errorf("expected remote restriction message, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_CommandAllowedRemoteRequiresConfirmWhenAllowCommandDisabled(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.AllowCommand = false
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram"}
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected allowlisted remote command scheduling to require confirm when allow_command is disabled")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "command_confirm=true") {
|
||||||
|
t.Errorf("expected command_confirm requirement message, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_AllowCommandDoesNotBypassRemoteAllowlist(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.AllowCommand = true
|
||||||
|
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "add",
|
||||||
|
"message": "check disk",
|
||||||
|
"command": "df -h",
|
||||||
|
"at_seconds": float64(60),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected allow_command=true not to bypass remote allowlist")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "restricted to internal channels or configured remote channels") {
|
||||||
|
t.Errorf("expected remote restriction message, got: %s", result.ForLLM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,6 +716,296 @@ func TestCronTool_RemoteCannotAccessCommandJob(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCronTool_AllowlistedRemoteCanAccessOwnCommandJob(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram:chat-1"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
listResult := tool.Execute(ctx, map[string]any{"action": "list"})
|
||||||
|
if listResult.IsError || !strings.Contains(listResult.ForLLM, job.ID) {
|
||||||
|
t.Fatalf("expected list to include own command job, got: %+v", listResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
getResult := tool.Execute(ctx, map[string]any{"action": "get", "job_id": job.ID})
|
||||||
|
if getResult.IsError {
|
||||||
|
t.Fatalf("expected get to access own command job, got: %s", getResult.ForLLM)
|
||||||
|
}
|
||||||
|
got := parseCronJobResult(t, getResult)
|
||||||
|
if got.ID != job.ID || got.Payload.Command != "df -h" {
|
||||||
|
t.Fatalf("get returned wrong command job: %+v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResult := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "update",
|
||||||
|
"job_id": job.ID,
|
||||||
|
"message": "updated command description",
|
||||||
|
})
|
||||||
|
if updateResult.IsError {
|
||||||
|
t.Fatalf("expected update to access own command job, got: %s", updateResult.ForLLM)
|
||||||
|
}
|
||||||
|
updated, _ := tool.cronService.GetJob(job.ID)
|
||||||
|
if updated.Payload.Message != "updated command description" || updated.Payload.Command != "df -h" {
|
||||||
|
t.Fatalf("update returned wrong command payload: %+v", updated.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_AllowlistedRemoteCannotAccessOtherChatCommandJob(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-2", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
listResult := tool.Execute(ctx, map[string]any{"action": "list"})
|
||||||
|
if listResult.IsError || strings.Contains(listResult.ForLLM, job.ID) {
|
||||||
|
t.Fatalf("expected list to hide other chat command job, got: %+v", listResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range []string{"get", "update"} {
|
||||||
|
args := map[string]any{"action": action, "job_id": job.ID}
|
||||||
|
if action == "update" {
|
||||||
|
args["message"] = "changed"
|
||||||
|
}
|
||||||
|
result := tool.Execute(ctx, args)
|
||||||
|
if !result.IsError || !strings.Contains(result.ForLLM, "not accessible") {
|
||||||
|
t.Fatalf("expected %s to reject other chat command job, got: %+v", action, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_NonAllowlistedRemoteCannotAccessOwnCommandJob(t *testing.T) {
|
||||||
|
tool := newTestCronTool(t)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
listResult := tool.Execute(ctx, map[string]any{"action": "list"})
|
||||||
|
if listResult.IsError || strings.Contains(listResult.ForLLM, job.ID) {
|
||||||
|
t.Fatalf("expected list to hide non-allowlisted command job, got: %+v", listResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range []string{"get", "update"} {
|
||||||
|
args := map[string]any{"action": action, "job_id": job.ID}
|
||||||
|
if action == "update" {
|
||||||
|
args["message"] = "changed"
|
||||||
|
}
|
||||||
|
result := tool.Execute(ctx, args)
|
||||||
|
if !result.IsError || !strings.Contains(result.ForLLM, "not accessible") {
|
||||||
|
t.Fatalf("expected %s to reject non-allowlisted command job, got: %+v", action, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_WildcardRemoteCanAccessOwnCommandJob(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"*"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
other := addTestCronJob(t, tool, "other", "telegram", "chat-2", "uptime")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
listResult := tool.Execute(ctx, map[string]any{"action": "list"})
|
||||||
|
if listResult.IsError || !strings.Contains(listResult.ForLLM, job.ID) {
|
||||||
|
t.Fatalf("expected wildcard list to include own command job, got: %+v", listResult)
|
||||||
|
}
|
||||||
|
if strings.Contains(listResult.ForLLM, other.ID) {
|
||||||
|
t.Fatalf("wildcard list should still hide other chat job, got: %s", listResult.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
getResult := tool.Execute(ctx, map[string]any{"action": "get", "job_id": job.ID})
|
||||||
|
if getResult.IsError {
|
||||||
|
t.Fatalf("expected wildcard get to access own command job, got: %s", getResult.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_InternalChannelCanAccessAllCommandJobs(t *testing.T) {
|
||||||
|
tool := newTestCronTool(t)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "cli", "direct")
|
||||||
|
|
||||||
|
listResult := tool.Execute(ctx, map[string]any{"action": "list"})
|
||||||
|
if listResult.IsError || !strings.Contains(listResult.ForLLM, job.ID) {
|
||||||
|
t.Fatalf("expected internal list to include command job, got: %+v", listResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
getResult := tool.Execute(ctx, map[string]any{"action": "get", "job_id": job.ID})
|
||||||
|
if getResult.IsError {
|
||||||
|
t.Fatalf("expected internal get to access command job, got: %s", getResult.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResult := tool.Execute(ctx, map[string]any{
|
||||||
|
"action": "update",
|
||||||
|
"job_id": job.ID,
|
||||||
|
"message": "internal update",
|
||||||
|
})
|
||||||
|
if updateResult.IsError {
|
||||||
|
t.Fatalf("expected internal update to access command job, got: %s", updateResult.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_AllowlistedRemoteCanManageOwnCommandJob(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram:chat-1"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
if action == "enable" {
|
||||||
|
tool.cronService.EnableJob(job.ID, false)
|
||||||
|
}
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected %s to access own command job, got: %s", action, result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, ok := tool.cronService.GetJob(job.ID)
|
||||||
|
switch action {
|
||||||
|
case "remove":
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("remove should delete own command job: %+v", saved)
|
||||||
|
}
|
||||||
|
case "enable":
|
||||||
|
if !ok || !saved.Enabled {
|
||||||
|
t.Fatalf("enable should enable own command job: %+v", saved)
|
||||||
|
}
|
||||||
|
case "disable":
|
||||||
|
if !ok || saved.Enabled {
|
||||||
|
t.Fatalf("disable should disable own command job: %+v", saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_RemoteCannotManageOtherChatJob(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"telegram"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-2", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if !result.IsError || !strings.Contains(result.ForLLM, "not accessible") {
|
||||||
|
t.Fatalf("expected %s to reject other chat job, got: %+v", action, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, ok := tool.cronService.GetJob(job.ID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s should not remove other chat job", action)
|
||||||
|
}
|
||||||
|
if !saved.Enabled {
|
||||||
|
t.Fatalf("%s should not disable other chat job: %+v", action, saved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_RemoteCannotManageCommandJobUnlessAllowlisted(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
tool := newTestCronTool(t)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if !result.IsError || !strings.Contains(result.ForLLM, "not accessible") {
|
||||||
|
t.Fatalf("expected %s to reject non-allowlisted command job, got: %+v", action, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, ok := tool.cronService.GetJob(job.ID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s should not remove non-allowlisted command job", action)
|
||||||
|
}
|
||||||
|
if !saved.Enabled {
|
||||||
|
t.Fatalf("%s should not disable non-allowlisted command job: %+v", action, saved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_InternalChannelCanManageAllJobs(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
tool := newTestCronTool(t)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
if action == "enable" {
|
||||||
|
tool.cronService.EnableJob(job.ID, false)
|
||||||
|
}
|
||||||
|
ctx := WithToolContext(context.Background(), "cli", "direct")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected internal %s to access command job, got: %s", action, result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, ok := tool.cronService.GetJob(job.ID)
|
||||||
|
switch action {
|
||||||
|
case "remove":
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("internal remove should delete command job: %+v", saved)
|
||||||
|
}
|
||||||
|
case "enable":
|
||||||
|
if !ok || !saved.Enabled {
|
||||||
|
t.Fatalf("internal enable should enable command job: %+v", saved)
|
||||||
|
}
|
||||||
|
case "disable":
|
||||||
|
if !ok || saved.Enabled {
|
||||||
|
t.Fatalf("internal disable should disable command job: %+v", saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_RemoteCanManageOwnNonCommandJob(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
tool := newTestCronTool(t)
|
||||||
|
job := addTestCronJob(t, tool, "reminder", "telegram", "chat-1", "")
|
||||||
|
if action == "enable" {
|
||||||
|
tool.cronService.EnableJob(job.ID, false)
|
||||||
|
}
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected %s to access own non-command job, got: %s", action, result.ForLLM)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCronTool_WildcardRemoteCanManageOwnCommandJob(t *testing.T) {
|
||||||
|
for _, action := range []string{"remove", "enable", "disable"} {
|
||||||
|
t.Run(action, func(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Tools.Cron.CommandAllowedRemotes = []string{"*"}
|
||||||
|
tool := newTestCronToolWithConfig(t, cfg)
|
||||||
|
job := addTestCronJob(t, tool, "command", "telegram", "chat-1", "df -h")
|
||||||
|
if action == "enable" {
|
||||||
|
tool.cronService.EnableJob(job.ID, false)
|
||||||
|
}
|
||||||
|
other := addTestCronJob(t, tool, "other", "telegram", "chat-2", "uptime")
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, map[string]any{"action": action, "job_id": job.ID})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected wildcard %s to access own command job, got: %s", action, result.ForLLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherResult := tool.Execute(ctx, map[string]any{"action": action, "job_id": other.ID})
|
||||||
|
if !otherResult.IsError || !strings.Contains(otherResult.ForLLM, "not accessible") {
|
||||||
|
t.Fatalf("wildcard %s should still reject other chat job, got: %+v", action, otherResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCronTool_CommandUpdateSafetyGates(t *testing.T) {
|
func TestCronTool_CommandUpdateSafetyGates(t *testing.T) {
|
||||||
t.Run("exec disabled", func(t *testing.T) {
|
t.Run("exec disabled", func(t *testing.T) {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
|
|||||||
+37
-266
@@ -55,9 +55,9 @@ var (
|
|||||||
`<a class="result__snippet[^"]*".*?>([\s\S]*?)</a>`,
|
`<a class="result__snippet[^"]*".*?>([\s\S]*?)</a>`,
|
||||||
)
|
)
|
||||||
reSogouTitle = regexp.MustCompile(
|
reSogouTitle = regexp.MustCompile(
|
||||||
`<a\s+class=resultLink\s+href="([^"]+)"[^>]*id="sogou_vr_\d+_\d+"[^>]*>\s*(.*?)\s*</a>`,
|
`<a\s+class="?resultLink"?\s+href="([^"]+)"[^>]*id="sogou_vr_\d+_\d+"[^>]*>\s*(.*?)\s*</a>`,
|
||||||
)
|
)
|
||||||
reSogouSnippet = regexp.MustCompile(`<div class="clamp\d*">\s*(.*?)\s*</div>`)
|
reSogouSnippet = regexp.MustCompile(`<div class="clamp\d*[^"]*">\s*(.*?)\s*</div>`)
|
||||||
reSogouRealURL = regexp.MustCompile(`url=([^&]+)`)
|
reSogouRealURL = regexp.MustCompile(`url=([^&]+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -350,6 +350,19 @@ func (p *BraveSearchProvider) Search(
|
|||||||
|
|
||||||
results := searchResp.Web.Results
|
results := searchResp.Web.Results
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
|
// Log a warning when the API returned 200 but no results.
|
||||||
|
// This helps diagnose API format changes or silent errors
|
||||||
|
// where the response body does not match the expected structure.
|
||||||
|
bodyPreview := string(body)
|
||||||
|
if len(bodyPreview) > 300 {
|
||||||
|
bodyPreview = bodyPreview[:300]
|
||||||
|
}
|
||||||
|
logger.WarnCF("web_search", "Brave API returned empty results",
|
||||||
|
map[string]any{
|
||||||
|
"query": query,
|
||||||
|
"status": resp.StatusCode,
|
||||||
|
"body_preview": bodyPreview,
|
||||||
|
})
|
||||||
return fmt.Sprintf("No results for: %s", query), nil
|
return fmt.Sprintf("No results for: %s", query), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1996,16 +2009,9 @@ type WebFetchTool struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
format string
|
format string
|
||||||
fetchLimitBytes int64
|
fetchLimitBytes int64
|
||||||
whitelist *privateHostWhitelist
|
whitelist *utils.PrivateHostWhitelist
|
||||||
}
|
}
|
||||||
|
|
||||||
type privateHostWhitelist struct {
|
|
||||||
exact map[string]struct{}
|
|
||||||
cidrs []*net.IPNet
|
|
||||||
}
|
|
||||||
|
|
||||||
type webFetchAllowedFirstHopHostKey struct{}
|
|
||||||
|
|
||||||
func NewWebFetchTool(maxChars int, format string, fetchLimitBytes int64) (*WebFetchTool, error) {
|
func NewWebFetchTool(maxChars int, format string, fetchLimitBytes int64) (*WebFetchTool, error) {
|
||||||
// createHTTPClient cannot fail with an empty proxy string.
|
// createHTTPClient cannot fail with an empty proxy string.
|
||||||
return NewWebFetchToolWithConfig(maxChars, "", format, fetchLimitBytes, nil)
|
return NewWebFetchToolWithConfig(maxChars, "", format, fetchLimitBytes, nil)
|
||||||
@@ -2035,31 +2041,22 @@ func NewWebFetchToolWithConfig(
|
|||||||
if maxChars <= 0 {
|
if maxChars <= 0 {
|
||||||
maxChars = defaultMaxChars
|
maxChars = defaultMaxChars
|
||||||
}
|
}
|
||||||
whitelist, err := newPrivateHostWhitelist(privateHostWhitelist)
|
whitelist, err := utils.NewPrivateHostWhitelist(privateHostWhitelist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse web fetch private host whitelist: %w", err)
|
return nil, fmt.Errorf("failed to parse web fetch private host whitelist: %w", err)
|
||||||
}
|
}
|
||||||
client, err := utils.CreateHTTPClient(proxy, fetchTimeout)
|
client, err := utils.CreateSafeHTTPClient(utils.SafeHTTPClientOptions{
|
||||||
|
ProxyURL: proxy,
|
||||||
|
Timeout: fetchTimeout,
|
||||||
|
PrivateHostWhitelist: privateHostWhitelist,
|
||||||
|
AllowPrivateHosts: func() bool {
|
||||||
|
return allowPrivateWebFetchHosts.Load()
|
||||||
|
},
|
||||||
|
MaxRedirects: maxRedirects,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create HTTP client for web fetch: %w", err)
|
return nil, fmt.Errorf("failed to create HTTP client for web fetch: %w", err)
|
||||||
}
|
}
|
||||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
|
||||||
dialer := &net.Dialer{
|
|
||||||
Timeout: 15 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
transport.DialContext = newSafeDialContext(dialer, whitelist)
|
|
||||||
}
|
|
||||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
||||||
if len(via) >= maxRedirects {
|
|
||||||
return fmt.Errorf("stopped after %d redirects", maxRedirects)
|
|
||||||
}
|
|
||||||
if isObviousPrivateHost(req.URL.Hostname(), whitelist) {
|
|
||||||
return fmt.Errorf("redirect target is private or local network host")
|
|
||||||
}
|
|
||||||
allowConfiguredProxyFirstHop(req, client.Transport)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if fetchLimitBytes <= 0 {
|
if fetchLimitBytes <= 0 {
|
||||||
fetchLimitBytes = 10 * 1024 * 1024 // Security Fallback
|
fetchLimitBytes = 10 * 1024 * 1024 // Security Fallback
|
||||||
}
|
}
|
||||||
@@ -2121,7 +2118,9 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
|||||||
// Lightweight pre-flight: block obvious localhost/literal-IP without DNS resolution.
|
// Lightweight pre-flight: block obvious localhost/literal-IP without DNS resolution.
|
||||||
// The real SSRF guard is newSafeDialContext at connect time.
|
// The real SSRF guard is newSafeDialContext at connect time.
|
||||||
hostname := parsedURL.Hostname()
|
hostname := parsedURL.Hostname()
|
||||||
if isObviousPrivateHost(hostname, t.whitelist) {
|
if utils.IsObviousPrivateHost(hostname, t.whitelist, func() bool {
|
||||||
|
return allowPrivateWebFetchHosts.Load()
|
||||||
|
}) {
|
||||||
return ErrorResult("fetching private or local network hosts is not allowed")
|
return ErrorResult("fetching private or local network hosts is not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2137,7 +2136,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
|||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to create request: %w", reqErr)
|
return nil, nil, fmt.Errorf("failed to create request: %w", reqErr)
|
||||||
}
|
}
|
||||||
allowConfiguredProxyFirstHop(req, t.client.Transport)
|
utils.AllowConfiguredProxyFirstHop(req, t.client.Transport)
|
||||||
req.Header.Set("User-Agent", ua)
|
req.Header.Set("User-Agent", ua)
|
||||||
resp, doErr := t.client.Do(req)
|
resp, doErr := t.client.Do(req)
|
||||||
if doErr != nil {
|
if doErr != nil {
|
||||||
@@ -2325,247 +2324,19 @@ func (t *WebFetchTool) extractText(htmlContent string) string {
|
|||||||
return strings.Join(cleanLines, "\n")
|
return strings.Join(cleanLines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSafeDialContext re-resolves DNS at connect time to mitigate DNS rebinding (TOCTOU)
|
|
||||||
// where a hostname resolves to a public IP during pre-flight but a private IP at connect time.
|
|
||||||
func newSafeDialContext(
|
func newSafeDialContext(
|
||||||
dialer *net.Dialer,
|
dialer *net.Dialer,
|
||||||
whitelist *privateHostWhitelist,
|
whitelist *utils.PrivateHostWhitelist,
|
||||||
) func(context.Context, string, string) (net.Conn, error) {
|
) func(context.Context, string, string) (net.Conn, error) {
|
||||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
return utils.NewSafeDialContext(dialer, whitelist, func() bool {
|
||||||
if allowPrivateWebFetchHosts.Load() {
|
return allowPrivateWebFetchHosts.Load()
|
||||||
return dialer.DialContext(ctx, network, address)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid target address %q: %w", address, err)
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
return nil, fmt.Errorf("empty target host")
|
|
||||||
}
|
|
||||||
if isAllowedFirstHopHost(ctx, host) {
|
|
||||||
return dialer.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
|
||||||
if shouldBlockPrivateIP(ip, whitelist) {
|
|
||||||
return nil, fmt.Errorf("blocked private or local target: %s", host)
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
|
||||||
}
|
|
||||||
|
|
||||||
ipAddrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve %s: %w", host, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
attempted := 0
|
|
||||||
var lastErr error
|
|
||||||
for _, ipAddr := range ipAddrs {
|
|
||||||
if shouldBlockPrivateIP(ipAddr.IP, whitelist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attempted++
|
|
||||||
conn, err := dialer.DialContext(
|
|
||||||
ctx,
|
|
||||||
network,
|
|
||||||
net.JoinHostPort(ipAddr.IP.String(), port),
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if attempted == 0 {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"all resolved addresses for %s are private, restricted, or not whitelisted",
|
|
||||||
host,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if lastErr != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"failed connecting to public addresses for %s: %w",
|
|
||||||
host,
|
|
||||||
lastErr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed connecting to public addresses for %s", host)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func allowConfiguredProxyFirstHop(req *http.Request, rt http.RoundTripper) {
|
func newPrivateHostWhitelist(entries []string) (*utils.PrivateHostWhitelist, error) {
|
||||||
if req == nil {
|
return utils.NewPrivateHostWhitelist(entries)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
transport, ok := rt.(*http.Transport)
|
|
||||||
if !ok || transport.Proxy == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyURL, err := transport.Proxy(req)
|
|
||||||
if err != nil || proxyURL == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
host := normalizeAllowedFirstHopHost(proxyURL.Hostname())
|
|
||||||
if host == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
*req = *req.WithContext(context.WithValue(
|
|
||||||
req.Context(),
|
|
||||||
webFetchAllowedFirstHopHostKey{},
|
|
||||||
host,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedFirstHopHost(ctx context.Context, host string) bool {
|
|
||||||
allowed, ok := ctx.Value(webFetchAllowedFirstHopHostKey{}).(string)
|
|
||||||
if !ok || allowed == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return allowed == normalizeAllowedFirstHopHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeAllowedFirstHopHost(host string) string {
|
|
||||||
host = strings.ToLower(strings.TrimSpace(host))
|
|
||||||
return strings.TrimSuffix(host, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPrivateHostWhitelist(entries []string) (*privateHostWhitelist, error) {
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
whitelist := &privateHostWhitelist{
|
|
||||||
exact: make(map[string]struct{}),
|
|
||||||
cidrs: make([]*net.IPNet, 0, len(entries)),
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
entry = strings.TrimSpace(entry)
|
|
||||||
if entry == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ip := net.ParseIP(entry); ip != nil {
|
|
||||||
whitelist.exact[normalizeWhitelistIP(ip).String()] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, network, err := net.ParseCIDR(entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid entry %q: expected IP or CIDR", entry)
|
|
||||||
}
|
|
||||||
whitelist.cidrs = append(whitelist.cidrs, network)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(whitelist.exact) == 0 && len(whitelist.cidrs) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return whitelist, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *privateHostWhitelist) Contains(ip net.IP) bool {
|
|
||||||
if w == nil || ip == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized := normalizeWhitelistIP(ip)
|
|
||||||
if _, ok := w.exact[normalized.String()]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, network := range w.cidrs {
|
|
||||||
if network.Contains(normalized) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeWhitelistIP(ip net.IP) net.IP {
|
|
||||||
if ip == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
return ip4
|
|
||||||
}
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldBlockPrivateIP(ip net.IP, whitelist *privateHostWhitelist) bool {
|
|
||||||
return isPrivateOrRestrictedIP(ip) && !whitelist.Contains(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isObviousPrivateHost performs a lightweight, no-DNS check for obviously private hosts.
|
|
||||||
// It catches localhost, literal private IPs, and empty hosts. It does NOT resolve DNS —
|
|
||||||
// the real SSRF guard is newSafeDialContext which checks IPs at connect time.
|
|
||||||
func isObviousPrivateHost(host string, whitelist *privateHostWhitelist) bool {
|
|
||||||
if allowPrivateWebFetchHosts.Load() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
h := strings.ToLower(strings.TrimSpace(host))
|
|
||||||
h = strings.TrimSuffix(h, ".")
|
|
||||||
if h == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if h == "localhost" || strings.HasSuffix(h, ".localhost") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip := net.ParseIP(h); ip != nil {
|
|
||||||
return shouldBlockPrivateIP(ip, whitelist)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPrivateOrRestrictedIP returns true for IPs that should never be reached via web_fetch:
|
|
||||||
// RFC 1918, loopback, link-local (incl. cloud metadata 169.254.x.x), carrier-grade NAT,
|
|
||||||
// benchmark (198.18.0.0/15), IPv6 unique-local (fc00::/7), 6to4 (2002::/16), and
|
|
||||||
// Teredo (2001:0000::/32).
|
|
||||||
func isPrivateOrRestrictedIP(ip net.IP) bool {
|
func isPrivateOrRestrictedIP(ip net.IP) bool {
|
||||||
if ip == nil {
|
return utils.IsPrivateOrRestrictedIP(ip)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
|
|
||||||
ip.IsMulticast() || ip.IsUnspecified() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
// IPv4 private, loopback, link-local, and carrier-grade NAT ranges.
|
|
||||||
if ip4[0] == 10 ||
|
|
||||||
ip4[0] == 127 ||
|
|
||||||
ip4[0] == 0 ||
|
|
||||||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
|
|
||||||
(ip4[0] == 192 && ip4[1] == 168) ||
|
|
||||||
(ip4[0] == 169 && ip4[1] == 254) ||
|
|
||||||
(ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127) ||
|
|
||||||
(ip4[0] == 198 && ip4[1] >= 18 && ip4[1] <= 19) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ip) == net.IPv6len {
|
|
||||||
// IPv6 unique local addresses (fc00::/7)
|
|
||||||
if (ip[0] & 0xfe) == 0xfc {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 6to4 addresses (2002::/16): check the embedded IPv4 at bytes [2:6].
|
|
||||||
if ip[0] == 0x20 && ip[1] == 0x02 {
|
|
||||||
embedded := net.IPv4(ip[2], ip[3], ip[4], ip[5])
|
|
||||||
return isPrivateOrRestrictedIP(embedded)
|
|
||||||
}
|
|
||||||
// Teredo (2001:0000::/32): client IPv4 is at bytes [12:16], XOR-inverted.
|
|
||||||
if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 {
|
|
||||||
client := net.IPv4(ip[12]^0xff, ip[13]^0xff, ip[14]^0xff, ip[15]^0xff)
|
|
||||||
return isPrivateOrRestrictedIP(client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -845,6 +845,25 @@ func TestWebFetch_Blocks6to4WithPrivateEmbed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWebFetch_BlocksISATAPWithPrivateEmbed verifies ISATAP with private embedded IPv4 is blocked
|
||||||
|
func TestWebFetch_BlocksISATAPWithPrivateEmbed(t *testing.T) {
|
||||||
|
tool, err := NewWebFetchTool(50000, format, testFetchLimit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create web fetch tool: %v", err)
|
||||||
|
}
|
||||||
|
// 2001:db8:1234::5efe:127.0.0.1 embeds 127.0.0.1
|
||||||
|
result := tool.Execute(context.Background(), map[string]any{
|
||||||
|
"url": "http://[2001:db8:1234::5efe:127.0.0.1]:0",
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Error("expected error for ISATAP with private embedded IPv4, got success")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "private or local network hosts is not allowed") {
|
||||||
|
t.Fatalf("expected private-host guard rejection, got %q", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestWebFetch_Allows6to4WithPublicEmbed verifies 6to4 with public embedded IPv4 is NOT blocked
|
// TestWebFetch_Allows6to4WithPublicEmbed verifies 6to4 with public embedded IPv4 is NOT blocked
|
||||||
func TestWebFetch_Allows6to4WithPublicEmbed(t *testing.T) {
|
func TestWebFetch_Allows6to4WithPublicEmbed(t *testing.T) {
|
||||||
tool, err := NewWebFetchTool(50000, format, testFetchLimit)
|
tool, err := NewWebFetchTool(50000, format, testFetchLimit)
|
||||||
@@ -981,6 +1000,12 @@ func TestIsPrivateOrRestrictedIP_Table(t *testing.T) {
|
|||||||
{"2002:7f00:0001::1", true, "6to4 with embedded 127.x (private)"},
|
{"2002:7f00:0001::1", true, "6to4 with embedded 127.x (private)"},
|
||||||
{"2002:0a00:0001::1", true, "6to4 with embedded 10.0.0.1 (private)"},
|
{"2002:0a00:0001::1", true, "6to4 with embedded 10.0.0.1 (private)"},
|
||||||
{"2002:0801:0101::1", false, "6to4 with embedded 8.1.1.1 (public)"},
|
{"2002:0801:0101::1", false, "6to4 with embedded 8.1.1.1 (public)"},
|
||||||
|
{"2001:db8:1234::5efe:127.0.0.1", true, "ISATAP with embedded 127.0.0.1 (private)"},
|
||||||
|
{"2001:db8:1234::5efe:10.0.0.1", true, "ISATAP with embedded 10.0.0.1 (private)"},
|
||||||
|
{"2001:db8:1234::5efe:8.8.8.8", false, "ISATAP with embedded 8.8.8.8 (public)"},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:127.0.0.1", true, "ISATAP 0200 with embedded 127.0.0.1 (private)"},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:10.0.0.1", true, "ISATAP 0200 with embedded 10.0.0.1 (private)"},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:8.8.8.8", false, "ISATAP 0200 with embedded 8.8.8.8 (public)"},
|
||||||
{"2001:0000:4136:e378:8000:63bf:f5ff:fffe", true, "Teredo with client 10.0.0.1 (private)"},
|
{"2001:0000:4136:e378:8000:63bf:f5ff:fffe", true, "Teredo with client 10.0.0.1 (private)"},
|
||||||
{"2001:0000:4136:e378:8000:63bf:f7f6:fefe", false, "Teredo with client 8.9.1.1 (public)"},
|
{"2001:0000:4136:e378:8000:63bf:f7f6:fefe", false, "Teredo with client 8.9.1.1 (public)"},
|
||||||
{"2607:f8b0:4004:800::200e", false, "public IPv6 (Google)"},
|
{"2607:f8b0:4004:800::200e", false, "public IPv6 (Google)"},
|
||||||
|
|||||||
+43
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -417,6 +418,16 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult
|
|||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("shell", "cmd.Wait goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
done <- fmt.Errorf("panic in cmd.Wait: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
done <- cmd.Wait()
|
done <- cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -558,7 +569,7 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
|
|||||||
// with synchronous exec runs.
|
// with synchronous exec runs.
|
||||||
if err := isolation.Start(cmd); err != nil {
|
if err := isolation.Start(cmd); err != nil {
|
||||||
if session.ptyMaster != nil {
|
if session.ptyMaster != nil {
|
||||||
session.ptyMaster.Close()
|
_ = session.ptyMaster.Close()
|
||||||
}
|
}
|
||||||
return ErrorResult(fmt.Sprintf("failed to start command: %v", err))
|
return ErrorResult(fmt.Sprintf("failed to start command: %v", err))
|
||||||
}
|
}
|
||||||
@@ -573,6 +584,18 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
|
|||||||
// so we need cmd.Wait() in a separate goroutine to detect process exit.
|
// so we need cmd.Wait() in a separate goroutine to detect process exit.
|
||||||
if session.PTY && session.ptyMaster != nil {
|
if session.PTY && session.ptyMaster != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("shell", "PTY cmd.Wait goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
session.mu.Lock()
|
||||||
|
session.Status = "error"
|
||||||
|
session.mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
cmd.Wait() // Wait for process to exit
|
cmd.Wait() // Wait for process to exit
|
||||||
session.mu.Lock()
|
session.mu.Lock()
|
||||||
if cmd.ProcessState != nil {
|
if cmd.ProcessState != nil {
|
||||||
@@ -583,6 +606,15 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("shell", "PTY read goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, err := session.ptyMaster.Read(buf)
|
n, err := session.ptyMaster.Read(buf)
|
||||||
@@ -613,6 +645,15 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
|
|||||||
// When Read() returns EOF (pipe closed), we break.
|
// When Read() returns EOF (pipe closed), we break.
|
||||||
// When process exits, OS closes pipe write end → Read() returns EOF → we exit.
|
// When process exits, OS closes pipe write end → Read() returns EOF → we exit.
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("shell", "pipe read goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
|
|
||||||
// Read stdout
|
// Read stdout
|
||||||
@@ -657,7 +698,7 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
|
|||||||
|
|
||||||
// All pipes closed, get exit status
|
// All pipes closed, get exit status
|
||||||
if stdinWriter != nil {
|
if stdinWriter != nil {
|
||||||
stdinWriter.Close()
|
_ = stdinWriter.Close()
|
||||||
}
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
@@ -165,6 +166,17 @@ func RunToolLoop(
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(idx int, tc providers.ToolCall) {
|
go func(idx int, tc providers.ToolCall) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.ErrorCF("toolloop", "tool execution goroutine panic recovered",
|
||||||
|
map[string]any{
|
||||||
|
"tool": tc.Name,
|
||||||
|
"panic": fmt.Sprintf("%v", r),
|
||||||
|
"stack": string(debug.Stack()),
|
||||||
|
})
|
||||||
|
results[idx].result = ErrorResult(fmt.Sprintf("internal panic in tool %s", tc.Name))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
argsPreview := utils.Truncate(string(argsJSON), 200)
|
argsPreview := utils.Truncate(string(argsJSON), 200)
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ func findAssetInfo(releaseURL, platform, arch string) (string, string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bs, err := io.ReadAll(resp2.Body)
|
bs, err := io.ReadAll(resp2.Body)
|
||||||
resp2.Body.Close()
|
_ = resp2.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -576,16 +576,16 @@ func extractZip(archivePath, destDir string) error {
|
|||||||
}
|
}
|
||||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, f.FileInfo().Mode())
|
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, f.FileInfo().Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc.Close()
|
_ = rc.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(out, rc); err != nil {
|
if _, err := io.Copy(out, rc); err != nil {
|
||||||
rc.Close()
|
_ = rc.Close()
|
||||||
out.Close()
|
_ = out.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := rc.Close(); err != nil {
|
if err := rc.Close(); err != nil {
|
||||||
out.Close()
|
_ = out.Close()
|
||||||
return fmt.Errorf("close zip entry reader: %w", err)
|
return fmt.Errorf("close zip entry reader: %w", err)
|
||||||
}
|
}
|
||||||
if err := out.Close(); err != nil {
|
if err := out.Close(); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SafeHTTPClientOptions struct {
|
||||||
|
ProxyURL string
|
||||||
|
Timeout time.Duration
|
||||||
|
PrivateHostWhitelist []string
|
||||||
|
AllowPrivateHosts func() bool
|
||||||
|
MaxRedirects int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrivateHostWhitelist struct {
|
||||||
|
exact map[string]struct{}
|
||||||
|
cidrs []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
type allowedFirstHopHostKey struct{}
|
||||||
|
|
||||||
|
func NewPrivateHostWhitelist(entries []string) (*PrivateHostWhitelist, error) {
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist := &PrivateHostWhitelist{
|
||||||
|
exact: make(map[string]struct{}),
|
||||||
|
cidrs: make([]*net.IPNet, 0, len(entries)),
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
entry = strings.TrimSpace(entry)
|
||||||
|
if entry == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(entry); ip != nil {
|
||||||
|
whitelist.exact[normalizeWhitelistIP(ip).String()] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, network, err := net.ParseCIDR(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid entry %q: expected IP or CIDR", entry)
|
||||||
|
}
|
||||||
|
whitelist.cidrs = append(whitelist.cidrs, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(whitelist.exact) == 0 && len(whitelist.cidrs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return whitelist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *PrivateHostWhitelist) Contains(ip net.IP) bool {
|
||||||
|
if w == nil || ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := normalizeWhitelistIP(ip)
|
||||||
|
if _, ok := w.exact[normalized.String()]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, network := range w.cidrs {
|
||||||
|
if network.Contains(normalized) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateSafeHTTPClient(opts SafeHTTPClientOptions) (*http.Client, error) {
|
||||||
|
client, err := CreateHTTPClient(opts.ProxyURL, opts.Timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist, err := NewPrivateHostWhitelist(opts.PrivateHostWhitelist)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transport, ok := client.Transport.(*http.Transport)
|
||||||
|
if ok {
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
transport.DialContext = NewSafeDialContext(dialer, whitelist, opts.AllowPrivateHosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRedirects := opts.MaxRedirects
|
||||||
|
if maxRedirects <= 0 {
|
||||||
|
maxRedirects = 10
|
||||||
|
}
|
||||||
|
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= maxRedirects {
|
||||||
|
return fmt.Errorf("stopped after %d redirects", maxRedirects)
|
||||||
|
}
|
||||||
|
if IsObviousPrivateHost(req.URL.Hostname(), whitelist, opts.AllowPrivateHosts) {
|
||||||
|
return fmt.Errorf("redirect target is private or local network host")
|
||||||
|
}
|
||||||
|
AllowConfiguredProxyFirstHop(req, client.Transport)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateSafeHTTPURL(urlStr string, whitelist *PrivateHostWhitelist, allowPrivateHosts func() bool) error {
|
||||||
|
parsedURL, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid URL: %w", err)
|
||||||
|
}
|
||||||
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||||
|
return fmt.Errorf("only http/https URLs are allowed")
|
||||||
|
}
|
||||||
|
if parsedURL.Host == "" {
|
||||||
|
return fmt.Errorf("missing domain in URL")
|
||||||
|
}
|
||||||
|
if IsObviousPrivateHost(parsedURL.Hostname(), whitelist, allowPrivateHosts) {
|
||||||
|
return fmt.Errorf("fetching private or local network hosts is not allowed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSafeDialContext(
|
||||||
|
dialer *net.Dialer,
|
||||||
|
whitelist *PrivateHostWhitelist,
|
||||||
|
allowPrivateHosts func() bool,
|
||||||
|
) func(context.Context, string, string) (net.Conn, error) {
|
||||||
|
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if allowPrivateHosts != nil && allowPrivateHosts() {
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid target address %q: %w", address, err)
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
return nil, fmt.Errorf("empty target host")
|
||||||
|
}
|
||||||
|
if isAllowedFirstHopHost(ctx, host) {
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
if shouldBlockPrivateIP(ip, whitelist) {
|
||||||
|
return nil, fmt.Errorf("blocked private or local target: %s", host)
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve %s: %w", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attempted := 0
|
||||||
|
var lastErr error
|
||||||
|
for _, ipAddr := range ipAddrs {
|
||||||
|
if shouldBlockPrivateIP(ipAddr.IP, whitelist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attempted++
|
||||||
|
conn, err := dialer.DialContext(
|
||||||
|
ctx,
|
||||||
|
network,
|
||||||
|
net.JoinHostPort(ipAddr.IP.String(), port),
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempted == 0 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"all resolved addresses for %s are private, restricted, or not whitelisted",
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"failed connecting to public addresses for %s: %w",
|
||||||
|
host,
|
||||||
|
lastErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed connecting to public addresses for %s", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllowConfiguredProxyFirstHop(req *http.Request, rt http.RoundTripper) {
|
||||||
|
if req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transport, ok := rt.(*http.Transport)
|
||||||
|
if !ok || transport.Proxy == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyURL, err := transport.Proxy(req)
|
||||||
|
if err != nil || proxyURL == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host := normalizeAllowedFirstHopHost(proxyURL.Hostname())
|
||||||
|
if host == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*req = *req.WithContext(context.WithValue(
|
||||||
|
req.Context(),
|
||||||
|
allowedFirstHopHostKey{},
|
||||||
|
host,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsObviousPrivateHost(
|
||||||
|
host string,
|
||||||
|
whitelist *PrivateHostWhitelist,
|
||||||
|
allowPrivateHosts func() bool,
|
||||||
|
) bool {
|
||||||
|
if allowPrivateHosts != nil && allowPrivateHosts() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
h := strings.ToLower(strings.TrimSpace(host))
|
||||||
|
h = strings.TrimSuffix(h, ".")
|
||||||
|
if h == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == "localhost" || strings.HasSuffix(h, ".localhost") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(h); ip != nil {
|
||||||
|
return shouldBlockPrivateIP(ip, whitelist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPrivateOrRestrictedIP(ip net.IP) bool {
|
||||||
|
if ip == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
|
||||||
|
ip.IsMulticast() || ip.IsUnspecified() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
if ip4[0] == 10 ||
|
||||||
|
ip4[0] == 127 ||
|
||||||
|
ip4[0] == 0 ||
|
||||||
|
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
|
||||||
|
(ip4[0] == 192 && ip4[1] == 168) ||
|
||||||
|
(ip4[0] == 169 && ip4[1] == 254) ||
|
||||||
|
(ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127) ||
|
||||||
|
(ip4[0] == 198 && ip4[1] >= 18 && ip4[1] <= 19) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ip) == net.IPv6len {
|
||||||
|
if (ip[0] & 0xfe) == 0xfc {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ip[0] == 0x20 && ip[1] == 0x02 {
|
||||||
|
embedded := net.IPv4(ip[2], ip[3], ip[4], ip[5])
|
||||||
|
return IsPrivateOrRestrictedIP(embedded)
|
||||||
|
}
|
||||||
|
if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 {
|
||||||
|
client := net.IPv4(ip[12]^0xff, ip[13]^0xff, ip[14]^0xff, ip[15]^0xff)
|
||||||
|
return IsPrivateOrRestrictedIP(client)
|
||||||
|
}
|
||||||
|
// ISATAP interface identifiers embed an IPv4 address behind either
|
||||||
|
// 00:00:5e:fe or 02:00:5e:fe.
|
||||||
|
if ((ip[8] == 0x00 && ip[9] == 0x00) || (ip[8] == 0x02 && ip[9] == 0x00)) &&
|
||||||
|
ip[10] == 0x5e && ip[11] == 0xfe {
|
||||||
|
embedded := net.IPv4(ip[12], ip[13], ip[14], ip[15])
|
||||||
|
return IsPrivateOrRestrictedIP(embedded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedFirstHopHost(ctx context.Context, host string) bool {
|
||||||
|
allowed, ok := ctx.Value(allowedFirstHopHostKey{}).(string)
|
||||||
|
if !ok || allowed == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return allowed == normalizeAllowedFirstHopHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAllowedFirstHopHost(host string) string {
|
||||||
|
host = strings.ToLower(strings.TrimSpace(host))
|
||||||
|
return strings.TrimSuffix(host, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWhitelistIP(ip net.IP) net.IP {
|
||||||
|
if ip == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
return ip4
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldBlockPrivateIP(ip net.IP, whitelist *PrivateHostWhitelist) bool {
|
||||||
|
return IsPrivateOrRestrictedIP(ip) && !whitelist.Contains(ip)
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateSafeHTTPClient_AllowsLoopbackProxy(t *testing.T) {
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.String() != "http://example.com/proxied" {
|
||||||
|
t.Fatalf("proxy received URL %q, want %q", r.URL.String(), "http://example.com/proxied")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("proxied"))
|
||||||
|
}))
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
client, err := CreateSafeHTTPClient(SafeHTTPClientOptions{
|
||||||
|
ProxyURL: proxy.URL,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSafeHTTPClient() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com/proxied", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest() error: %v", err)
|
||||||
|
}
|
||||||
|
AllowConfiguredProxyFirstHop(req, client.Transport)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client.Do() error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSafeHTTPClient_BlocksPrivateRedirect(t *testing.T) {
|
||||||
|
allowPrivateHosts := true
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "http://127.0.0.1/secret", http.StatusFound)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, err := CreateSafeHTTPClient(SafeHTTPClientOptions{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
AllowPrivateHosts: func() bool {
|
||||||
|
return allowPrivateHosts
|
||||||
|
},
|
||||||
|
MaxRedirects: 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSafeHTTPClient() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowPrivateHosts = false
|
||||||
|
resp, err := client.Get(server.URL)
|
||||||
|
if resp != nil && resp.Body != nil {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected redirect to private host to fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "private or local network host") &&
|
||||||
|
!strings.Contains(err.Error(), "blocked private or local target") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSafeHTTPURL_BlocksLoopback(t *testing.T) {
|
||||||
|
err := ValidateSafeHTTPURL("http://127.0.0.1:8080/file", nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected loopback URL to be blocked")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "private or local network hosts") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSafeHTTPURL_AllowsWhitelistedPrivateHost(t *testing.T) {
|
||||||
|
whitelist, err := NewPrivateHostWhitelist([]string{"127.0.0.1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPrivateHostWhitelist() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ValidateSafeHTTPURL("http://127.0.0.1:8080/file", whitelist, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected whitelisted private host to pass, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSafeDialContext_BlocksPrivateDNSResolutionWithoutWhitelist(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on loopback: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
_, port, err := net.SplitHostPort(listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to split listener address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialContext := NewSafeDialContext(&net.Dialer{Timeout: time.Second}, nil, nil)
|
||||||
|
_, err = dialContext(context.Background(), "tcp", net.JoinHostPort("localhost", port))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected localhost DNS resolution to be blocked without whitelist")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "private") && !strings.Contains(err.Error(), "whitelisted") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSafeDialContext_AllowsWhitelistedPrivateDNSResolution(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on loopback: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
accepted := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
conn, acceptErr := listener.Accept()
|
||||||
|
if acceptErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
accepted <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, port, err := net.SplitHostPort(listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to split listener address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist, err := NewPrivateHostWhitelist([]string{"127.0.0.0/8"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse whitelist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialContext := NewSafeDialContext(&net.Dialer{Timeout: time.Second}, whitelist, nil)
|
||||||
|
conn, err := dialContext(context.Background(), "tcp", net.JoinHostPort("localhost", port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected localhost DNS resolution to succeed with whitelist, got %v", err)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-accepted:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("expected localhost listener to accept a connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPrivateOrRestrictedIP_Table(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
blocked bool
|
||||||
|
}{
|
||||||
|
{"127.0.0.1", true},
|
||||||
|
{"10.0.0.1", true},
|
||||||
|
{"172.16.0.1", true},
|
||||||
|
{"192.168.1.1", true},
|
||||||
|
{"169.254.169.254", true},
|
||||||
|
{"100.64.0.1", true},
|
||||||
|
{"198.18.0.1", true},
|
||||||
|
{"198.20.0.1", false},
|
||||||
|
{"0.0.0.0", true},
|
||||||
|
{"8.8.8.8", false},
|
||||||
|
{"::1", true},
|
||||||
|
{"::ffff:127.0.0.1", true},
|
||||||
|
{"fc00::1", true},
|
||||||
|
{"2002:7f00:0001::1", true},
|
||||||
|
{"2002:0801:0101::1", false},
|
||||||
|
{"2001:db8:1234::5efe:127.0.0.1", true},
|
||||||
|
{"2001:db8:1234::5efe:10.0.0.1", true},
|
||||||
|
{"2001:db8:1234::5efe:8.8.8.8", false},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:127.0.0.1", true},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:10.0.0.1", true},
|
||||||
|
{"2001:db8:1234:0:0200:5efe:8.8.8.8", false},
|
||||||
|
{"2001:0000:4136:e378:8000:63bf:f5ff:fffe", true},
|
||||||
|
{"2607:f8b0:4004:800::200e", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.ip, func(t *testing.T) {
|
||||||
|
ip := net.ParseIP(tt.ip)
|
||||||
|
if ip == nil {
|
||||||
|
t.Fatalf("failed to parse IP: %s", tt.ip)
|
||||||
|
}
|
||||||
|
got := IsPrivateOrRestrictedIP(ip)
|
||||||
|
if got != tt.blocked {
|
||||||
|
t.Fatalf("IsPrivateOrRestrictedIP(%s) = %v, want %v", tt.ip, got, tt.blocked)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadFile_DefaultAllowsLoopbackURL(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("local download"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
path := DownloadFile(server.URL, "file.txt", DownloadOptions{
|
||||||
|
LoggerPrefix: "test",
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
})
|
||||||
|
if path == "" {
|
||||||
|
t.Fatal("expected default DownloadFile to allow loopback URL")
|
||||||
|
}
|
||||||
|
defer os.Remove(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadFile_BlockPrivateTargetsBlocksRedirectToLoopback(t *testing.T) {
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("secret"))
|
||||||
|
}))
|
||||||
|
defer target.Close()
|
||||||
|
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, target.URL, http.StatusFound)
|
||||||
|
}))
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
path := DownloadFile("http://example.com/file.txt", "file.txt", DownloadOptions{
|
||||||
|
LoggerPrefix: "test",
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
ProxyURL: proxy.URL,
|
||||||
|
BlockPrivateTargets: true,
|
||||||
|
})
|
||||||
|
if path != "" {
|
||||||
|
t.Fatalf("expected safe DownloadFile to block redirect to loopback, got %q", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ func DoRequestWithRetry(client *http.Client, req *http.Request) (*http.Response,
|
|||||||
|
|
||||||
for i := range maxRetries {
|
for i := range maxRetries {
|
||||||
if i > 0 && resp != nil {
|
if i > 0 && resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
@@ -42,7 +42,7 @@ func DoRequestWithRetry(client *http.Client, req *http.Request) (*http.Response,
|
|||||||
if i < maxRetries-1 {
|
if i < maxRetries-1 {
|
||||||
if err = sleepWithCtx(req.Context(), retryDelayForAttempt(resp, i)); err != nil {
|
if err = sleepWithCtx(req.Context(), retryDelayForAttempt(resp, i)); err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to sleep: %w", err)
|
return nil, fmt.Errorf("failed to sleep: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ func TestDoRequestWithRetry(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
} else {
|
} else {
|
||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, tc.wantAttempts, attempts)
|
assert.Equal(t, tc.wantAttempts, attempts)
|
||||||
@@ -111,7 +111,7 @@ func TestDoRequestWithRetry_RetryAfter429Honored(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
require.Equal(t, 2, attempts)
|
require.Equal(t, 2, attempts)
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, secondAttemptAt.Sub(firstAttemptAt), 900*time.Millisecond)
|
assert.GreaterOrEqual(t, secondAttemptAt.Sub(firstAttemptAt), 900*time.Millisecond)
|
||||||
@@ -148,7 +148,7 @@ func TestDoRequestWithRetry_RetryAfter429InvalidFallsBack(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
require.Equal(t, 2, attempts)
|
require.Equal(t, 2, attempts)
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, secondAttemptAt.Sub(firstAttemptAt), 45*time.Millisecond)
|
assert.GreaterOrEqual(t, secondAttemptAt.Sub(firstAttemptAt), 45*time.Millisecond)
|
||||||
@@ -200,7 +200,7 @@ func TestDoRequestWithRetry_ContextCancel(t *testing.T) {
|
|||||||
|
|
||||||
resp, err := DoRequestWithRetry(client, req)
|
resp, err := DoRequestWithRetry(client, req)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
require.Error(t, err, "expected error from context cancellation")
|
require.Error(t, err, "expected error from context cancellation")
|
||||||
assert.Nil(t, resp, "expected nil response when context is canceled")
|
assert.Nil(t, resp, "expected nil response when context is canceled")
|
||||||
@@ -275,7 +275,7 @@ func TestDoRequestWithRetry_Delay(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, delays[2], time.Millisecond)
|
assert.GreaterOrEqual(t, delays[2], time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-11
@@ -68,6 +68,7 @@ type DownloadOptions struct {
|
|||||||
ExtraHeaders map[string]string
|
ExtraHeaders map[string]string
|
||||||
LoggerPrefix string
|
LoggerPrefix string
|
||||||
ProxyURL string
|
ProxyURL string
|
||||||
|
BlockPrivateTargets bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadFile downloads a file from URL to a local temp directory.
|
// DownloadFile downloads a file from URL to a local temp directory.
|
||||||
@@ -93,21 +94,30 @@ func DownloadFile(urlStr, filename string, opts DownloadOptions) string {
|
|||||||
safeName := SanitizeFilename(filename)
|
safeName := SanitizeFilename(filename)
|
||||||
localPath := filepath.Join(mediaDir, uuid.New().String()[:8]+"_"+safeName)
|
localPath := filepath.Join(mediaDir, uuid.New().String()[:8]+"_"+safeName)
|
||||||
|
|
||||||
// Create HTTP request
|
var client *http.Client
|
||||||
req, err := http.NewRequest("GET", urlStr, nil)
|
var err error
|
||||||
|
if opts.BlockPrivateTargets {
|
||||||
|
validateErr := ValidateSafeHTTPURL(urlStr, nil, nil)
|
||||||
|
if validateErr != nil {
|
||||||
|
logger.ErrorCF(opts.LoggerPrefix, "Blocked unsafe download URL", map[string]any{
|
||||||
|
"error": validateErr.Error(),
|
||||||
|
"url": urlStr,
|
||||||
|
})
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
client, err = CreateSafeHTTPClient(SafeHTTPClientOptions{
|
||||||
|
ProxyURL: opts.ProxyURL,
|
||||||
|
Timeout: opts.Timeout,
|
||||||
|
MaxRedirects: 10,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]any{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create safe download client", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Add extra headers (e.g., Authorization for Slack)
|
client = &http.Client{Timeout: opts.Timeout}
|
||||||
for key, value := range opts.ExtraHeaders {
|
|
||||||
req.Header.Set(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: opts.Timeout}
|
|
||||||
if opts.ProxyURL != "" {
|
if opts.ProxyURL != "" {
|
||||||
proxyURL, parseErr := url.Parse(opts.ProxyURL)
|
proxyURL, parseErr := url.Parse(opts.ProxyURL)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
@@ -121,6 +131,24 @@ func DownloadFile(urlStr, filename string, opts DownloadOptions) string {
|
|||||||
Proxy: http.ProxyURL(proxyURL),
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add extra headers (e.g., Authorization for Slack)
|
||||||
|
for key, value := range opts.ExtraHeaders {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
if opts.BlockPrivateTargets {
|
||||||
|
AllowConfiguredProxyFirstHop(req, client.Transport)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]any{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]any{
|
||||||
@@ -148,7 +176,7 @@ func DownloadFile(urlStr, filename string, opts DownloadOptions) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||||
out.Close()
|
_ = out.Close()
|
||||||
os.Remove(localPath)
|
os.Remove(localPath)
|
||||||
logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
|||||||
@@ -734,9 +734,10 @@ type upstreamModel struct {
|
|||||||
|
|
||||||
func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string) ([]upstreamModel, error) {
|
func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string) ([]upstreamModel, error) {
|
||||||
apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/")
|
apiBase = strings.TrimRight(strings.TrimSpace(apiBase), "/")
|
||||||
|
provider = providers.NormalizeProvider(provider)
|
||||||
|
|
||||||
var fetchURL string
|
var fetchURL string
|
||||||
switch strings.ToLower(provider) {
|
switch provider {
|
||||||
case "ollama":
|
case "ollama":
|
||||||
// Strip /v1 suffix if present to get the Ollama root
|
// Strip /v1 suffix if present to get the Ollama root
|
||||||
root := apiBase
|
root := apiBase
|
||||||
@@ -746,6 +747,9 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
|
|||||||
root = strings.TrimRight(root, "/")
|
root = strings.TrimRight(root, "/")
|
||||||
fetchURL = root + "/api/tags"
|
fetchURL = root + "/api/tags"
|
||||||
return fetchOllamaModels(ctx, fetchURL)
|
return fetchOllamaModels(ctx, fetchURL)
|
||||||
|
case "nearai":
|
||||||
|
fetchURL = apiBase + "/model/list"
|
||||||
|
return fetchNearAIModels(ctx, fetchURL, apiKey)
|
||||||
default:
|
default:
|
||||||
// OpenAI-compatible: /v1/models
|
// OpenAI-compatible: /v1/models
|
||||||
fetchURL = apiBase + "/models"
|
fetchURL = apiBase + "/models"
|
||||||
@@ -753,6 +757,53 @@ func fetchUpstreamModels(ctx context.Context, provider, apiBase, apiKey string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchNearAIModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("nearai returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed struct {
|
||||||
|
Models []struct {
|
||||||
|
ModelID string `json:"modelId"`
|
||||||
|
OwnedBy string `json:"ownedBy"`
|
||||||
|
Metadata struct {
|
||||||
|
OwnedBy string `json:"ownedBy"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
} `json:"models"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]upstreamModel, 0, len(parsed.Models))
|
||||||
|
for _, m := range parsed.Models {
|
||||||
|
id := strings.TrimSpace(m.ModelID)
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ownedBy := strings.TrimSpace(m.OwnedBy)
|
||||||
|
if ownedBy == "" {
|
||||||
|
ownedBy = strings.TrimSpace(m.Metadata.OwnedBy)
|
||||||
|
}
|
||||||
|
models = append(models, upstreamModel{ID: id, OwnedBy: ownedBy})
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
func fetchOpenAICompatibleModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
|
func fetchOpenAICompatibleModels(ctx context.Context, fetchURL, apiKey string) ([]upstreamModel, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1961,6 +1961,13 @@ func TestHandleListModels_ReturnsProviderOptionsWithoutPersistingLegacyMigration
|
|||||||
"https://api.siliconflow.cn/v1",
|
"https://api.siliconflow.cn/v1",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if option, ok := optionsByID["nearai"]; !ok {
|
||||||
|
t.Fatal("nearai provider option missing")
|
||||||
|
} else if option.DefaultAPIBase != "https://cloud-api.near.ai/v1" {
|
||||||
|
t.Fatalf("nearai default_api_base = %q, want %q", option.DefaultAPIBase, "https://cloud-api.near.ai/v1")
|
||||||
|
} else if !option.SupportsFetch {
|
||||||
|
t.Fatal("nearai provider option should report supports_fetch")
|
||||||
|
}
|
||||||
if option, ok := optionsByID["bedrock"]; !ok {
|
if option, ok := optionsByID["bedrock"]; !ok {
|
||||||
t.Fatal("bedrock provider option missing")
|
t.Fatal("bedrock provider option missing")
|
||||||
} else if !option.CreateAllowed {
|
} else if !option.CreateAllowed {
|
||||||
@@ -2592,6 +2599,65 @@ func TestHandleFetchModels_SiliconFlowUsesOpenAICompatibleEndpoint(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleFetchModels_NearAIUsesPublicModelListEndpoint(t *testing.T) {
|
||||||
|
configPath, cleanup := setupOAuthTestEnv(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
var gotPath string
|
||||||
|
var gotAuth string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
gotAuth = r.Header.Get("Authorization")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprint(w, `{"models":[`+
|
||||||
|
`{"modelId":"zai-org/GLM-5.1-FP8","metadata":{"ownedBy":"nearai"}},`+
|
||||||
|
`{"modelId":"openai/gpt-oss-120b","metadata":{"ownedBy":"nearai"}},`+
|
||||||
|
`{"modelId":""}]}`)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/models/fetch", bytes.NewBufferString(fmt.Sprintf(`{
|
||||||
|
"provider":"nearai",
|
||||||
|
"api_key":"nearai-key",
|
||||||
|
"api_base":"%s"
|
||||||
|
}`, srv.URL)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotPath != "/model/list" {
|
||||||
|
t.Fatalf("path = %q, want %q", gotPath, "/model/list")
|
||||||
|
}
|
||||||
|
if gotAuth != "Bearer nearai-key" {
|
||||||
|
t.Fatalf("Authorization = %q, want %q", gotAuth, "Bearer nearai-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Models []upstreamModel `json:"models"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Total != 2 || len(resp.Models) != 2 {
|
||||||
|
t.Fatalf("response = %+v, want two fetched models", resp)
|
||||||
|
}
|
||||||
|
if resp.Models[0].ID != "zai-org/GLM-5.1-FP8" || resp.Models[0].OwnedBy != "nearai" {
|
||||||
|
t.Fatalf("models[0] = %+v, want GLM model owned by nearai", resp.Models[0])
|
||||||
|
}
|
||||||
|
if resp.Models[1].ID != "openai/gpt-oss-120b" || resp.Models[1].OwnedBy != "nearai" {
|
||||||
|
t.Fatalf("models[1] = %+v, want GPT OSS model owned by nearai", resp.Models[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleFetchModels_ModelIndexUsesStoredKey(t *testing.T) {
|
func TestHandleFetchModels_ModelIndexUsesStoredKey(t *testing.T) {
|
||||||
var gotAuth string
|
var gotAuth string
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.4.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
@@ -68,6 +68,6 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.59.3",
|
"typescript-eslint": "^8.59.3",
|
||||||
"vite": "^8.0.13"
|
"vite": "^8.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+214
-181
@@ -19,7 +19,7 @@ importers:
|
|||||||
version: 3.43.0(react@19.2.5)
|
version: 3.43.0(react@19.2.5)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
version: 4.3.0(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.99.0
|
specifier: ^5.99.0
|
||||||
version: 5.99.0(react@19.2.5)
|
version: 5.99.0(react@19.2.5)
|
||||||
@@ -104,13 +104,13 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^10.0.1
|
specifier: ^10.0.1
|
||||||
version: 10.0.1(eslint@10.2.1(jiti@2.7.0))
|
version: 10.0.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.19
|
specifier: ^0.5.19
|
||||||
version: 0.5.19(tailwindcss@4.3.0)
|
version: 0.5.19(tailwindcss@4.3.0)
|
||||||
'@tanstack/router-plugin':
|
'@tanstack/router-plugin':
|
||||||
specifier: ^1.164.0
|
specifier: ^1.164.0
|
||||||
version: 1.167.9(@tanstack/react-router@1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
version: 1.167.9(@tanstack/react-router@1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(prettier@3.8.3)
|
version: 6.0.2(prettier@3.8.3)
|
||||||
@@ -125,22 +125,22 @@ importers:
|
|||||||
version: 19.2.3(@types/react@19.2.14)
|
version: 19.2.3(@types/react@19.2.14)
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.58.2
|
specifier: ^8.58.2
|
||||||
version: 8.58.2(@typescript-eslint/parser@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
version: 8.58.2(@typescript-eslint/parser@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
version: 6.0.1(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^10.2.1
|
specifier: ^10.4.1
|
||||||
version: 10.2.1(jiti@2.7.0)
|
version: 10.4.1(jiti@2.7.0)
|
||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: ^10.1.8
|
specifier: ^10.1.8
|
||||||
version: 10.1.8(eslint@10.2.1(jiti@2.7.0))
|
version: 10.1.8(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-react-hooks:
|
eslint-plugin-react-hooks:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1(eslint@10.2.1(jiti@2.7.0))
|
version: 7.1.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
eslint-plugin-react-refresh:
|
eslint-plugin-react-refresh:
|
||||||
specifier: ^0.5.2
|
specifier: ^0.5.2
|
||||||
version: 0.5.2(eslint@10.2.1(jiti@2.7.0))
|
version: 0.5.2(eslint@10.4.1(jiti@2.7.0))
|
||||||
globals:
|
globals:
|
||||||
specifier: ^17.6.0
|
specifier: ^17.6.0
|
||||||
version: 17.6.0
|
version: 17.6.0
|
||||||
@@ -155,10 +155,10 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.59.3
|
specifier: ^8.59.3
|
||||||
version: 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
version: 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.13
|
specifier: ^8.0.16
|
||||||
version: 8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
version: 8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -489,8 +489,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
|
resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@eslint/config-helpers@0.5.5':
|
'@eslint/config-helpers@0.6.0':
|
||||||
resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==}
|
resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@eslint/core@1.2.1':
|
'@eslint/core@1.2.1':
|
||||||
@@ -510,8 +510,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
|
resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.7.1':
|
'@eslint/plugin-kit@0.7.2':
|
||||||
resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
|
resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@floating-ui/core@1.7.5':
|
'@floating-ui/core@1.7.5':
|
||||||
@@ -538,12 +538,16 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
hono: ^4
|
hono: ^4
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.2':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
'@humanfs/node@0.16.8':
|
||||||
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
|
||||||
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
|
'@humanfs/types@0.15.0':
|
||||||
|
resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
'@humanwhocodes/module-importer@1.0.1':
|
'@humanwhocodes/module-importer@1.0.1':
|
||||||
@@ -619,8 +623,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==}
|
resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.4':
|
'@napi-rs/wasm-runtime@1.1.5':
|
||||||
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
|
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
@@ -661,8 +665,8 @@ packages:
|
|||||||
'@open-draft/until@2.1.0':
|
'@open-draft/until@2.1.0':
|
||||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||||
|
|
||||||
'@oxc-project/types@0.130.0':
|
'@oxc-project/types@0.133.0':
|
||||||
resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==}
|
resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1':
|
'@radix-ui/number@1.1.1':
|
||||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||||
@@ -1354,97 +1358,97 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.1':
|
'@rolldown/binding-android-arm64@1.0.3':
|
||||||
resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==}
|
resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.1':
|
'@rolldown/binding-darwin-arm64@1.0.3':
|
||||||
resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==}
|
resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.1':
|
'@rolldown/binding-darwin-x64@1.0.3':
|
||||||
resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==}
|
resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.1':
|
'@rolldown/binding-freebsd-x64@1.0.3':
|
||||||
resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==}
|
resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.1':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.3':
|
||||||
resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==}
|
resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.1':
|
'@rolldown/binding-linux-arm64-gnu@1.0.3':
|
||||||
resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==}
|
resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.1':
|
'@rolldown/binding-linux-arm64-musl@1.0.3':
|
||||||
resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==}
|
resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.0.1':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.3':
|
||||||
resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==}
|
resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.0.1':
|
'@rolldown/binding-linux-s390x-gnu@1.0.3':
|
||||||
resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==}
|
resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.1':
|
'@rolldown/binding-linux-x64-gnu@1.0.3':
|
||||||
resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==}
|
resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.1':
|
'@rolldown/binding-linux-x64-musl@1.0.3':
|
||||||
resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==}
|
resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.1':
|
'@rolldown/binding-openharmony-arm64@1.0.3':
|
||||||
resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==}
|
resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openharmony]
|
os: [openharmony]
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.1':
|
'@rolldown/binding-wasm32-wasi@1.0.3':
|
||||||
resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==}
|
resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [wasm32]
|
cpu: [wasm32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.1':
|
'@rolldown/binding-win32-arm64-msvc@1.0.3':
|
||||||
resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==}
|
resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.1':
|
'@rolldown/binding-win32-x64-msvc@1.0.3':
|
||||||
resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==}
|
resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1697,8 +1701,8 @@ packages:
|
|||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
@@ -1880,6 +1884,11 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
acorn@8.17.0:
|
||||||
|
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
agent-base@7.1.4:
|
agent-base@7.1.4:
|
||||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -1892,8 +1901,8 @@ packages:
|
|||||||
ajv:
|
ajv:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
ajv@6.14.0:
|
ajv@6.15.0:
|
||||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||||
|
|
||||||
ajv@8.20.0:
|
ajv@8.20.0:
|
||||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||||
@@ -1962,8 +1971,8 @@ packages:
|
|||||||
brace-expansion@2.0.3:
|
brace-expansion@2.0.3:
|
||||||
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
||||||
|
|
||||||
brace-expansion@5.0.5:
|
brace-expansion@5.0.6:
|
||||||
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
@@ -2297,8 +2306,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
eslint@10.2.1:
|
eslint@10.4.1:
|
||||||
resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==}
|
resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3142,6 +3151,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@3.3.15:
|
||||||
|
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
@@ -3302,6 +3316,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
|
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postcss@8.5.15:
|
||||||
|
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
powershell-utils@0.1.0:
|
powershell-utils@0.1.0:
|
||||||
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -3539,8 +3557,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
rolldown@1.0.1:
|
rolldown@1.0.3:
|
||||||
resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==}
|
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3736,8 +3754,8 @@ packages:
|
|||||||
tiny-invariant@1.3.3:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
tldts-core@7.0.30:
|
tldts-core@7.0.30:
|
||||||
@@ -3937,8 +3955,8 @@ packages:
|
|||||||
vfile@6.0.3:
|
vfile@6.0.3:
|
||||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||||
|
|
||||||
vite@8.0.13:
|
vite@8.0.16:
|
||||||
resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==}
|
resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4375,9 +4393,9 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.4':
|
'@esbuild/win32-x64@0.27.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.7.0))':
|
'@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
@@ -4390,7 +4408,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/config-helpers@0.5.5':
|
'@eslint/config-helpers@0.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
|
|
||||||
@@ -4398,13 +4416,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
'@eslint/js@10.0.1(eslint@10.2.1(jiti@2.7.0))':
|
'@eslint/js@10.0.1(eslint@10.4.1(jiti@2.7.0))':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
|
|
||||||
'@eslint/object-schema@3.0.5': {}
|
'@eslint/object-schema@3.0.5': {}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.7.1':
|
'@eslint/plugin-kit@0.7.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
@@ -4432,13 +4450,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.18
|
hono: 4.12.18
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.2':
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanfs/core': 0.19.1
|
'@humanfs/types': 0.15.0
|
||||||
|
|
||||||
|
'@humanfs/node@0.16.8':
|
||||||
|
dependencies:
|
||||||
|
'@humanfs/core': 0.19.2
|
||||||
|
'@humanfs/types': 0.15.0
|
||||||
'@humanwhocodes/retry': 0.4.3
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
|
||||||
|
'@humanfs/types@0.15.0': {}
|
||||||
|
|
||||||
'@humanwhocodes/module-importer@1.0.1': {}
|
'@humanwhocodes/module-importer@1.0.1': {}
|
||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
@@ -4520,7 +4543,7 @@ snapshots:
|
|||||||
outvariant: 1.4.3
|
outvariant: 1.4.3
|
||||||
strict-event-emitter: 0.5.1
|
strict-event-emitter: 0.5.1
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.10.0
|
'@emnapi/core': 1.10.0
|
||||||
'@emnapi/runtime': 1.10.0
|
'@emnapi/runtime': 1.10.0
|
||||||
@@ -4558,7 +4581,7 @@ snapshots:
|
|||||||
|
|
||||||
'@open-draft/until@2.1.0': {}
|
'@open-draft/until@2.1.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.130.0': {}
|
'@oxc-project/types@0.133.0': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
|
|
||||||
@@ -5307,53 +5330,53 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.1':
|
'@rolldown/binding-android-arm64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.1':
|
'@rolldown/binding-darwin-arm64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.1':
|
'@rolldown/binding-darwin-x64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.1':
|
'@rolldown/binding-freebsd-x64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.1':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.1':
|
'@rolldown/binding-linux-arm64-gnu@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.1':
|
'@rolldown/binding-linux-arm64-musl@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.0.1':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.0.1':
|
'@rolldown/binding-linux-s390x-gnu@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.1':
|
'@rolldown/binding-linux-x64-gnu@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.1':
|
'@rolldown/binding-linux-x64-musl@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.1':
|
'@rolldown/binding-openharmony-arm64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.1':
|
'@rolldown/binding-wasm32-wasi@1.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.10.0
|
'@emnapi/core': 1.10.0
|
||||||
'@emnapi/runtime': 1.10.0
|
'@emnapi/runtime': 1.10.0
|
||||||
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
'@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.1':
|
'@rolldown/binding-win32-arm64-msvc@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.1':
|
'@rolldown/binding-win32-x64-msvc@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||||
@@ -5437,12 +5460,12 @@ snapshots:
|
|||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 4.3.0
|
tailwindcss: 4.3.0
|
||||||
|
|
||||||
'@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.3.0
|
'@tailwindcss/node': 4.3.0
|
||||||
'@tailwindcss/oxide': 4.3.0
|
'@tailwindcss/oxide': 4.3.0
|
||||||
tailwindcss: 4.3.0
|
tailwindcss: 4.3.0
|
||||||
vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
vite: 8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
||||||
|
|
||||||
'@tanstack/history@1.161.6': {}
|
'@tanstack/history@1.161.6': {}
|
||||||
|
|
||||||
@@ -5515,7 +5538,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tanstack/router-plugin@1.167.9(@tanstack/react-router@1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
'@tanstack/router-plugin@1.167.9(@tanstack/react-router@1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
|
||||||
@@ -5532,7 +5555,7 @@ snapshots:
|
|||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tanstack/react-router': 1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
'@tanstack/react-router': 1.169.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
vite: 8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5546,7 +5569,7 @@ snapshots:
|
|||||||
babel-dead-code-elimination: 1.0.12
|
babel-dead-code-elimination: 1.0.12
|
||||||
diff: 8.0.4
|
diff: 8.0.4
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.17
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5587,9 +5610,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5627,15 +5650,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/validate-npm-package-name@4.0.2': {}
|
'@types/validate-npm-package-name@4.0.2': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.58.2
|
'@typescript-eslint/scope-manager': 8.58.2
|
||||||
'@typescript-eslint/type-utils': 8.58.2(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.58.2(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.58.2(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.58.2
|
'@typescript-eslint/visitor-keys': 8.58.2
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
@@ -5643,15 +5666,15 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.59.3
|
'@typescript-eslint/scope-manager': 8.59.3
|
||||||
'@typescript-eslint/type-utils': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.59.3
|
'@typescript-eslint/visitor-keys': 8.59.3
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
@@ -5659,14 +5682,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.59.3
|
'@typescript-eslint/scope-manager': 8.59.3
|
||||||
'@typescript-eslint/types': 8.59.3
|
'@typescript-eslint/types': 8.59.3
|
||||||
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.59.3
|
'@typescript-eslint/visitor-keys': 8.59.3
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5707,25 +5730,25 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.58.2(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.58.2(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.58.2
|
'@typescript-eslint/types': 8.58.2
|
||||||
'@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.58.2(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.58.2(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.59.3
|
'@typescript-eslint/types': 8.59.3
|
||||||
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5744,7 +5767,7 @@ snapshots:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.0
|
semver: 7.8.0
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.17
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5759,30 +5782,30 @@ snapshots:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.0
|
semver: 7.8.0
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.17
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.58.2(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.58.2(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.7.0))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
'@typescript-eslint/scope-manager': 8.58.2
|
'@typescript-eslint/scope-manager': 8.58.2
|
||||||
'@typescript-eslint/types': 8.58.2
|
'@typescript-eslint/types': 8.58.2
|
||||||
'@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3)
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.7.0))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
'@typescript-eslint/scope-manager': 8.59.3
|
'@typescript-eslint/scope-manager': 8.59.3
|
||||||
'@typescript-eslint/types': 8.59.3
|
'@typescript-eslint/types': 8.59.3
|
||||||
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5799,29 +5822,31 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.0.1(vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
'@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||||
vite: 8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
vite: 8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
acorn-jsx@5.3.2(acorn@8.17.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.17.0
|
||||||
|
|
||||||
acorn@8.16.0: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
|
acorn@8.17.0: {}
|
||||||
|
|
||||||
agent-base@7.1.4: {}
|
agent-base@7.1.4: {}
|
||||||
|
|
||||||
ajv-formats@3.0.1(ajv@8.20.0):
|
ajv-formats@3.0.1(ajv@8.20.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.20.0
|
ajv: 8.20.0
|
||||||
|
|
||||||
ajv@6.14.0:
|
ajv@6.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
@@ -5899,7 +5924,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
brace-expansion@5.0.5:
|
brace-expansion@5.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
@@ -6169,29 +6194,29 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@10.2.1(jiti@2.7.0)):
|
eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
|
|
||||||
eslint-plugin-react-hooks@7.1.1(eslint@10.2.1(jiti@2.7.0)):
|
eslint-plugin-react-hooks@7.1.1(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/parser': 7.29.2
|
'@babel/parser': 7.29.2
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
hermes-parser: 0.25.1
|
hermes-parser: 0.25.1
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
zod-validation-error: 4.0.2(zod@4.3.6)
|
zod-validation-error: 4.0.2(zod@4.3.6)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-react-refresh@0.5.2(eslint@10.2.1(jiti@2.7.0)):
|
eslint-plugin-react-refresh@0.5.2(eslint@10.4.1(jiti@2.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
|
|
||||||
eslint-scope@9.1.2:
|
eslint-scope@9.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/esrecurse': 4.3.1
|
'@types/esrecurse': 4.3.1
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
esrecurse: 4.3.0
|
esrecurse: 4.3.0
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
|
|
||||||
@@ -6199,19 +6224,19 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@5.0.1: {}
|
eslint-visitor-keys@5.0.1: {}
|
||||||
|
|
||||||
eslint@10.2.1(jiti@2.7.0):
|
eslint@10.4.1(jiti@2.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.7.0))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0))
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@eslint/config-array': 0.23.5
|
'@eslint/config-array': 0.23.5
|
||||||
'@eslint/config-helpers': 0.5.5
|
'@eslint/config-helpers': 0.6.0
|
||||||
'@eslint/core': 1.2.1
|
'@eslint/core': 1.2.1
|
||||||
'@eslint/plugin-kit': 0.7.1
|
'@eslint/plugin-kit': 0.7.2
|
||||||
'@humanfs/node': 0.16.7
|
'@humanfs/node': 0.16.8
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@humanwhocodes/retry': 0.4.3
|
'@humanwhocodes/retry': 0.4.3
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
ajv: 6.14.0
|
ajv: 6.15.0
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
@@ -6238,8 +6263,8 @@ snapshots:
|
|||||||
|
|
||||||
espree@11.2.0:
|
espree@11.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.17.0
|
||||||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
acorn-jsx: 5.3.2(acorn@8.17.0)
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
esprima@4.0.1: {}
|
esprima@4.0.1: {}
|
||||||
@@ -6536,7 +6561,7 @@ snapshots:
|
|||||||
|
|
||||||
hast-util-to-jsx-runtime@2.3.6:
|
hast-util-to-jsx-runtime@2.3.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
comma-separated-tokens: 2.0.3
|
comma-separated-tokens: 2.0.3
|
||||||
@@ -7226,7 +7251,7 @@ snapshots:
|
|||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.5
|
brace-expansion: 5.0.6
|
||||||
|
|
||||||
minimatch@9.0.9:
|
minimatch@9.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7265,6 +7290,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.12: {}
|
nanoid@3.3.12: {}
|
||||||
|
|
||||||
|
nanoid@3.3.15: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
@@ -7425,6 +7452,12 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
postcss@8.5.15:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.15
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
powershell-utils@0.1.0: {}
|
powershell-utils@0.1.0: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
@@ -7687,26 +7720,26 @@ snapshots:
|
|||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
rolldown@1.0.1:
|
rolldown@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.130.0
|
'@oxc-project/types': 0.133.0
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rolldown/binding-android-arm64': 1.0.1
|
'@rolldown/binding-android-arm64': 1.0.3
|
||||||
'@rolldown/binding-darwin-arm64': 1.0.1
|
'@rolldown/binding-darwin-arm64': 1.0.3
|
||||||
'@rolldown/binding-darwin-x64': 1.0.1
|
'@rolldown/binding-darwin-x64': 1.0.3
|
||||||
'@rolldown/binding-freebsd-x64': 1.0.1
|
'@rolldown/binding-freebsd-x64': 1.0.3
|
||||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.1
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.3
|
||||||
'@rolldown/binding-linux-arm64-gnu': 1.0.1
|
'@rolldown/binding-linux-arm64-gnu': 1.0.3
|
||||||
'@rolldown/binding-linux-arm64-musl': 1.0.1
|
'@rolldown/binding-linux-arm64-musl': 1.0.3
|
||||||
'@rolldown/binding-linux-ppc64-gnu': 1.0.1
|
'@rolldown/binding-linux-ppc64-gnu': 1.0.3
|
||||||
'@rolldown/binding-linux-s390x-gnu': 1.0.1
|
'@rolldown/binding-linux-s390x-gnu': 1.0.3
|
||||||
'@rolldown/binding-linux-x64-gnu': 1.0.1
|
'@rolldown/binding-linux-x64-gnu': 1.0.3
|
||||||
'@rolldown/binding-linux-x64-musl': 1.0.1
|
'@rolldown/binding-linux-x64-musl': 1.0.3
|
||||||
'@rolldown/binding-openharmony-arm64': 1.0.1
|
'@rolldown/binding-openharmony-arm64': 1.0.3
|
||||||
'@rolldown/binding-wasm32-wasi': 1.0.1
|
'@rolldown/binding-wasm32-wasi': 1.0.3
|
||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.1
|
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.1
|
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7935,7 +7968,7 @@ snapshots:
|
|||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinyglobby@0.2.16:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -8000,13 +8033,13 @@ snapshots:
|
|||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|
||||||
typescript-eslint@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3):
|
typescript-eslint@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.59.3(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.59.3(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)
|
||||||
eslint: 10.2.1(jiti@2.7.0)
|
eslint: 10.4.1(jiti@2.7.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8137,13 +8170,13 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite@8.0.13(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0):
|
vite@8.0.16(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
postcss: 8.5.14
|
postcss: 8.5.15
|
||||||
rolldown: 1.0.1
|
rolldown: 1.0.3
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.6.0
|
'@types/node': 25.6.0
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
|
|||||||
Reference in New Issue
Block a user