feat: AI Agent API — HTTP REST + CLI for external LLM agents
Adds a self-healing infrastructure accessible via HTTP REST and CLI: ## New files - tachyon/files/usr/lib/service/agent_api.uc Full REST API handler for /tachyon/agent/v1/* endpoints READ (no auth): /health /snapshot /diagnose /logs /config /tools WRITE (Bearer): /heal /restart /reload /config/set /section/toggle /domain/add - tachyon/files/usr/lib/cgi-bin/tachyon-agent CGI shell wrapper — installed into /www/cgi-bin by postinst Exposes agent API via uhttpd at http://<router>/cgi-bin/tachyon-agent/<endpoint> - AGENT.md Full documentation: curl examples, SSH usage, Claude/Open-WebUI integration ## Modified files - tachyon/files/usr/bin/tachyon + diagnose_json command -> diagnostics/runtime.uc diagnose-json + agent_cgi command -> service/agent_api.uc __cgi + agent subcommand with special arg-passing (path method body bearer) + show_help() updated - tachyon/files/usr/lib/diagnostics/runtime.uc + diagnose_json() — structured JSON output of doctor checks for LLM agents + Registered in CLI dispatch as 'diagnose-json' Fix: UTF-8 byte offset for arrow character U+2192 (+3 not +2) - tachyon/Makefile + INSTALL_BIN for usr/lib/cgi-bin/tachyon-agent + postinst: symlink CGI into /www/cgi-bin on install
This commit is contained in:
@@ -0,0 +1,150 @@
|
|||||||
|
# Tachyon AI Agent API
|
||||||
|
|
||||||
|
Tachyon поддерживает подключение внешних ИИ-агентов (Claude, GPT, Open-WebUI, Cursor и т.д.) через встроенный HTTP REST API и CLI-интерфейс.
|
||||||
|
|
||||||
|
## Самодиагностика и самовосстановление (из коробки)
|
||||||
|
|
||||||
|
Watchdog Tachyon уже работает автономно: каждые 5–15 секунд он проверяет состояние всех подсистем и автоматически исправляет проблемы. Для принудительного запуска диагностики и восстановления:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tachyon ai_heal # полный цикл диагностики + авто-починки
|
||||||
|
tachyon ai_status # текущий JSON-статус watchdog
|
||||||
|
tachyon ai_status_full # расширенный JSON с метриками
|
||||||
|
tachyon doctor # текстовый отчёт доктора
|
||||||
|
tachyon diagnose_json # структурированный JSON для ИИ-агента
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP REST API (для ИИ-агентов)
|
||||||
|
|
||||||
|
После установки Tachyon поднимает CGI-эндпоинт через uhttpd:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<ip-роутера>/cgi-bin/tachyon-agent/<endpoint>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка токена (обязательна для WRITE-операций)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uci set tachyon.settings.agent_api_token='ВАШ_СЕКРЕТНЫЙ_ТОКЕН'
|
||||||
|
uci commit tachyon
|
||||||
|
```
|
||||||
|
|
||||||
|
Без токена READ-эндпоинты работают свободно с локальной сети. WRITE-эндпоинты всегда требуют `Authorization: Bearer <token>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Эндпоинты
|
||||||
|
|
||||||
|
### READ (без токена)
|
||||||
|
|
||||||
|
| Метод | Путь | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/health` | Быстрый статус служб |
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/snapshot` | Полный снимок системы для LLM |
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/diagnose` | Диагностика + автоисправление |
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/logs` | Последние 100 строк лога |
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/config` | Конфигурация UCI (секреты скрыты) |
|
||||||
|
| GET | `/cgi-bin/tachyon-agent/tools` | JSON-схема инструментов для LLM |
|
||||||
|
|
||||||
|
### WRITE (Bearer token обязателен)
|
||||||
|
|
||||||
|
| Метод | Путь | Тело | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/heal` | `{}` | Авто-диагностика и ремонт |
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/restart` | `{"service":"singbox"}` | Перезапуск службы |
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/reload` | `{}` | Перезагрузка конфигурации |
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/config/set` | `{"section":"settings","option":"dns_type","value":"doh"}` | Установить UCI-опцию |
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/section/toggle` | `{"section":"myrule"}` | Вкл/выкл секцию |
|
||||||
|
| POST | `/cgi-bin/tachyon-agent/domain/add` | `{"section":"myrule","domain":"example.com"}` | Добавить домен |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры curl
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Быстрый статус
|
||||||
|
curl http://192.168.1.1/cgi-bin/tachyon-agent/health
|
||||||
|
|
||||||
|
# Полный снимок системы
|
||||||
|
curl http://192.168.1.1/cgi-bin/tachyon-agent/snapshot | jq .
|
||||||
|
|
||||||
|
# Запуск диагностики и авто-починки
|
||||||
|
curl http://192.168.1.1/cgi-bin/tachyon-agent/diagnose | jq .
|
||||||
|
|
||||||
|
# Принудительный heal с токеном
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer ВАШ_ТОКЕН" \
|
||||||
|
http://192.168.1.1/cgi-bin/tachyon-agent/heal
|
||||||
|
|
||||||
|
# Перезапуск sing-box
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer ВАШ_ТОКЕН" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"service":"singbox"}' \
|
||||||
|
http://192.168.1.1/cgi-bin/tachyon-agent/restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI через SSH
|
||||||
|
|
||||||
|
Для ИИ-агентов с SSH-доступом:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Полный диагностический JSON
|
||||||
|
tachyon diagnose_json
|
||||||
|
|
||||||
|
# Вызов любого эндпоинта
|
||||||
|
tachyon agent /tachyon/agent/v1/health GET
|
||||||
|
tachyon agent /tachyon/agent/v1/snapshot GET
|
||||||
|
tachyon agent /tachyon/agent/v1/diagnose GET
|
||||||
|
tachyon agent /tachyon/agent/v1/heal POST '{}' 'Bearer ВАШ_ТОКЕН'
|
||||||
|
tachyon agent /tachyon/agent/v1/restart POST '{"service":"singbox"}' 'Bearer ВАШ_ТОКЕН'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подключение к Claude / Open-WebUI / Cursor
|
||||||
|
|
||||||
|
### 1. Получить схему инструментов
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://192.168.1.1/cgi-bin/tachyon-agent/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ содержит массив `tools` в формате OpenAI Function Calling / MCP.
|
||||||
|
|
||||||
|
### 2. Системный промпт для ИИ-агента
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a network assistant connected to a Tachyon OpenWrt router.
|
||||||
|
Router API base: http://192.168.1.1/cgi-bin/tachyon-agent/
|
||||||
|
Authorization token for write operations: Bearer <ВАШ_ТОКЕН>
|
||||||
|
|
||||||
|
Start by calling GET /snapshot to understand the current system state.
|
||||||
|
Then call GET /diagnose to check for problems.
|
||||||
|
Use the tools listed at GET /tools to interact with the system.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Через Open-WebUI с MCP
|
||||||
|
|
||||||
|
Добавьте URL инструментов в конфигурацию Open-WebUI:
|
||||||
|
```
|
||||||
|
http://192.168.1.1/cgi-bin/tachyon-agent/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- **READ-эндпоинты** доступны с локальной сети без токена (данные не содержат секретов)
|
||||||
|
- **WRITE-эндпоинты** всегда требуют Bearer-токен
|
||||||
|
- Токен хранится в UCI: `tachyon.settings.agent_api_token`
|
||||||
|
- Внешний (WAN) доступ закрыт фаерволом OpenWrt по умолчанию
|
||||||
|
- Для удалённого доступа используйте SSH-туннель:
|
||||||
|
```sh
|
||||||
|
ssh -L 8080:192.168.1.1:80 root@ваш-роутер
|
||||||
|
curl http://localhost:8080/cgi-bin/tachyon-agent/health
|
||||||
|
```
|
||||||
@@ -123,6 +123,12 @@ TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/m
|
|||||||
# Harden config: telegram bot_token must not be world-readable
|
# Harden config: telegram bot_token must not be world-readable
|
||||||
chmod 600 /etc/config/tachyon
|
chmod 600 /etc/config/tachyon
|
||||||
chown root:root /etc/config/tachyon
|
chown root:root /etc/config/tachyon
|
||||||
|
|
||||||
|
# Install AI Agent API CGI handler into uhttpd's cgi-bin
|
||||||
|
if [ -d /www/cgi-bin ]; then
|
||||||
|
cp /usr/lib/cgi-bin/tachyon-agent /www/cgi-bin/tachyon-agent 2>/dev/null || true
|
||||||
|
chmod 755 /www/cgi-bin/tachyon-agent 2>/dev/null || true
|
||||||
|
fi
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/tachyon/conffiles
|
define Package/tachyon/conffiles
|
||||||
@@ -145,6 +151,10 @@ define Package/tachyon/install
|
|||||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
|
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
|
||||||
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/99-tachyon-wan-monitor $(1)/etc/hotplug.d/iface/99-tachyon-wan-monitor
|
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/99-tachyon-wan-monitor $(1)/etc/hotplug.d/iface/99-tachyon-wan-monitor
|
||||||
|
|
||||||
|
# AI Agent API CGI handler (executable shell wrapper for uhttpd)
|
||||||
|
$(INSTALL_DIR) $(1)/usr/lib/cgi-bin
|
||||||
|
$(INSTALL_BIN) ./files/usr/lib/cgi-bin/tachyon-agent $(1)/usr/lib/cgi-bin/tachyon-agent
|
||||||
|
|
||||||
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(TACHYON_COMPILED_VERSION)/g' \
|
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(TACHYON_COMPILED_VERSION)/g' \
|
||||||
-e 's/__COMPILED_COMMIT_SHA__/$(shell git -C $(TOPDIR)/../ rev-parse --short HEAD 2>/dev/null || echo unknown)/g' \
|
-e 's/__COMPILED_COMMIT_SHA__/$(shell git -C $(TOPDIR)/../ rev-parse --short HEAD 2>/dev/null || echo unknown)/g' \
|
||||||
$(1)/usr/lib/tachyon/core/constants.uc
|
$(1)/usr/lib/tachyon/core/constants.uc
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ function show_help() {
|
|||||||
print(" ai_status Show AI watchdog status\n");
|
print(" ai_status Show AI watchdog status\n");
|
||||||
print(" ai_status_full Show full AI watchdog status (JSON)\n");
|
print(" ai_status_full Show full AI watchdog status (JSON)\n");
|
||||||
print(" ai_heal Run AI watchdog heal cycle\n");
|
print(" ai_heal Run AI watchdog heal cycle\n");
|
||||||
|
print(" diagnose_json Run diagnostics and return structured JSON (for AI agents)\n");
|
||||||
|
print(" agent <path> <method> [body_json] [bearer]\n");
|
||||||
|
print(" AI Agent API: call a /tachyon/agent/v1/* endpoint\n");
|
||||||
|
print(" agent_cgi AI Agent API: CGI handler (called by uhttpd)\n");
|
||||||
print(" watchdog Manage watchdog daemon (start-runtime|stop-runtime|status)\n");
|
print(" watchdog Manage watchdog daemon (start-runtime|stop-runtime|status)\n");
|
||||||
print(" telegram_status Get Telegram bot running status\n");
|
print(" telegram_status Get Telegram bot running status\n");
|
||||||
print(" telegram_start Start Telegram bot\n");
|
print(" telegram_start Start Telegram bot\n");
|
||||||
@@ -215,6 +219,8 @@ function command_spec(command) {
|
|||||||
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
||||||
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
||||||
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
||||||
|
diagnose_json: [ "diagnostics/runtime.uc", "diagnose-json", 0 ],
|
||||||
|
agent_cgi: [ "service/agent_api.uc", "__cgi", 0 ],
|
||||||
watchdog: [ "service/watchdog.uc", "", 1 ],
|
watchdog: [ "service/watchdog.uc", "", 1 ],
|
||||||
watchdog_start: [ "service/watchdog.uc", "start-runtime", 0 ],
|
watchdog_start: [ "service/watchdog.uc", "start-runtime", 0 ],
|
||||||
watchdog_stop: [ "service/watchdog.uc", "stop-runtime", 0 ],
|
watchdog_stop: [ "service/watchdog.uc", "stop-runtime", 0 ],
|
||||||
@@ -253,8 +259,34 @@ function run_spec(command, spec, user_args) {
|
|||||||
return normalize_status(system(command_from_args(args)));
|
return normalize_status(system(command_from_args(args)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function run_agent(user_args) {
|
||||||
|
// tachyon agent <path> <method> [body_json] [bearer_token]
|
||||||
|
// passes all 4 args directly to agent_api.uc which reads them as ARGV[0..3]
|
||||||
|
let module = module_path("service/agent_api.uc");
|
||||||
|
if (!file_readable(module)) {
|
||||||
|
warn("Error: service/agent_api.uc is missing\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let args = [ "ucode", "-L", LIB_DIR, module,
|
||||||
|
user_args[0] || "/",
|
||||||
|
user_args[1] || "GET",
|
||||||
|
user_args[2] || "{}",
|
||||||
|
user_args[3] || ""
|
||||||
|
];
|
||||||
|
return normalize_status(system(command_from_args(args)));
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
let command = ARGV[0] || "";
|
let command = ARGV[0] || "";
|
||||||
|
|
||||||
|
// agent command gets all remaining args passed verbatim
|
||||||
|
if (command == "agent") {
|
||||||
|
let user_args = [];
|
||||||
|
for (let i = 1; i < length(ARGV); i++)
|
||||||
|
push(user_args, ARGV[i]);
|
||||||
|
return run_agent(user_args);
|
||||||
|
}
|
||||||
|
|
||||||
let spec = command_spec(command);
|
let spec = command_spec(command);
|
||||||
if (spec == null) {
|
if (spec == null) {
|
||||||
show_help();
|
show_help();
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tachyon AI Agent API — CGI handler for uhttpd
|
||||||
|
# Install at: /usr/lib/cgi-bin/tachyon-agent
|
||||||
|
# then symlink: ln -sf /usr/lib/cgi-bin/tachyon-agent /www/cgi-bin/tachyon-agent
|
||||||
|
#
|
||||||
|
# API is served at: http://<router>/cgi-bin/tachyon-agent/<endpoint>
|
||||||
|
# Examples:
|
||||||
|
# curl http://192.168.1.1/cgi-bin/tachyon-agent/health
|
||||||
|
# curl http://192.168.1.1/cgi-bin/tachyon-agent/snapshot
|
||||||
|
# curl http://192.168.1.1/cgi-bin/tachyon-agent/diagnose
|
||||||
|
# curl -X POST -H "Authorization: Bearer mytoken" \
|
||||||
|
# http://192.168.1.1/cgi-bin/tachyon-agent/heal
|
||||||
|
|
||||||
|
TACHYON_LIB=/usr/lib/tachyon
|
||||||
|
AGENT_UC="$TACHYON_LIB/service/agent_api.uc"
|
||||||
|
|
||||||
|
PATH_INFO="${PATH_INFO:-/}"
|
||||||
|
API_PATH="/tachyon/agent/v1${PATH_INFO}"
|
||||||
|
METHOD="${REQUEST_METHOD:-GET}"
|
||||||
|
AUTH="${HTTP_AUTHORIZATION:-}"
|
||||||
|
|
||||||
|
BODY="{}"
|
||||||
|
if [ "$METHOD" = "POST" ] || [ "$METHOD" = "PUT" ]; then
|
||||||
|
if [ -n "$CONTENT_LENGTH" ] && [ "$CONTENT_LENGTH" -gt 0 ] 2>/dev/null; then
|
||||||
|
BODY=$(dd bs=1 count="$CONTENT_LENGTH" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUTPUT=$(ucode -L "$TACHYON_LIB" "$AGENT_UC" "$API_PATH" "$METHOD" "$BODY" "$AUTH" 2>/dev/null)
|
||||||
|
|
||||||
|
printf 'Content-Type: application/json\r\n'
|
||||||
|
printf 'Access-Control-Allow-Origin: *\r\n'
|
||||||
|
printf 'Cache-Control: no-cache\r\n'
|
||||||
|
printf '\r\n'
|
||||||
|
printf '%s\n' "$OUTPUT"
|
||||||
@@ -2834,6 +2834,72 @@ function doctor(format) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── AI Agent: structured JSON diagnostics ────────────────────────────────────
|
||||||
|
// Same checks as run_doctor_checks() but output is machine-readable JSON for
|
||||||
|
// LLM agents. Each problem has: id, severity, description, suggested_fix.
|
||||||
|
function diagnose_json() {
|
||||||
|
let res = run_doctor_checks();
|
||||||
|
let problems = [];
|
||||||
|
|
||||||
|
// Parse the text report lines into structured objects
|
||||||
|
for (let line in split(res.report, "\n")) {
|
||||||
|
line = trim(as_string(line));
|
||||||
|
if (line == "") continue;
|
||||||
|
|
||||||
|
let severity = null;
|
||||||
|
let fixed_inline = false;
|
||||||
|
if (index(line, "❌") >= 0) {
|
||||||
|
severity = "critical";
|
||||||
|
} else if (index(line, "⚠️") >= 0) {
|
||||||
|
severity = "warning";
|
||||||
|
} else if (index(line, "ℹ️") >= 0) {
|
||||||
|
severity = "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (severity == null) continue;
|
||||||
|
|
||||||
|
// Extract check name and message (format: icon name message)
|
||||||
|
let clean = replace(replace(replace(replace(
|
||||||
|
line, "❌", ""), "⚠️", ""), "ℹ️", ""), "✅", "");
|
||||||
|
clean = trim(clean);
|
||||||
|
|
||||||
|
let arrow_idx = index(clean, "→");
|
||||||
|
let description = trim(arrow_idx >= 0 ? substr(clean, 0, arrow_idx) : clean);
|
||||||
|
// "→" is U+2192, encoded as 3 UTF-8 bytes (E2 86 92); skip all 3
|
||||||
|
let suggested_fix = arrow_idx >= 0 ? trim(substr(clean, arrow_idx + 3)) : "";
|
||||||
|
|
||||||
|
// Detect if already fixed inline (FIXED: prefix in suggested_fix)
|
||||||
|
if (index(suggested_fix, "FIXED:") >= 0) {
|
||||||
|
fixed_inline = true;
|
||||||
|
severity = "info"; // was critical but already fixed
|
||||||
|
}
|
||||||
|
|
||||||
|
push(problems, {
|
||||||
|
severity: severity,
|
||||||
|
description: description,
|
||||||
|
suggested_fix: suggested_fix,
|
||||||
|
fixed: fixed_inline
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ai_status_data = {};
|
||||||
|
let ai_status_raw = fs.readfile("/tmp/tachyon_ai_status.json");
|
||||||
|
if (ai_status_raw) {
|
||||||
|
try { ai_status_data = json(ai_status_raw); } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(sprintf("%J\n", {
|
||||||
|
success: true,
|
||||||
|
timestamp: time(),
|
||||||
|
issues_found: res.issues,
|
||||||
|
issues_fixed: res.fixed,
|
||||||
|
overall: (res.issues == 0) ? "healthy" : ((res.fixed == res.issues) ? "repaired" : "degraded"),
|
||||||
|
problems: problems,
|
||||||
|
watchdog_status: ai_status_data
|
||||||
|
}));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
function ai_doctor() {
|
function ai_doctor() {
|
||||||
let cfg = uci_settings();
|
let cfg = uci_settings();
|
||||||
if (cfg.enable_ai_doctor != "1" || !cfg.ai_doctor_api_key) {
|
if (cfg.enable_ai_doctor != "1" || !cfg.ai_doctor_api_key) {
|
||||||
@@ -3073,6 +3139,8 @@ else if (mode == "global-check")
|
|||||||
exit(global_check(ARGV[1] || "", ARGV[2] || ""));
|
exit(global_check(ARGV[1] || "", ARGV[2] || ""));
|
||||||
else if (mode == "doctor")
|
else if (mode == "doctor")
|
||||||
exit(doctor(ARGV[1]));
|
exit(doctor(ARGV[1]));
|
||||||
|
else if (mode == "diagnose-json")
|
||||||
|
exit(diagnose_json());
|
||||||
else if (mode == "ai-doctor")
|
else if (mode == "ai-doctor")
|
||||||
exit(ai_doctor());
|
exit(ai_doctor());
|
||||||
else if (mode == "validate-nfqws-strategy-json")
|
else if (mode == "validate-nfqws-strategy-json")
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
#!/usr/bin/env ucode
|
||||||
|
|
||||||
|
// ─── Tachyon AI Agent API ─────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// HTTP handler for /tachyon/agent/v1/*
|
||||||
|
// Provides structured read/write endpoints for LLM agents (Claude, GPT, etc.)
|
||||||
|
//
|
||||||
|
// READ endpoints (no auth required from LAN):
|
||||||
|
// GET /tachyon/agent/v1/snapshot
|
||||||
|
// GET /tachyon/agent/v1/health
|
||||||
|
// GET /tachyon/agent/v1/diagnose
|
||||||
|
// GET /tachyon/agent/v1/logs
|
||||||
|
// GET /tachyon/agent/v1/config
|
||||||
|
// GET /tachyon/agent/v1/tools
|
||||||
|
//
|
||||||
|
// WRITE endpoints (require Bearer token via agent_api_token UCI option):
|
||||||
|
// POST /tachyon/agent/v1/heal
|
||||||
|
// POST /tachyon/agent/v1/restart
|
||||||
|
// POST /tachyon/agent/v1/reload
|
||||||
|
// POST /tachyon/agent/v1/config/set
|
||||||
|
// POST /tachyon/agent/v1/section/toggle
|
||||||
|
// POST /tachyon/agent/v1/domain/add
|
||||||
|
|
||||||
|
let fs = require("fs");
|
||||||
|
let uci = require("core.uci");
|
||||||
|
let common = require("core.common");
|
||||||
|
|
||||||
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||||
|
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
||||||
|
const RUNTIME_UC = LIB_DIR + "/diagnostics/runtime.uc";
|
||||||
|
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
|
||||||
|
const API_VERSION = "1";
|
||||||
|
|
||||||
|
let as_string = common.as_string;
|
||||||
|
let shell_quote = common.shell_quote;
|
||||||
|
|
||||||
|
function write_json(obj) {
|
||||||
|
print(sprintf("%J\n", obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok(data) {
|
||||||
|
let result = { success: true };
|
||||||
|
for (let k in keys(data)) result[k] = data[k];
|
||||||
|
write_json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function err(message, code) {
|
||||||
|
write_json({ success: false, error: as_string(message), code: code || 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function command_capture(command) {
|
||||||
|
let pipe = fs.popen(command, "r");
|
||||||
|
if (!pipe) return { status: 1, output: "" };
|
||||||
|
let data = pipe.read("all");
|
||||||
|
let status = pipe.close();
|
||||||
|
status = int(status);
|
||||||
|
if (status == -1) status = 255;
|
||||||
|
let sig = status & 127;
|
||||||
|
if (sig != 0) status = 128 + sig;
|
||||||
|
else status = (status >> 8) & 255;
|
||||||
|
return { status: status, output: data == null ? "" : as_string(data) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function command_output(command) {
|
||||||
|
let res = command_capture(command);
|
||||||
|
return (res.status == 0) ? trim(res.output) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function module_capture(args) {
|
||||||
|
let parts = ["ucode", "-L", shell_quote(LIB_DIR), shell_quote(RUNTIME_UC)];
|
||||||
|
for (let a in args) push(parts, shell_quote(as_string(a)));
|
||||||
|
return command_capture(join(" ", parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_json_safe(raw) {
|
||||||
|
try { return json(as_string(raw)); } catch(e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_agent_token() {
|
||||||
|
let c = uci.cursor();
|
||||||
|
if (!c) return "";
|
||||||
|
c.load(CONFIG_NAME);
|
||||||
|
return as_string(c.get(CONFIG_NAME + ".settings.agent_api_token") || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_write_auth(bearer) {
|
||||||
|
let token = get_agent_token();
|
||||||
|
if (token == "") return false;
|
||||||
|
return ("Bearer " + token) == as_string(bearer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── READ endpoints ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handle_health() {
|
||||||
|
let ai_raw = fs.readfile("/tmp/tachyon_ai_status.json") || "{}";
|
||||||
|
let ai = parse_json_safe(ai_raw) || {};
|
||||||
|
let sb_pid = trim(command_output("pidof sing-box"));
|
||||||
|
let wd_pid = trim(fs.readfile("/var/run/tachyon_watchdog.pid") || "");
|
||||||
|
ok({
|
||||||
|
status: ai.status || (sb_pid != "" ? "healthy" : "critical"),
|
||||||
|
singbox: sb_pid != "" ? "running" : "stopped",
|
||||||
|
watchdog: (wd_pid != "" && fs.stat("/proc/" + wd_pid) != null) ? "running" : "stopped",
|
||||||
|
incidents: ai.incidents_resolved_total || 0,
|
||||||
|
timestamp: time()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_diagnose() {
|
||||||
|
let res = module_capture(["diagnose-json"]);
|
||||||
|
if (res.status != 0) {
|
||||||
|
err("Diagnostics failed: " + res.output, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = parse_json_safe(res.output);
|
||||||
|
if (data == null) {
|
||||||
|
err("Invalid JSON from diagnostics", 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
print(res.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_logs() {
|
||||||
|
let raw = command_output("logread -l 100 2>/dev/null | grep -i tachyon");
|
||||||
|
let lines = [];
|
||||||
|
for (let line in split(raw, "\n")) {
|
||||||
|
line = trim(as_string(line));
|
||||||
|
if (line != "") push(lines, line);
|
||||||
|
}
|
||||||
|
ok({ lines: lines });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_config() {
|
||||||
|
let res = module_capture(["show-config", "masked"]);
|
||||||
|
let data = parse_json_safe(res.output);
|
||||||
|
if (data != null) {
|
||||||
|
print(res.output);
|
||||||
|
} else {
|
||||||
|
ok({ raw: res.output });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_snapshot() {
|
||||||
|
let mem_info = fs.readfile("/proc/meminfo") || "";
|
||||||
|
let mem_avail = 0;
|
||||||
|
for (let line in split(mem_info, "\n")) {
|
||||||
|
if (index(line, "MemAvailable:") == 0) {
|
||||||
|
let f = split(trim(line), /[ \t]+/);
|
||||||
|
if (length(f) >= 2) mem_avail = int(f[1]) / 1024;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uptime_raw = fs.readfile("/proc/uptime") || "0";
|
||||||
|
let up_sec = double(split(uptime_raw, " ")[0] || 0);
|
||||||
|
let d = int(up_sec / 86400);
|
||||||
|
let h = int((up_sec - d * 86400) / 3600);
|
||||||
|
let m = int((up_sec - d * 86400 - h * 3600) / 60);
|
||||||
|
|
||||||
|
let openwrt_ver = trim(command_output("cat /etc/openwrt_release 2>/dev/null | grep DISTRIB_RELEASE | cut -d= -f2 | tr -d '\"'"));
|
||||||
|
let tachyon_ver = trim(command_output("opkg status tachyon 2>/dev/null | grep Version | awk '{print $2}'"));
|
||||||
|
if (tachyon_ver == "")
|
||||||
|
tachyon_ver = trim(command_output("apk list tachyon 2>/dev/null | head -1"));
|
||||||
|
|
||||||
|
let sb_pid = trim(command_output("pidof sing-box"));
|
||||||
|
let wd_pid = trim(fs.readfile("/var/run/tachyon_watchdog.pid") || "");
|
||||||
|
let wd_ok = wd_pid != "" && fs.stat("/proc/" + wd_pid) != null;
|
||||||
|
|
||||||
|
let proxy_resp = command_capture("curl -s -o /dev/null -m 5 --connect-timeout 3 -w '%{http_code}' --proxy http://127.0.0.1:4534 http://www.google.com 2>/dev/null").output;
|
||||||
|
let direct_resp = command_capture("curl -s -o /dev/null -m 5 --connect-timeout 3 -w '%{http_code}' http://www.google.com 2>/dev/null").output;
|
||||||
|
|
||||||
|
let ai_status = parse_json_safe(fs.readfile("/tmp/tachyon_ai_status.json") || "{}") || {};
|
||||||
|
|
||||||
|
let logs_raw = command_output("logread -l 50 2>/dev/null | grep -i tachyon | tail -30");
|
||||||
|
let logs = [];
|
||||||
|
for (let line in split(logs_raw, "\n")) {
|
||||||
|
line = trim(as_string(line));
|
||||||
|
if (line != "") push(logs, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = [];
|
||||||
|
let section_objs = uci.section_objects(CONFIG_NAME, "section");
|
||||||
|
for (let s in section_objs) {
|
||||||
|
push(sections, {
|
||||||
|
name: s[".name"],
|
||||||
|
label: s.label || s[".name"],
|
||||||
|
enabled: s.enabled == "1",
|
||||||
|
action: s.action || "unknown"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ok({
|
||||||
|
system: {
|
||||||
|
openwrt_version: openwrt_ver || "unknown",
|
||||||
|
tachyon_version: tachyon_ver || "unknown",
|
||||||
|
uptime: sprintf("%dd %02d:%02d", d, h, m),
|
||||||
|
memory_mb_avail: mem_avail,
|
||||||
|
timestamp: time()
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
singbox: sb_pid != "" ? "running" : "stopped",
|
||||||
|
watchdog: wd_ok ? "running" : "stopped"
|
||||||
|
},
|
||||||
|
connectivity: {
|
||||||
|
direct_ok: int(direct_resp || "0") >= 200 && int(direct_resp || "0") < 400,
|
||||||
|
proxy_ok: int(proxy_resp || "0") >= 200 && int(proxy_resp || "0") < 400
|
||||||
|
},
|
||||||
|
incidents: {
|
||||||
|
total: ai_status.incidents_resolved_total || 0,
|
||||||
|
last: ai_status.last_incident || null
|
||||||
|
},
|
||||||
|
sections: sections,
|
||||||
|
logs_tail: logs,
|
||||||
|
agent_api: {
|
||||||
|
version: API_VERSION,
|
||||||
|
tools_url: "/tachyon/agent/v1/tools",
|
||||||
|
write_auth_required: get_agent_token() != ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_tools() {
|
||||||
|
ok({
|
||||||
|
schema_version: "1.0",
|
||||||
|
description: "Tachyon AI Agent API — diagnose and manage Tachyon on OpenWrt",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "tachyon_get_health",
|
||||||
|
description: "Quick health status of Tachyon services",
|
||||||
|
method: "GET",
|
||||||
|
path: "/tachyon/agent/v1/health",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_get_snapshot",
|
||||||
|
description: "Full system snapshot (services, connectivity, sections, logs). Use this first.",
|
||||||
|
method: "GET",
|
||||||
|
path: "/tachyon/agent/v1/snapshot",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_diagnose",
|
||||||
|
description: "Run full self-diagnostics. Returns structured problem list. Also auto-repairs detected issues.",
|
||||||
|
method: "GET",
|
||||||
|
path: "/tachyon/agent/v1/diagnose",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_get_logs",
|
||||||
|
description: "Last 100 Tachyon-related syslog lines",
|
||||||
|
method: "GET",
|
||||||
|
path: "/tachyon/agent/v1/logs",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_get_config",
|
||||||
|
description: "Current UCI configuration (secrets masked)",
|
||||||
|
method: "GET",
|
||||||
|
path: "/tachyon/agent/v1/config",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_heal",
|
||||||
|
description: "Run automatic self-healing. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/heal",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_restart",
|
||||||
|
description: "Restart a subsystem: singbox | tachyon | dnsmasq | watchdog. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/restart",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["service"],
|
||||||
|
properties: {
|
||||||
|
service: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["singbox", "tachyon", "dnsmasq", "watchdog"],
|
||||||
|
description: "Service to restart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_reload",
|
||||||
|
description: "Reload Tachyon config without full restart. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/reload",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: { type: "object", properties: {} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_set_config",
|
||||||
|
description: "Set a UCI option. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/config/set",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["section", "option", "value"],
|
||||||
|
properties: {
|
||||||
|
section: { type: "string", description: "UCI section name" },
|
||||||
|
option: { type: "string", description: "UCI option name" },
|
||||||
|
value: { type: "string", description: "Value to set" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_toggle_section",
|
||||||
|
description: "Enable or disable a routing section. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/section/toggle",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["section"],
|
||||||
|
properties: {
|
||||||
|
section: { type: "string", description: "Section .name" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tachyon_add_domain",
|
||||||
|
description: "Add a domain to a routing section. Requires Bearer token.",
|
||||||
|
method: "POST",
|
||||||
|
path: "/tachyon/agent/v1/domain/add",
|
||||||
|
auth: "bearer",
|
||||||
|
input_schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["section", "domain"],
|
||||||
|
properties: {
|
||||||
|
section: { type: "string", description: "Section .name" },
|
||||||
|
domain: { type: "string", description: "Domain to add" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── WRITE endpoints ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handle_heal() {
|
||||||
|
let res = module_capture(["diagnose-json"]);
|
||||||
|
let data = parse_json_safe(res.output);
|
||||||
|
command_capture("ucode -L " + shell_quote(LIB_DIR) + " " + shell_quote(WATCHDOG_UC) + " ai-heal > /dev/null 2>&1");
|
||||||
|
ok({
|
||||||
|
message: "Self-healing completed",
|
||||||
|
issues_found: data ? (data.issues_found || 0) : 0,
|
||||||
|
issues_fixed: data ? (data.issues_fixed || 0) : 0,
|
||||||
|
overall: data ? (data.overall || "unknown") : "unknown",
|
||||||
|
problems: data ? (data.problems || []) : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_restart(body) {
|
||||||
|
let svc = as_string(body.service || "");
|
||||||
|
let allowed = { singbox: 1, tachyon: 1, dnsmasq: 1, watchdog: 1 };
|
||||||
|
if (!allowed[svc]) {
|
||||||
|
err("Unknown service '" + svc + "'. Allowed: singbox, tachyon, dnsmasq, watchdog");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd_map = {
|
||||||
|
singbox: "/etc/init.d/sing-box restart > /dev/null 2>&1",
|
||||||
|
tachyon: "/etc/init.d/tachyon restart > /dev/null 2>&1",
|
||||||
|
dnsmasq: "/etc/init.d/dnsmasq restart > /dev/null 2>&1",
|
||||||
|
watchdog: "/usr/bin/tachyon watchdog start-runtime > /dev/null 2>&1"
|
||||||
|
};
|
||||||
|
|
||||||
|
system(cmd_map[svc]);
|
||||||
|
ok({ message: "Restart of '" + svc + "' initiated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_reload() {
|
||||||
|
system("/usr/bin/tachyon reload > /dev/null 2>&1");
|
||||||
|
ok({ message: "Reload initiated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_config_set(body) {
|
||||||
|
let section = as_string(body.section || "");
|
||||||
|
let option = as_string(body.option || "");
|
||||||
|
let value = as_string(body.value || "");
|
||||||
|
|
||||||
|
if (section == "" || option == "") {
|
||||||
|
err("section and option are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use module-level uci.set() which handles path as package.section.option
|
||||||
|
let path = CONFIG_NAME + "." + section + "." + option;
|
||||||
|
if (!uci.set(path, value)) {
|
||||||
|
err("Failed to set UCI option: " + path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!uci.commit(CONFIG_NAME)) {
|
||||||
|
err("Failed to commit UCI config", 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok({ message: "Set " + path + " = " + value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_section_toggle(body) {
|
||||||
|
let section = as_string(body.section || "");
|
||||||
|
if (section == "") { err("section is required"); return; }
|
||||||
|
|
||||||
|
let s = uci.get_all(CONFIG_NAME, section);
|
||||||
|
if (!s) { err("Section not found: " + section, 404); return; }
|
||||||
|
|
||||||
|
let new_val = (s.enabled == "1") ? "0" : "1";
|
||||||
|
if (!uci.set(CONFIG_NAME + "." + section + ".enabled", new_val)) {
|
||||||
|
err("Failed to toggle section", 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uci.commit(CONFIG_NAME);
|
||||||
|
system("/usr/bin/tachyon reload > /dev/null 2>&1");
|
||||||
|
|
||||||
|
ok({
|
||||||
|
message: "Section '" + section + "' is now " + (new_val == "1" ? "enabled" : "disabled"),
|
||||||
|
enabled: new_val == "1"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_domain_add(body) {
|
||||||
|
let section = as_string(body.section || "");
|
||||||
|
let domain = as_string(body.domain || "");
|
||||||
|
if (section == "" || domain == "") { err("section and domain are required"); return; }
|
||||||
|
|
||||||
|
if (!match(domain, /^[a-zA-Z0-9*._-]+$/)) {
|
||||||
|
err("Invalid domain: " + domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = CONFIG_NAME + "." + section + ".domain";
|
||||||
|
if (!uci.add_list(path, domain)) {
|
||||||
|
err("Failed to add domain (section may not exist or domain already present)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uci.commit(CONFIG_NAME);
|
||||||
|
system("/usr/bin/tachyon reload > /dev/null 2>&1");
|
||||||
|
|
||||||
|
ok({ message: "Domain '" + domain + "' added to section '" + section + "'" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let path_arg = as_string(ARGV[0] || "");
|
||||||
|
let method = uc(as_string(ARGV[1] || "GET"));
|
||||||
|
let body = parse_json_safe(ARGV[2] || "{}") || {};
|
||||||
|
let bearer = as_string(ARGV[3] || "");
|
||||||
|
|
||||||
|
let route = replace(path_arg, /^\/tachyon\/agent\/v1/, "");
|
||||||
|
if (route == "") route = "/";
|
||||||
|
|
||||||
|
if (method == "GET") {
|
||||||
|
if (route == "/health" || route == "/health/")
|
||||||
|
handle_health();
|
||||||
|
else if (route == "/snapshot" || route == "/snapshot/")
|
||||||
|
handle_snapshot();
|
||||||
|
else if (route == "/diagnose" || route == "/diagnose/")
|
||||||
|
handle_diagnose();
|
||||||
|
else if (route == "/logs" || route == "/logs/")
|
||||||
|
handle_logs();
|
||||||
|
else if (route == "/config" || route == "/config/")
|
||||||
|
handle_config();
|
||||||
|
else if (route == "/tools" || route == "/tools/")
|
||||||
|
handle_tools();
|
||||||
|
else
|
||||||
|
err("Unknown endpoint: GET " + route, 404);
|
||||||
|
} else if (method == "POST") {
|
||||||
|
if (!check_write_auth(bearer)) {
|
||||||
|
err("Unauthorized. Configure agent_api_token in UCI and use Bearer token.", 401);
|
||||||
|
} else {
|
||||||
|
if (route == "/heal" || route == "/heal/")
|
||||||
|
handle_heal();
|
||||||
|
else if (route == "/restart" || route == "/restart/")
|
||||||
|
handle_restart(body);
|
||||||
|
else if (route == "/reload" || route == "/reload/")
|
||||||
|
handle_reload();
|
||||||
|
else if (route == "/config/set")
|
||||||
|
handle_config_set(body);
|
||||||
|
else if (route == "/section/toggle")
|
||||||
|
handle_section_toggle(body);
|
||||||
|
else if (route == "/domain/add")
|
||||||
|
handle_domain_add(body);
|
||||||
|
else
|
||||||
|
err("Unknown endpoint: POST " + route, 404);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err("Method not allowed: " + method, 405);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user