Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# PAUSED: public CE releases stay disabled until the repository-history exposure
# review is closed and a maintainer deliberately removes this workflow guard.
# Build and publish the HugAgentOS Tauri desktop client from the public CE tree.
#
# This workflow belongs to the CE overlay because the main repository excludes
# `.github/**` during derivation. Windows and macOS stage the already-derived
# tracked CE checkout as their local-server payload; the private FULL checkout
# continues to generate that payload with scripts/build_ce.py before it reaches
# this tree.
# tracked CE checkout as their local-server payload.
#
# Required repository secrets:
# TAURI_SIGNING_PRIVATE_KEY
Expand Down Expand Up @@ -34,6 +34,7 @@ concurrency:

jobs:
validate-release:
if: ${{ false }} # Security hold: CE release publication is intentionally paused.
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.desktop_version.outputs.version }}
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
run code, and carry real tasks through to completion.
</p>

<p align="center">
<img src="./assets/poster.png" alt="HugAgentOS capability overview poster" width="100%" />
</p>

<p align="center">
<a href="./README.md">English</a> ·
<a href="./README_CN.md">简体中文</a>
Expand Down
4 changes: 4 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
并持续完成真实任务。
</p>

<p align="center">
<img src="./assets/poster-cn.png" alt="HugAgentOS 功能概览海报" width="100%" />
</p>

<p align="center">
<a href="./README.md">English</a> ·
<a href="./README_CN.md">简体中文</a>
Expand Down
Binary file added assets/poster-cn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/poster.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ HUGAGENT_SERVER_BASE=https://你的后端 npm run dev

> Windows 和 macOS overlay 的 `beforeBuildCommand` 会运行 `scripts/prepare-bundle.mjs`:构建
> 桌面前端、准备 CE 服务树、构建 CE 登录前端,并删除构建期 `node_modules` 后再交给 Tauri
> 打包。FULL 主仓存在
> 打包。源代码仓存在
> `scripts/build_ce.py` 时,脚本正常运行生成器并执行开源边界门禁;公开 CE 仓不含生成器,脚本会先
> 校验根目录 `.hugagent-edition` 为 `ce`,再只复制当前已派生 checkout 中的 Git tracked 文件。
> Linux 仍只构建桌面前端,不携带 Windows 本机服务载荷;dev 模式从仓库内
Expand Down Expand Up @@ -143,7 +143,7 @@ HUGAGENT_SERVER_BASE=https://你的后端 npm run dev
| `resources/server-bootstrap/install-local-server.ps1` | Windows 用户目录内创建 Python 环境并安装随包 CE 服务 |
| `resources/server-bootstrap/install-local-server.sh` | macOS 应用数据目录内准备独立 Python 运行时并安装随包 CE 服务 |
| `scripts/prepare-bundle.mjs` | 发行构建前生成同版本 CE 服务资源和 `desktop-bundle.json` |
| `scripts/ce-payload.mjs` | 在公开 CE 仓校验版本标识并只暂存 tracked tree,FULL 仓仍走生成器 |
| `scripts/ce-payload.mjs` | 在派生 CE 仓校验版本标识并只暂存 tracked tree,源代码仓仍走生成器 |
| `scripts/validate-release-version.mjs` | CI 三平台矩阵启动前校验桌面版本文件与 release tag |
| `src-tauri/capabilities/default.json` | 插件权限(opener / deep-link / notification / global-shortcut / updater) |

Expand Down
17 changes: 8 additions & 9 deletions desktop/scripts/ce-payload.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,23 @@ export function assertDerivedCeRepository(repoRoot) {
}

export function assertCleanRepository(repoRoot) {
// Tauri may touch Cargo.toml while inspecting dependencies. On Windows with
// CRLF conversion, `git status` can report that stat-only rewrite as modified
// even when the normalized content is identical to HEAD. Compare content
// directly so only real staged or unstaged tracked changes block a release.
const result = spawnSync("git", ["diff", "--quiet", "HEAD", "--"], {
// Release payloads require a genuinely clean checkout: staged, unstaged, and
// non-ignored untracked files all block staging. This is deliberately stricter
// than a content-only diff because release inputs must be auditable from HEAD.
const result = spawnSync("git", ["status", "--porcelain", "--untracked-files=all"], {
cwd: repoRoot,
encoding: "utf8",
shell: false,
});
if (result.error) throw result.error;
if (result.status === 1) {
if (result.status !== 0) {
throw new Error(
"Desktop release payloads must be built from a clean Git checkout",
`git status --porcelain failed with exit code ${result.status}`,
);
}
if (result.status !== 0) {
if (result.stdout.trim()) {
throw new Error(
`git diff --quiet HEAD -- failed with exit code ${result.status}`,
"Desktop release payloads must be built from a clean Git checkout",
);
}
}
Expand Down
14 changes: 14 additions & 0 deletions desktop/scripts/ce-payload.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ test("rejects release staging when a tracked change is staged", () => {
}
});

test("rejects release staging when a non-ignored untracked file exists", () => {
const root = createCeFixture();
const output = join(root, "desktop", "generated", "server-ce");
try {
writeFileSync(join(root, "stray-source.py"), 'print("untracked")\n');
assert.throws(
() => stageTrackedCeRepository(root, output, { requireClean: true }),
/clean Git checkout/,
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("rejects fallback staging without the derived CE marker", () => {
const root = mkdtempSync(join(tmpdir(), "hugagent-not-ce-"));
try {
Expand Down
2 changes: 1 addition & 1 deletion desktop/scripts/prepare-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ if (existsSync(ceBuilder)) {
if (!requireClean) ceArgs.push("--allow-dirty");

console.log(
"[desktop] Generating the CE server payload from the FULL source tree",
"[desktop] Generating the CE server payload from the source checkout",
);
rmSync(generatedRoot, {
recursive: true,
Expand Down
5 changes: 4 additions & 1 deletion desktop/src-tauri/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ pub async fn redeem(
return Err(format!("换票失败: HTTP {}", resp.status()));
}

let body: serde_json::Value = resp.json().await.map_err(|e| format!("响应解析失败: {e}"))?;
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| format!("响应解析失败: {e}"))?;
// 后端统一信封 { code, message, data: { token, cookie_name, expires_at } }
let token = body
.get("data")
Expand Down
12 changes: 10 additions & 2 deletions desktop/src-tauri/src/notify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ pub fn start(app: AppHandle, port: u16, token: Arc<RwLock<Option<String>>>, http
}

let status = it.get("status").and_then(|v| v.as_str()).unwrap_or("");
let name = it.get("task_name").and_then(|v| v.as_str()).unwrap_or("任务");
let name = it
.get("task_name")
.and_then(|v| v.as_str())
.unwrap_or("任务");
let summary = it.get("summary").and_then(|v| v.as_str()).unwrap_or("");

let title = if status == "failed" {
Expand All @@ -83,7 +86,12 @@ pub fn start(app: AppHandle, port: u16, token: Arc<RwLock<Option<String>>>, http
format!("{}:{}", name, summary)
};

let _ = app.notification().builder().title(title).body(body_text).show();
let _ = app
.notification()
.builder()
.title(title)
.body(body_text)
.show();
}

if seen.len() > SEEN_CAP {
Expand Down
8 changes: 4 additions & 4 deletions document/en/api/error-codes.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Error Code Reference

> Last updated: 2026-06-11
> Last updated: 2026-07-22

This document reflects what is actually implemented in code: business exception classes are defined in `src/backend/core/infra/exceptions.py` (the two license-related ones live in `src/backend/core/licensing/features.py`) and are converted into the [unified response envelope](overview.md#unified-response-envelope) by the global exception handler `src/backend/api/middleware/error_handler.py`. Only **implemented** error codes are listed here; unlisted positions within each range are reserved for future use.
This document reflects what is actually implemented in code: business exception classes are defined in `src/backend/core/infra/exceptions.py` (the two EE license-related ones live in `src/backend/edition_ee/licensing/features.py`) and are converted into the [unified response envelope](overview.md#unified-response-envelope) by the global exception handler `src/backend/api/middleware/error_handler.py`. Only **implemented** error codes are listed here; unlisted positions within each range are reserved for future use.

## Error Response Shape

Expand Down Expand Up @@ -108,7 +108,7 @@ A mismatched `require_admin` / `require_config` token is even simpler: just `{"d

## Unlicensed Features (HTTP 402)

EE routes are mounted with license feature guards per the registry in `api/routes/v1/__init__.py` (`core/licensing/deps.py` → `requires_feature`). When a feature is not licensed, `FeatureNotLicensed` is raised and rendered by `error_handler` as:
EE routes are mounted with license feature guards per the registry in `edition_ee/routes/registry.py` (`edition_ee/licensing/deps.py` → `requires_feature`). When a feature is not licensed, `FeatureNotLicensed` is raised and rendered by `error_handler` as:

```json
{
Expand All @@ -120,7 +120,7 @@ EE routes are mounted with license feature guards per the registry in `api/route
}
```

Design notes (`core/licensing/features.py`):
Design notes (`edition_ee/licensing/features.py`):

- `FeatureNotLicensed` is the **single source** of the 402 envelope; routes/services must never hand-roll `HTTPException(402)`.
- 402 was chosen over 403 deliberately: the frontend treats 403 as session expiry and forces a logout, and a missing license must not log the user out.
Expand Down
10 changes: 5 additions & 5 deletions document/en/api/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,11 @@ The last column is the license feature flag declared in `EE_ROUTERS`; `—` mean
| Observability & audit | `admin_logs.py` | `/v1/admin/logs` | `GET /tools`, `GET /subagents`, `GET /trace/{trace_id}` | CONFIG | `audit` |
| Login & session | `auth.py` | `/v1/auth` | `POST /ticket/exchange` (SSO ticket → session), `GET /session/check`, `POST /logout` | Public (session infrastructure) | — |
| Config console | `config_verify.py` | `/v1/config` | `GET /verify` (CONFIG_TOKEN validation) | CONFIG | — |
| Config console | `config_license.py` | `/v1/config/license` | `GET /` (license details), `POST /` (replace license) | CONFIG | — |
| Multi-tenancy | `config_users.py` | `/v1/config/users` | `GET /`, `PATCH /{user_id}/status`, `POST /{user_id}/reset-password` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `config_teams.py` | `/v1/config/teams` | `GET/POST /`, `POST /{team_id}/members` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `config_invites.py` | `/v1/config/invite-codes` | `GET/POST /`, `POST /{code}/revoke` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `team_files.py` | `/v1/my-teams`, `/v1/teams`, `/v1/artifacts` | `GET /my-teams`, `POST /teams/{id}/files/upload`, `POST /artifacts/{id}/move-to-team` | User + team file permission | `multi_tenancy` |
| Config console | `edition_ee/routes/config_license.py` | `/v1/config/license` | `GET /` (license details), `POST /` (replace license) | CONFIG | — |
| Multi-tenancy | `edition_ee/routes/config_users.py` | `/v1/config/users` | `GET /`, `PATCH /{user_id}/status`, `POST /{user_id}/reset-password` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `edition_ee/routes/config_teams.py` | `/v1/config/teams` | `GET/POST /`, `POST /{team_id}/members` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `edition_ee/routes/config_invites.py` | `/v1/config/invite-codes` | `GET/POST /`, `POST /{code}/revoke` | CONFIG | `multi_tenancy` |
| Multi-tenancy | `edition_ee/routes/team_files.py` | `/v1/my-teams`, `/v1/teams`, `/v1/artifacts` | `GET /my-teams`, `POST /teams/{id}/files/upload`, `POST /artifacts/{id}/move-to-team` | User + team file permission | `multi_tenancy` |
| System config | `config_security.py` | `/v1/config/security` | `GET /sandbox/overview`, `GET /audit-logs`, `GET /system-health` | CONFIG | `system_config` |
| System config | `service_configs.py` | `/v1/service-configs` | `GET/PUT /`, `POST /test/{group_key}` (external service connectivity tests) | CONFIG | `system_config` |

Expand Down
19 changes: 11 additions & 8 deletions document/en/architecture/backend.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Backend Architecture

> Last updated: 2026-07-19
> Last updated: 2026-07-22

The backend lives in `src/backend/` and is a cleanly layered FastAPI monolith: the API layer handles only protocol and auth, the orchestration layer turns each chat into a resumable streaming Run, `core/` holds all domain logic, and MCP tools plus the script-execution sidecar run as independent processes. This page walks the stack top-down.

## Top-Level Layout

```
src/backend/
├── api/ # FastAPI app, middleware, 74 v1 route files
├── api/ # FastAPI app, middleware, and CE routes
├── edition_ee/ # EE routes, License, Team/RBAC, ORM, Dify, and service implementations
├── orchestration/ # Chat orchestration: Run executor, workflow, strategy, citations, schedulers
├── core/ # Domain core: 17 submodules (auth/llm/db/ontology/services/...)
├── mcp_servers/ # 10 standalone MCP servers (streamable-http processes)
Expand Down Expand Up @@ -133,10 +134,10 @@ Enterprise Edition (EE): `team_service` / `team_folder_service` / `sso_sync` (te
|---|---|
| `core/chat` | Workflow context assembly (`context.py`), SSE tool-log event construction (`tool_log.py`) |
| `core/content` | Attachment parsing (`file_parser.py`), KB document chunking/keywords/vectorization (`kb_processing.py`), upload validation (`file_validation.py`), artifact reading and summaries (`artifact_reader/refs/summary.py`), content-block import/export (`content_blocks.py`), `svg_fit.py` |
| `core/kb` | Private-KB parsing and parent-child chunking (`kb_parser.py`), Milvus vector store (`kb_vector.py`), Dify external-KB client (`dify_kb.py`; external KB integration is an Enterprise Edition (EE) add-on) |
| `core/kb` / `edition_ee/kb` | Shared private-KB parsing and Milvus vector storage stay in `core/kb`; the Dify client and external retrieval adapter live only in `edition_ee/kb` (EE) |
| `core/artifacts` | Artifact store `store.py`: local / OSS dual mode |
| `core/infra` | Unified responses (`responses.py`), exceptions (`exceptions.py`), structured logging (`logging.py`), rate limiting (`rate_limit.py`), Redis singleton (`redis.py`), metrics (`metrics.py`), background-task registry (`runtime_state.py`), data masking (`data_masking.py`), distillation budget gates (`distillation_budget.py`, Enterprise Edition, EE) |
| `core/licensing` | License facade `manager.py` (GitLab-style offline model: signed file + in-process verification), feature enum `features.py`, FastAPI guard dependency `deps.py`, seat counting `seats.py`; the verification implementation `_ee_verify.py` (Enterprise Edition, EE — replaced by a hard-`False` stub in the CE tree) |
| `edition_ee/licensing` | Feature enums, signature verification, clock guard, seat policy, middleware, and the manager all exist only in EE; the CE tree has no License package and gets a fixed edition probe from its `api/middleware/edition.py` overlay |
| `core/storage` | Storage protocol `protocol.py` + factory `factory.py`; `local.py` (CE), `s3.py` / `oss.py` (Enterprise Edition, EE) |

## orchestration/ — the orchestration layer
Expand Down Expand Up @@ -167,9 +168,11 @@ Enterprise Edition (EE): `team_service` / `team_folder_service` / `sso_sync` (te

### The router registry (CE/EE seam C1)

`api/routes/v1/__init__.py` is the registry shared by both editions: `CE_ROUTERS` (39 entries) register unconditionally; `EE_ROUTERS` (32 entries) each carry a license feature bit enforced by `core/licensing/deps.py` as the second line of defense (the first being that the CE derived tree physically deletes those files). Three entries — `config_verify` / `config_license` / `auth` — are explicitly exempt so that an expired license can still be replaced.
The full repository composes `CE_ROUTERS` in `api/routes/v1/__init__.py` with `EE_ROUTERS` in `edition_ee/routes/registry.py` (currently 36 entries each). EE entries carry a License feature bit enforced by `edition_ee/licensing/deps.py` as the second line of defense (the first is physical removal of EE files from the CE tree); the CE overlay fixes `EE_ROUTERS` to empty. Three entries — `config_verify` / `config_license` / `auth` — are explicitly exempt so login and license replacement remain reachable after expiry.

### Route file groups (74 files under v1)
### Route file groups

CE routes live under `api/routes/v1/`; physically split commercial routes live under `edition_ee/routes/`, while remaining historical admin routes are mounted only by the EE registry and its feature guards.

| Group | Files |
|---|---|
Expand Down Expand Up @@ -197,7 +200,7 @@ Enterprise Edition (EE): `team_service` / `team_folder_service` / `sso_sync` (te
2. **orchestration does orchestration only**: it chains domain services into streaming workflows and never touches the ORM directly;
3. **core/services is the only business entrance**: routes must not bypass the service layer to query `core/db/models` (a handful of read-only fast paths excepted);
4. **Process boundaries are failure boundaries**: MCP, script-runner, and sandboxes all run as separate processes/containers and interact with the backend only through protocol layers;
5. **CE/EE seams stay concentrated**: the router registry, `edition_tables`, `permissions_iface`, and the licensing facade are the four choke points — business code carries no scattered `if edition` branches.
5. **CE/EE seams stay concentrated**: the router registry, `edition_tables`, `permissions_iface`, and edition middleware are the choke points; commercial implementations move under `edition_ee` instead of scattering `if edition` branches through business code.

## Related Source

Expand All @@ -210,6 +213,6 @@ Enterprise Edition (EE): `team_service` / `team_folder_service` / `sso_sync` (te
| Capability catalog | `src/backend/core/config/catalog.py` |
| Sandbox protocol | `src/backend/core/sandbox/protocol.py` |
| Memory pipeline | `src/backend/core/memory/pipeline.py` |
| License facade | `src/backend/core/licensing/manager.py` |
| EE License implementation / CE edition middleware | `src/backend/edition_ee/licensing/`, `src/backend/api/middleware/edition.py` |
| Skill engine | `src/backend/core/agent_skills/loader.py` |
| MCP port table | `src/backend/mcp_servers/_ports.py` |
Loading
Loading