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:
Dushnilin
2026-08-10 13:14:30 +03:00
parent 2562420bce
commit 23f3c8c587
6 changed files with 790 additions and 0 deletions
+150
View File
@@ -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
```
+10
View File
@@ -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
chmod 600 /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
define Package/tachyon/conffiles
@@ -145,6 +151,10 @@ define Package/tachyon/install
$(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
# 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' \
-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
+32
View File
@@ -133,6 +133,10 @@ function show_help() {
print(" ai_status Show AI watchdog status\n");
print(" ai_status_full Show full AI watchdog status (JSON)\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(" telegram_status Get Telegram bot running status\n");
print(" telegram_start Start Telegram bot\n");
@@ -215,6 +219,8 @@ function command_spec(command) {
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
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_start: [ "service/watchdog.uc", "start-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)));
}
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() {
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);
if (spec == null) {
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;
}
// ─── 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() {
let cfg = uci_settings();
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] || ""));
else if (mode == "doctor")
exit(doctor(ARGV[1]));
else if (mode == "diagnose-json")
exit(diagnose_json());
else if (mode == "ai-doctor")
exit(ai_doctor());
else if (mode == "validate-nfqws-strategy-json")
+495
View File
@@ -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);
}