diff --git a/agentflow/Makefile b/agentflow/Makefile
index 9bee136eb..f70ec2ad2 100644
--- a/agentflow/Makefile
+++ b/agentflow/Makefile
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
AGENTFLOW_ARCH_aarch64:=arm64
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
-AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc
-AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231
+AGENTFLOW_HASH_x86_64:=478fdb17cfb2b1664e7bc01315186e71f228871c0d20c816d8b3b8fe789bdb4d
+AGENTFLOW_HASH_aarch64:=257faeaad2445dd14f87c1e1d9ded869fd301c5df2c2c9af817e70303eab2512
PKG_NAME:=agentflow
PKG_VERSION:=0.3.3
-PKG_RELEASE:=11
+PKG_RELEASE:=12
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
diff --git a/luci-app-agentflow/Makefile b/luci-app-agentflow/Makefile
index 3a42f8eb7..fbc2e99f6 100644
--- a/luci-app-agentflow/Makefile
+++ b/luci-app-agentflow/Makefile
@@ -7,9 +7,9 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
-PKG_VERSION:=1.0.0
-PKG_RELEASE:=5
-LUCI_DEPENDS:=+agentflow +luci-compat
+PKG_VERSION:=1.0.1
+PKG_RELEASE:=6
+LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
diff --git a/luci-app-agentflow/luasrc/controller/agentflow.lua b/luci-app-agentflow/luasrc/controller/agentflow.lua
index c1b0924b8..3c7621859 100644
--- a/luci-app-agentflow/luasrc/controller/agentflow.lua
+++ b/luci-app-agentflow/luasrc/controller/agentflow.lua
@@ -17,10 +17,38 @@ function index()
return
end
+ local install = entry({"admin", "services", "agentflow", "agent_install"}, call("agentflow_agent_install"))
+ install.leaf = true
+
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
page.dependent = true
end
+local function write_json(obj)
+ http.prepare_content("application/json")
+ http.write_json(obj)
+end
+
+local function require_post_csrf()
+ local dispatcher = require "luci.dispatcher"
+ local context = dispatcher.context or {}
+ local expected = context.authtoken or context.token
+
+ if (http.getenv("REQUEST_METHOD") or "") ~= "POST" then
+ http.status(405, "Method Not Allowed")
+ write_json({ ok = false, error = "method not allowed" })
+ return false
+ end
+
+ if not context.authsession or not expected or http.formvalue("token") ~= expected then
+ http.status(403, "Forbidden")
+ write_json({ ok = false, error = "invalid csrf token" })
+ return false
+ end
+
+ return true
+end
+
local function uhttpd_has_apps_proxy_prefix()
local uci = require "luci.model.uci".cursor()
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
@@ -119,8 +147,97 @@ function agentflow_status()
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
}
- http.prepare_content("application/json")
- http.write_json(status)
+ write_json(status)
+end
+
+function agentflow_agent_install()
+ local fs = require "nixio.fs"
+ local jsonc = require "luci.jsonc"
+ local sys = require "luci.sys"
+ local util = require "luci.util"
+ local task_id = "agentflow-agent-install"
+ local task_script = "/tmp/agentflow-agent-install.sh"
+ local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh"
+ local agents = {
+ codexcli = true,
+ ["claude-code"] = true,
+ opencode = true,
+ kimi = true,
+ reasonix = true
+ }
+
+ if not require_post_csrf() then
+ return
+ end
+
+ local agent = http.formvalue("agent") or ""
+ if not agents[agent] then
+ write_json({ ok = false, error = "unsupported agent" })
+ return
+ end
+ if not fs.access("/etc/init.d/tasks") then
+ write_json({ ok = false, error = "taskd is not available" })
+ return
+ end
+ if not fs.access("/lib/functions/mise.sh") then
+ write_json({ ok = false, error = "mise environment helper is not available" })
+ return
+ end
+
+ local task_status = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null")
+ local status = jsonc.parse(task_status) or {}
+ local running = type(status) == "table" and status.running
+ if running == true or running == 1 or running == "1" or running == "true" then
+ write_json({ ok = true, busy = true, task_id = task_id })
+ return
+ end
+
+ local install_script = table.concat({
+ "set -e",
+ "agent=" .. util.shellquote(agent),
+ "installer_url=" .. util.shellquote(installer_url),
+ 'installer="/tmp/agentflow-installapp-mise.$$"',
+ 'cleanup() { rm -f "$installer" "$0"; }',
+ "trap cleanup 0 HUP INT TERM",
+ '. /lib/functions/mise.sh',
+ 'if ! istore_runtime_env; then echo "[agentflow] Failed to initialize the shared runtime environment" >&2; exit 1; fi',
+ "set -u",
+ "export MISE_YES=1",
+ 'echo "[agentflow] Downloading installer: $installer_url"',
+ 'if command -v wget >/dev/null 2>&1; then',
+ '\twget -O "$installer" "$installer_url"',
+ 'elif command -v curl >/dev/null 2>&1; then',
+ '\tcurl -fL -o "$installer" "$installer_url"',
+ 'elif command -v uclient-fetch >/dev/null 2>&1; then',
+ '\tuclient-fetch -O "$installer" "$installer_url"',
+ "else",
+ '\techo "[agentflow] No HTTPS download tool is available" >&2',
+ "\texit 1",
+ "fi",
+ 'if [ ! -s "$installer" ]; then echo "[agentflow] Failed to download the agent installer" >&2; exit 1; fi',
+ 'chmod 0700 "$installer"',
+ 'echo "[agentflow] Running installapp-mise.sh for $agent in $HOME"',
+ '/bin/sh "$installer" "$agent"'
+ }, "\n")
+ if not fs.writefile(task_script, install_script .. "\n") then
+ write_json({ ok = false, error = "failed to create install task", task_id = task_id })
+ return
+ end
+ if sys.call("chmod 0600 " .. util.shellquote(task_script)) ~= 0 then
+ fs.unlink(task_script)
+ write_json({ ok = false, error = "failed to secure install task", task_id = task_id })
+ return
+ end
+
+ local command = "/bin/sh " .. util.shellquote(task_script)
+ local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(command) .. " >/dev/null 2>&1")
+ if rc ~= 0 then
+ fs.unlink(task_script)
+ write_json({ ok = false, error = "failed to start install task", task_id = task_id })
+ return
+ end
+
+ write_json({ ok = true, task_id = task_id })
end
function agentflow_open()
diff --git a/luci-app-agentflow/luasrc/view/agentflow/status.htm b/luci-app-agentflow/luasrc/view/agentflow/status.htm
index 6e21cd833..615acb0cf 100644
--- a/luci-app-agentflow/luasrc/view/agentflow/status.htm
+++ b/luci-app-agentflow/luasrc/view/agentflow/status.htm
@@ -1,11 +1,80 @@
+<%+tasks/embed%>
+
+
+
+
+
+
+
<%:Install agent%>
+
<%:Select a coding agent to install into the shared AgentFlow runtime.%>
+
+
+
+
+
+
+
+
+
+
diff --git a/luci-app-agentflow/po/zh-cn/agentflow.po b/luci-app-agentflow/po/zh-cn/agentflow.po
index 41746741e..3b684b092 100644
--- a/luci-app-agentflow/po/zh-cn/agentflow.po
+++ b/luci-app-agentflow/po/zh-cn/agentflow.po
@@ -51,3 +51,24 @@ msgstr "AgentFlow 服务未运行。"
msgid "The AgentFlow service is running."
msgstr "AgentFlow 服务运行中。"
+
+msgid "Install agent"
+msgstr "安装智能体"
+
+msgid "Agent"
+msgstr "智能体"
+
+msgid "Select a coding agent to install into the shared AgentFlow runtime."
+msgstr "选择要安装到 AgentFlow 共享运行环境中的编码智能体。"
+
+msgid "Cancel"
+msgstr "取消"
+
+msgid "Install now"
+msgstr "立即安装"
+
+msgid "Task log window is not available."
+msgstr "任务日志窗口不可用。"
+
+msgid "Failed to start agent installation."
+msgstr "启动智能体安装失败。"
diff --git a/luci-app-agentflow/tests/test_agent_install_contract.py b/luci-app-agentflow/tests/test_agent_install_contract.py
new file mode 100644
index 000000000..99391e4a0
--- /dev/null
+++ b/luci-app-agentflow/tests/test_agent_install_contract.py
@@ -0,0 +1,69 @@
+from pathlib import Path
+import unittest
+
+
+APP_DIR = Path(__file__).resolve().parents[1]
+
+
+class AgentInstallContractTest(unittest.TestCase):
+ def read(self, relative):
+ return (APP_DIR / relative).read_text(encoding="utf-8")
+
+ def test_luci_loads_task_window_and_starts_whitelisted_installer(self):
+ makefile = self.read("Makefile")
+ controller = self.read("luasrc/controller/agentflow.lua")
+ status = self.read("luasrc/view/agentflow/status.htm")
+
+ self.assertIn("+luci-lib-taskd", makefile)
+ self.assertIn('<%+tasks/embed%>', status)
+ self.assertIn('