feat: update this installation's own source in one action

An operator running a fork has no honest way to upgrade: upgrade.sh downloads the
Release tarball of one hard-coded third-party repository and rsyncs it over the
tree, so "upgrade" means "overwrite yourself with somebody else's code", and the
safe alternative is to pull by hand and remember the build command.

internal/update drives the remote the directory already tracks: fetch, fast-forward,
go build into .update-staging, atomic rename of the binary (the previous one kept as
cyberstrike-ai.prev), and a .update-state.json so a rollback has a target that is
not guessed at. Refusals are the point - local source edits and a diverged branch
stop with the file names and commit counts in the error rather than being reset to
match, and operator-owned content (roles/skills/tools/agents/data/config.yaml) is put
aside before the merge and restored over it, with what was kept listed in keptContent
instead of quietly dropped.

Consumers: GET/POST /api/system/update* behind update:read / update:apply (mutations
are process-global, so they need a global-scope session); a console page at
#system-update with an apply button that names why it is blocked, a job polled from
the server and a rollback control; the same three actions as -check-update / -update
/ -update-rollback on the binary; OpenAPI entries for all five endpoints. Permission
gating no longer re-enables a control a page disabled for a reason of its own
(data-state-disabled), which every state-disabled, permission-gated button in the
console was subject to. upgrade.sh becomes a thin shell over the same implementation
and only falls back to the tarball path for installs that are not git work trees.

Docs and the zh/EN/RU dictionaries are updated. The package runs 13 tests against
real temporary git repositories; the HTTP layer, RBAC and the console page each
carry their own contract tests.
This commit is contained in:
Sycun committed 2026-10-07 12:07:26 +08:00
1 parent d2084a9481
commit cb8bc6b824
34 files changed
+5225 -112

No files matched your search

+56
View File
@@ -75,6 +75,62 @@ proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
```
## Upgrading
**Prefer the platform's own one-click update** (console Platform management -> One-click update,
`POST /api/system/update/apply`, or `./cyberstrike-ai -update` at a keyboard). It pulls **the remote
this install directory already tracks**: fetch, fast-forward, `go build`, atomic binary swap (the old
binary is kept as `cyberstrike-ai.prev`), while operator content - `roles/ skills/ tools/ agents/
knowledge_base/ data/ config.yaml` - is put aside and restored, and the result names every file it
kept. For a fork this is the only path that does not overwrite you with somebody else's code; when
source is modified locally or the branch has diverged it refuses and says why instead of forcing.
See [the developer guide](developer-guide.md) for the full semantics.
`upgrade.sh` still works and is now a thin shell over that implementation, with two paths decided by
the installation kind:
- **git work tree**: calls `./cyberstrike-ai -update` directly (building
`go build -o cyberstrike-ai ./cmd/server` first when there is no binary yet, and failing with
instructions on installing Go when there is neither). `--check` inspects only, through
`-check-update`.
- **not a git work tree** (Release tarball install): only here does it fall back to downloading a
Release tarball and `rsync --delete`-ing it in. The source repository then comes from
`--repo owner/name` or the `GITHUB_REPO` environment variable, and the built-in default is used only
when neither is given - with a warning naming which repository the code is coming from.
`config.yaml`, `data/`, `tools/`, `roles/`, `skills/`, `agents/`, `venv/` (skippable with
`--no-venv`) and the rollback points `.update-backup/`, `.update-state.json`, `cyberstrike-ai.prev`
are on the rsync keep list, so `--delete` cannot remove them.
Recommended flow either way:
1. Stop the service.
2. Back up `config.yaml`, `data/` and your custom directories.
3. Pull or replace the new code/binary.
4. Keep your own configuration and add fields the new `config.yaml` example introduced.
5. Start the service and check login, model test, tool list and knowledge-base status.
The one-click update performs step 3 (plus the rebuild); 1, 2, 4 and 5 stay operator decisions - a
database migration does not become compatible just because the binary changed. `upgrade.sh` suits
quick upgrades without compatibility risk; in production still back up first. Python dependencies:
`venv/` is created and managed by `run.sh`, and keeping it means an upgrade does not remove it - only
`--no-venv` deletes it and lets `run.sh` reinstall.
## Rollback
The one-click update leaves its own rollback point: `./cyberstrike-ai -update-rollback`, or the button
on the page, goes back to the commit before that update and puts `cyberstrike-ai.prev` back. It runs
only while HEAD is still the commit that update wrote, so work done afterwards is not discarded.
Content that was put aside also survives under `.update-backup/<timestamp>/`.
Beyond that, roll back together:
- The previous binary or code.
- The pre-upgrade `config.yaml`.
- The pre-upgrade `data/`.
If the new version already wrote database schema changes, rolling back the binary alone may not be
enough; restore the whole backup.
## Deployment Decision Table
| Scenario | Recommended setup | Key settings | Avoid |
+76
View File
@@ -35,6 +35,79 @@ go run ./cmd/server --config config.yaml
The frontend is static. Most JS/CSS/template changes only require a browser refresh.
## One-Click Update: Letting an Installation Update Its Own Source
On a deployed machine the routine is "my tree moved forward by a few dozen commits -> bring this
installation up -> rebuild -> swap the binary -> restart". This used to be only `upgrade.sh`, which
pointed at one fixed upstream repository, so for anyone running a fork "upgrade" meant "overwrite
yourself with somebody else's code". The capability now lives in the platform (`internal/update`) and
the console page, the REST API and the CLI share one implementation, so the three surfaces cannot
disagree about why an update was refused.
### Three entry points
| entry | how it is used |
|---|---|
| console | Platform management -> One-click update (`#system-update`). Opening the page reads the local tree only and does not go online; **Check** is what performs the fetch; **Update** starts a job the page polls; the page also carries the "exit after updating" option and a rollback button |
| REST | `GET /api/system/update` (state on disk, no network), `POST /api/system/update/check` (fetch, then report the gap), `POST /api/system/update/apply` (`202` with `job_id`, polled through `GET /api/system/update/job`), `POST /api/system/update/rollback` |
| CLI | `./cyberstrike-ai -check-update`, `./cyberstrike-ai -update`, `./cyberstrike-ai -update-rollback`. The install root is the directory holding `--config`, or the current directory when `--config` was not given |
Permissions are `update:read` (GET) and `update:apply` (the four mutating endpoints). `update:apply`
additionally requires a global (`all` scope) session: one machine has one source tree, and an
`assigned`/`own` session must not be able to move the code everybody else is running.
### Where the code comes from
From **the remote this directory already tracks**; no third-party repository is hardcoded. The remote
is picked from the ones this tree has, in the order `mine`, `origin`, `upstream` (falling back to the
first remote when none of those names exists), and the branch defaults to what the current branch's
`@{upstream}` names (the current branch itself when there is no upstream). `upgrade.sh` is now a thin
shell over the same implementation: when this directory is a git work tree it just calls
`./cyberstrike-ai -update`.
### The four refusals
| situation | reason | meaning and what to do |
|---|---|---|
| product **source** modified locally (a path outside the protected list) | `local_source_edits` | An update is a download, not a conflict decision. The file names are listed; commit or restore them first |
| branch **diverged** from its remote (ahead and behind at once) | `diverged` | That is a merge decision; the counts are reported so you can resolve it by hand |
| no Go toolchain on this box | `no_toolchain` | **The source still updates**; only the binary is not swapped. Install `go` and press update again to finish |
| this directory is not a git work tree | `not_a_repo` (`installed: false` in the status) | A tarball installation has no remote to fast-forward; update it through the release package |
`apply` is asynchronous, so it always answers `202` first and the refusals above arrive in the polled
`job.failure` (`reason` / `message` / `items`). `rollback` is synchronous and maps refusals to status
codes (`local_source_edits`, `diverged`, `no_state`, `moved_since_update`, `no_binary` -> 409, anything
else -> 400). Other failures (`merge_failed`, `build_failed`, `swap_failed`) carry git's or the
compiler's own output; a failed build leaves the previous binary in place while the source has already
moved and the state file is written, so it stays rollback-able.
### How operator content survives
`roles/ skills/ tools/ agents/ knowledge_base/ data/ log/ venv/ config.yaml .env` belong to the
**operator** (`update.Protected` is the judge). Before the merge, only those of those paths the
update actually writes to are copied into `.update-backup/<timestamp>/` and removed from the tree -
your extra directories that upstream never touched are not disturbed - and they are put back
afterwards, which is what "update the code, keep my work" means. The result names every file in
`keptContent` instead of quietly dropping it: where both you and upstream changed the same file, the
version that lands is yours, and the result says so.
### Rollback
`./cyberstrike-ai -update-rollback`, or the button on the page: `git reset --hard` back to the commit
the update came from, the binary kept as `cyberstrike-ai.prev` is put back, and `.update-state.json`
is removed. It refuses when there is no update record (`no_state`), no kept binary (`no_binary`), the
recorded commit is not in this repository (`bad_state`), **HEAD has moved since that update**
(`moved_since_update` - a rollback undoes the update, not the work done after it), or source is
modified locally (`local_source_edits`).
### The two restart cases
The process only stands down (graceful `Shutdown`, then exit 0) when the request explicitly says
`restart: true` **and** a restart hook was wired at startup. Whether it actually comes back is up to a
supervisor (systemd, `run.sh`), and the page says exactly that rather than promising a boot that may
not happen. Otherwise the API only reports `needsRestart` and the old binary keeps running. Asking for
a restart with no hook wired is a `400`, instead of stopping the service and claiming it restarted.
## Adding a Business Module
Do not add only a handler. A complete module usually needs:
@@ -109,3 +182,6 @@ High-value tests:
- OpenAPI: `internal/handler/openapi.go`
- Tool executor: `internal/security/executor.go`
- Skill package: `internal/skillpackage/`
- One-click update: `internal/update/` (status, apply, rollback), HTTP surface in
`internal/handler/update.go`, CLI switches in `cmd/server/update_cli.go`, thin shell in
`upgrade.sh`
+7
View File
@@ -74,6 +74,7 @@ Permissions use `module:action`. Common actions are `read`, `write`, `delete`, a
| WebShell | `webshell:read`, `webshell:write`, `webshell:delete` |
| C2 | `c2:read`, `c2:write`, `c2:delete` |
| MCP | `mcp:read`, `mcp:execute`, `mcp:write`, `mcp:external:execute` |
| One-click update (local source) | `update:read`, `update:apply` |
| Knowledge | `knowledge:read`, `knowledge:write`, `knowledge:delete` |
| Skills | `skills:read`, `skills:write`, `skills:delete` |
| Markdown Agents | `agents:read`, `agents:write`, `agents:delete` |
@@ -99,6 +100,11 @@ Important distinctions:
- `mcp:external:execute` allows Agent calls to external MCP tools and currently also requires `all` scope.
- `fofa:execute` is kept for backward compatibility, but it now protects the Reconnaissance page for FOFA, ZoomEye, Quake, and Shodan searches.
- `mcp:write` manages external MCP configuration; it is separate from external tool execution.
- One-click update is two permissions: `update:read` shows the installed version and the gap against
the remote, while `update:apply` pulls this installation's own repository, rebuilds the platform
binary and swaps it. The mutating side additionally requires the session to hold that permission
with `all` scope - one machine has one source tree, so an `assigned`/`own` session must not be able
to move the code everybody else is running.
- `robot:write` manages robot configuration and the test endpoint. Chatbot conversations use the bound user or configured service account's business permissions.
---
@@ -143,6 +149,7 @@ Some definitions have no owner. Their mutations require the corresponding permis
- Knowledge mutations other than search.
- Global HITL allowlist, reviewer, and audit policy.
- C2 Profile mutations.
- One-click update mutations (`check`/`apply`/`rollback`, i.e. `update:apply`).
- Some global monitor statistics.
---
+27 -2
View File
@@ -179,6 +179,24 @@ SQLite 热备份时最好先停止服务,或至少复制 `*.db`、`*.db-wal`
## 升级
**优先用平台自带的「一键更新」**(控制台「平台管理 → 一键更新」、`POST /api/system/update/apply`、
或键盘上的 `./cyberstrike-ai -update`)。它拉的是**这个安装目录自己跟踪的远端**:fetch → 快进合并 →
`go build` → 原子换二进制(旧的留作 `cyberstrike-ai.prev`),并把 `roles/ skills/ tools/ agents/
knowledge_base/ data/ config.yaml` 等运维者内容先暂存再放回,结果里点名保留了哪些文件。
跑 fork 时这是唯一不会"用别人的代码覆盖自己"的路径;本地源码有改动或分支已分叉时它会拒绝并说明原因,
而不是硬来。细节见 [开发者指南](developer-guide.md) 的「一键更新」一节。
`upgrade.sh` 仍然可用,它现在只是这条实现的薄壳,并按安装形态分两条路:
- **本目录是 git 工作树**:直接调用 `./cyberstrike-ai -update`(还没有二进制时先
`go build -o cyberstrike-ai ./cmd/server` 再更新;两者都没有就明确报错并说明怎么装 Go)。
`--check` 只检查不改动,走 `-check-update`。
- **本目录不是 git 工作树**(Release tarball 安装):才回落到"下载 Release 包 + `rsync --delete`"的老路径。
此时源码仓库取自 `--repo owner/name` 或环境变量 `GITHUB_REPO`,两者都没给才用内置默认值,
并且会打一行警告说明代码正从哪个仓库取。`config.yaml`、`data/`、`tools/`、`roles/`、`skills/`、
`agents/`、`venv/`(`--no-venv` 可关闭)以及回滚点 `.update-backup/`、`.update-state.json`、
`cyberstrike-ai.prev` 都在 rsync 的保留名单里,不会被 `--delete` 顺手删掉。
推荐流程:
1. 停止服务。
@@ -187,11 +205,18 @@ SQLite 热备份时最好先停止服务,或至少复制 `*.db`、`*.db-wal`
4. 保留原配置,按新版 `config.yaml` 示例补新增字段。
5. 启动服务,检查登录、模型测试、工具列表、知识库状态。
仓库提供 `upgrade.sh`,适合无兼容性问题的快速升级;生产环境仍建议先备份再运行。
一键更新只做 3(以及需要时的换二进制),1/2/4/5 仍是运维者的判断:数据库结构变更不会因为二进制换了
就自动兼容。`upgrade.sh` 适合无兼容性问题的快速升级;生产环境仍建议先备份再运行。
Python 依赖方面:`venv/` 由 `run.sh` 自动创建和管理,保留它就不会被升级打掉;用 `--no-venv` 才会删除并由
`run.sh` 重装。
## 回滚
回滚时同时恢复:
一键更新自带回滚点:`./cyberstrike-ai -update-rollback` 或页面上的回滚按钮,会撤到那次更新前的提交并把
`cyberstrike-ai.prev` 换回原位;它只在"HEAD 仍是那次更新写下的提交"时才执行,之后的工作不会被顺手抹掉。
被暂存过的内容也留在 `.update-backup/<时间戳>/` 里。
除此之外,回滚时同时恢复:
- 上一版本二进制或代码。
- 升级前的 `config.yaml`。
+60
View File
@@ -43,6 +43,66 @@ go run ./cmd/server --config config.yaml
4. 如需对外文档,更新 `internal/handler/openapi.go`。
5. 如需前端调用,更新 `web/static/js/`。
## 一键更新:让安装目录更新它自己的源码
部署现场的日常是"我的 fork 又前进了几十个提交 → 把这套安装带上去 → 重新编译 → 换二进制 → 重启"。
这件事以前只有 `upgrade.sh`,而它写死了别人的仓库:对跑 fork 的人来说"升级"等于"用别人的代码覆盖自己"。
现在这个能力做进了平台自己(`internal/update`),页面、REST 与 CLI 三条入口共用同一份实现,
所以它们给出的拒绝理由不可能各说各话。
### 三种入口
| 入口 | 怎么用 |
|---|---|
| 控制台 | 「平台管理 → 一键更新」(`#system-update`)。打开页面只读本机状态、不联网;点「检查更新」才去 fetch;「一键更新」发起任务并轮询进度;页面另有"更新完成后退出进程"勾选与回滚按钮 |
| REST | `GET /api/system/update`(磁盘现状,不联网)、`POST /api/system/update/check`(fetch 后报告差集)、`POST /api/system/update/apply`(`202` 返回 `job_id`,用 `GET /api/system/update/job` 轮询)、`POST /api/system/update/rollback` |
| CLI | `./cyberstrike-ai -check-update`、`./cyberstrike-ai -update`、`./cyberstrike-ai -update-rollback`。安装目录 = `--config` 所在目录,未给 `--config` 时是当前目录 |
权限是 `update:read`(GET)与 `update:apply`(四个写接口)。`update:apply` 还要求会话是 global(`all` scope):
一台机器只有一份源码,`assigned`/`own` scope 的账号不该能移动别人正在跑的代码。
### 它从哪里取代码
从**这个目录自己的远端**,没有任何写死的第三方仓库地址。远端按 `mine → origin → upstream` 的次序在这个目录已有的
remote 里取(都不在时取第一个 remote),分支默认取当前分支 `@{upstream}` 指向的名字(没有 upstream 时用当前分支名)。
`upgrade.sh` 现在只是这条实现的薄壳:本目录是 git 工作树时它直接调 `./cyberstrike-ai -update`。
### 四种拒绝场景
| 场景 | reason | 含义与处理 |
|---|---|---|
| 本地改过**产品源码**(不在 protected 清单里的路径) | `local_source_edits` | 一键更新是"下载",不是"替你决定冲突"。点名文件,先提交或还原再重试 |
| 分支与远端**分叉**(本地既领先又落后) | `diverged` | 那是合并决策,给出 ahead/behind 数字;手工处理后再点 |
| 本机没有 Go 工具链 | `no_toolchain` | **源码照样更新**,只有二进制没换;装好 `go` 再点一次即可补上 |
| 这个目录不是 git 工作树 | `not_a_repo`(状态里 `installed: false`) | tarball 安装的目录没有可快进的远端,按发布包方式更新 |
`apply` 是异步的,所以它总是先回 `202`,上表的拒绝出现在轮询到的 `job.failure`(`reason`/`message`/`items`)里;
`rollback` 是同步的,拒绝直接落到状态码(`local_source_edits`、`diverged`、`no_state`、`moved_since_update`、
`no_binary` → 409,其余 → 400)。其他失败(`merge_failed`、`build_failed`、`swap_failed`)带着 git/go 的原始输出;
编译失败时二进制保持原版本,而源码已移动、状态文件已写好,因此仍然可以回滚。
### 运维者的内容如何被保留
`roles/ skills/ tools/ agents/ knowledge_base/ data/ log/ venv/ config.yaml .env` 是**运维者的内容**,
判定在 `update.Protected`。合并前,只有本次更新**确实会写到**的那些路径被复制到
`.update-backup/<时间戳>/` 并从工作树移开(你多出来的、上游没动的目录连碰都不碰),合并后再原样放回,
于是"更新代码、留下我的工作"成立。结果里的 `keptContent` 逐个点名被保留的文件——绝不静默丢弃:
当上游与你在同一个文件上都有改动,落地的是你的版本,而这件事写在结果里。
### 回滚
`./cyberstrike-ai -update-rollback` 或页面按钮:`git reset --hard` 回到那次更新前的提交,
并把换二进制时留下的 `cyberstrike-ai.prev` 放回原位,随后删除 `.update-state.json`。
它会拒绝的情形:没有更新记录(`no_state`)、没有留下旧二进制(`no_binary`)、记录里的提交在本仓库不存在
(`bad_state`)、**HEAD 自那次更新之后又动过**(`moved_since_update`:回滚只该撤到更新前,不该顺手抹掉之后的工作)、
以及存在本地源码改动(`local_source_edits`)。
### 重启的两种情形
进程只有在请求显式带 `restart: true`、且启动时装配了重启钩子时才会优雅退出(`Shutdown` 后退出码 0)。
能不能被拉起来取决于外部守护(systemd、`run.sh`),页面如实这么写,而不是承诺一次可能不发生的启动;
没装配钩子时要求重启是 `400`,而不是先停掉服务再自称重启了。未重启时接口只报告 `needsRestart`,旧二进制继续跑。
## 数据库
默认 SQLite。新增表或字段时:
+4
View File
@@ -81,6 +81,7 @@ AI 测试角色不是安全授权边界。即使选择了“渗透测试”角
| WebShell | `webshell:read`、`webshell:write`、`webshell:delete` |
| C2 | `c2:read`、`c2:write`、`c2:delete` |
| MCP | `mcp:read`、`mcp:execute`、`mcp:write`、`mcp:external:execute` |
| 一键更新(本机源码) | `update:read`、`update:apply` |
| 知识库 | `knowledge:read`、`knowledge:write`、`knowledge:delete` |
| Skills | `skills:read`、`skills:write`、`skills:delete` |
| Markdown Agents | `agents:read`、`agents:write`、`agents:delete` |
@@ -107,6 +108,8 @@ AI 测试角色不是安全授权边界。即使选择了“渗透测试”角
- `mcp:execute` 用于访问认证后的 MCP HTTP 入口。
- `mcp:external:execute` 用于 Agent 调用外部 MCP 工具,当前还要求该权限的 Scope 为 `all`。
- 管理外部 MCP 配置使用 `mcp:write`,与执行外部工具是两项权限。
- 一键更新是**两项**:`update:read` 看本机安装的版本与远端差集;`update:apply` 拉自己的远端、重编译并换掉
平台二进制。写侧还要求会话是 `all` scope——一台机器一份源码,`assigned`/`own` 不该能移动别人正在跑的代码。
- `robot:write` 管理机器人配置和测试入口;机器人聊天本身使用绑定用户或服务账号的业务权限。
---
@@ -152,6 +155,7 @@ project:write → own
- 知识库写操作(搜索除外)。
- HITL 全局白名单、默认审核方和审计策略。
- C2 Profile 写操作。
- 一键更新的写操作(`check`/`apply`/`rollback`,即 `update:apply`)。
- 部分全局监控统计。
---