Commit Graph
53 Commits
Author SHA1 Message Date
chenchenandClaude Opus 4.8 233f99fd22 docs(integration): retire artifact download model; final product lives only on git
Per the locked-in model: sub mode requires git binding; the final deliverable
exists ONLY in the user's own git repo (clone/pull). During a run HM streams
ONLY run-info (logs, status, work-view). There is no product download —
project_folder / manifest / files / archive(zip) / local-edits-revision are all
retired across both docs.

- spec: header note, sequence diagram, §2 contract table (code product = git_ref),
  §3.0 step13, §3.1 (mandatory git), §3.5/§3.6 (git-only view), §3.7 (git is the
  iterate baseline, no local-edits), §6.4 (deliverable check on git_ref, not files),
  §7 / §8#8 / §10 TODO aligned. HM "artifact" demoted to a delivery/run-info record.
- unified-api: §3 parity note + legacy error codes marked retired (prior commit
  already reworked §5/§6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:54:54 +08:00
chenchenandClaude Opus 4.8 9b1f74265c docs(integration): align §5 product model to mandatory-git sub mode
sub mode requires git binding (else local-only), so the code deliverable
lives solely in the user's own git repo (clone/pull). Demote the legacy
"HM parses text artifact into project_folder (manifest/files/archive)" and
the local-edits revision flow to legacy/back-compat: §5 now = code via git +
artifacts only for non-code (test_report/summary); §6 redirects to git.
Backend endpoints remain for back-compat but new integrations must not use them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:47:48 +08:00
chenchenandClaude Opus 4.8 0afd9cd764 docs(integration): update desktop-unified-api to the corrected sub-mode flow
The client API doc still carried the old flow; aligned it to
heicode-sub-mode-flow-spec.md (which it now references as the authority):
- Top banner: client is Claude-Code-like agentic; HM is the model gateway +
  control plane (no AI/exec); AM is the cloud runtime; sub = offload to cloud.
- display_status reframed: HM judges presence/anti-empty-shell, NOT code
  correctness; correctness = client (runs/tests) + user review (§0, §4).
- Deploy (§7) rewritten: client-executed + mandatory user confirmation; HM only
  hands the credential from Key Vault; the old Manager-side Deploy-Worker model
  is marked deprecated (also in §0.1 flow + §3 route table).
- New §8 Resource binding (git/vm/db/blob -> KV) with the /api/resources API and
  the pending V2 client-facing read/credential endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:36:54 +08:00
chenchenandClaude Opus 4.8 0eb754b1e1 refactor(web): Overview usage reuses WalletStatsCard (same as 模型与余额)
The wallet page already renders Current Balance / Total Usage / API Requests
from /api/user/self. Reuse that exact component (WalletStatsCard) on the
Overview instead of a hand-written block — DRY, identical numbers
($160.47 / $39.53 / 521 for the current user).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:08:45 +08:00
chenchenandClaude Opus 4.8 42883dac63 fix(web): Overview usage shows the CURRENT user own consumption, not global
The reused LogStatCards aggregates ALL users for admin accounts, so an admin
(root) saw gateway-wide totals (e.g. $628 / 339M tokens / 5218 req) instead of
their own. And per-user windowed quota-dates were empty (0). Replaced with the
user's own lifetime usage from /api/user/self (used_quota = spend, quota =
remaining, request_count), formatted with formatQuota. Real, non-zero, per-user.
(LogStatCards also gained an optional userScope prop for future per-user use.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:04:54 +08:00
chenchenandClaude Opus 4.8 448571496d feat(web): add usage & cost (user consumption) to the Overview dashboard
Reuse the existing LogStatCards (requests / quota=cost / tokens=consumption,
all props optional) on the Overview cockpit — no new data/logic, just renders
the current user usage stats under a "Usage & cost" section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:40:29 +08:00
chenchenandClaude Opus 4.8 8710de7c20 feat(web): resource binding page in the Manager console (5 types)
Re-adds Resource binding to the Heicode cockpit (/resources). Add bindings for
GitHub / Gitea (type=git + provider), VM (ssh), database, blob; non-secret
fields go to metadata, the credential is written to Azure Key Vault via
POST /api/resources/:id/secret and only the secret_ref is shown (masked).
List + unbind. Backend already supported this; now functional since KV is up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:39:28 +08:00
chenchenandClaude Opus 4.8 ae251cf70e feat(resource): add vm/database/blob binding types; KV now provisioned
Resource binding backend (CRUD + secret-to-KV) is now fully functional after
provisioning the Key Vault. Added vm/database/blob to allowedResourceTypes for
the desktop binding set (gitea/github use type=git + provider). git binding +
KV secret write verified end-to-end (secret lands in heicode-kv).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:31:47 +08:00
chenchenandClaude Opus 4.8 8329a323b0 refactor(web): Overview is now a dashboard, drop the idea-capture hero
Per direction: 概述 should be a 看板, not a "turn an idea into shippable
software" marketing surface.
- Removed the idea-hero (home-hero.tsx: IdeaInput + mcp-fed ContinueTasks/
  TodayFocus/HelperEntries, which were often empty/unreachable).
- Overview now renders the live metrics dashboard (CockpitView, previously
  dead code): Running / Completed / Failed / Pending counts by display_status
  + recent runs, fed by the working listAgentDeployments.
- Dropped the marketing "Quick actions / Control plane online" tier.
- Subtitle: "From an idea to shippable software." -> "Live overview of your
  Agent runs." Added zh translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:22:44 +08:00
chenchenandClaude Opus 4.8 6f76162126 docs(integration): correctness is judged by client+user, not HM (fix §6)
HM has no AI and must not compile/test/judge code correctness — that was an
overreach. Reframed §6: HM only relays AM execution status + the FACT of
whether an artifact exists (anti-empty-shell), never a quality/correctness
verdict. The desktop client (Claude-Code-like) pulls the code/git, runs and
tests it, and the user reviews — that is where "is it correct/valid/what I
wanted" is decided. AM test results are a test_report artifact for the client
to read, NOT a signal fed to HM for judging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:21:01 +08:00
chenchenandClaude Opus 4.8 201a64cf35 docs(integration): spell out the limits of HM display_status judging (§6.6)
Answer "how does HM know AM output is real/valid": honestly, it does not — HM
has no AI and never reads/runs code. It only checks structural signals
(non-fallback, non-summary, delivery-type artifact with file changes), which
rules out fake/empty completions but does NOT guarantee correctness and can be
gamed by AM mislabeling. To upgrade "has something" to "really valid" needs:
(1) AM reports a verification block (build/tests/lint) that HM folds into the
verdict, (2) acceptance_criteria self-check, (3) final user review. Added the
verification block to the AM TODO as the key missing signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:16:13 +08:00
chenchenandClaude Opus 4.8 c19d04555c docs(integration): re-plan sub-mode flow now that the client is itself agentic
Re-examined the whole spec under the corrected model (client = Claude-Code-
like agentic program, not a shell):
- New §0.2: what sub mode IS and when to use it — local mode (client runs an
  agent on the user machine) vs sub mode (offload a multi-agent job to cloud
  AM). Client in sub mode = initiator + work viewer + local artifact/deploy.
- §2: added the model-call channel that was entirely missing — both client and
  AM call HM /v1/* for models (a parallel channel to the task control plane).
- §3.7: iteration now has two paths — (A) send back to cloud AM, or (B) git
  pull and fix locally with the client own agent (reuses local capability).
- Scope + client TODO updated to reflect the client agentic role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:06:47 +08:00
chenchenandClaude Opus 4.8 5b0bf438e4 docs(integration): fix core architecture framing — client is Claude-Code-like; HM is the model gateway
Correct a fundamental mislabel: the desktop client is an agentic coding
program (like Claude Code) that runs agents and executes commands locally —
not a dumb shell. HM is the model-call gateway (new-api /v1/*) that BOTH the
desktop client and agent_management call to use models; HM does not run agents
itself. AI = the model, served by HM to both the local client and the cloud
AM runtime. sub mode = offloading a multi-agent job to the cloud AM (vs the
client running locally), with AM agents calling HM /v1/* for models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:01:08 +08:00
chenchenandClaude Opus 4.8 0ee7623aec docs(integration): sub-mode spec — disambiguate HM vs AM, full display_status, explicit flow
Address review feedback for three-team clarity:
- Terminology nailed: HM = Heicode Manager (Go gateway, NO AI, never executes,
  never touches a VM); AM = agent_management (the "Agent Manager" runtime, the
  one with AI that runs agents). Removed all ambiguous bare "Manager".
- Capability boundary table: who has AI / who executes commands / who connects
  the VM. Spells out that HM cannot deploy or read VM logs — deploy is run by
  the client (user-confirmed, short-lived creds from HM); code execution is AM.
- §3.0 explicit step-by-step execution flow (17 steps, each naming HM/AM/client).
- §6 full display_status definition: enum, judging algorithm, real-vs-fake
  artifact rules, success criterion — so all three teams interpret it the same.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:56:15 +08:00
chenchenandClaude Opus 4.8 7c55b6d9f7 docs(integration): sub-mode spec — lock git/deploy decisions + add edge-case analysis
- git = user-bound own repo (github/gitea by URL); auth = fine-grained PAT
  (universal, paste a token) with SSH deploy key fallback.
- deploy MUST be client-executed with mandatory user confirmation; Manager
  only issues short-lived encrypted credentials + audits.
- git executed by agent_management with an injected short-lived PAT; Manager
  records refs only; agents push to delivery/PR branch, not main.
- New section 9: additional details to settle before v1 (repo state, secret
  hygiene, budget/cancel/crash handling, acceptance, concurrency, provider
  limits, deploy confirm/rollback, work-view UX).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:38:36 +08:00
chenchenandClaude Opus 4.8 c10f6d2f6b docs(integration): sub-mode end-to-end flow + three-end spec (v0.1 draft)
Authoritative flow/spec aligning desktop client <-> Heicode Manager <->
agent_management for the sub mode: encrypt/auth, dispatch, streamed agent
work view, git-per-agent + merge into one deliverable, review/redo, deploy
via Manager-held credentials, and the resource-binding spec (git/vm/db/blob
-> Key Vault). Marks each item 已实现/部分/待建/待决策 with recommendations,
and lists per-end TODOs. Open decisions in section 8 for sign-off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:25:08 +08:00
chenchenandClaude Opus 4.8 5d7854d9ac fix(heicode): mode-filter task lists, hard-delete, mode-aware Agent runs page
- /api/heicode/sub-agile/tasks now returns only sub_agile, /swarm only swarm
  (both previously returned every deployment regardless of mode).
- DELETE /tasks/{id} now hard-deletes the record (best-effort runtime stop +
  remove row + in-memory entry) so a deleted task disappears from the list,
  instead of only marking it stopped.
- Admin runs page: renamed Task overview -> Agent runs, cards show the mode
  (Sub Agile / Swarm) chip, added a mode filter; status already uses
  display_status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:11:39 +08:00
chenchenandClaude Opus 4.8 8b28c180c7 refactor(web): remove legacy/non-functional admin console pages, align to current product
The Manager console carried screens built to an early control-plane vision
that no longer matches how the product runs (desktop client drives tasks;
Manager is gateway + status judge). Removed the dead/misleading ones and
aligned task overview to the real status model. Frontend only; no backend
endpoints touched.

Deleted (routes + pages + menu entries):
- Resource binding (/sk-sources): mcp-server /api/resources unwired (301) +
  Azure Key Vault unreachable -> page was inert.
- Audit (/audit): only simulated approvals, empty leases, mcp audit unwired.
- Events / Templates / Agents pages: legacy control-plane (hardcoded mock
  templates), not in the main menu but route-reachable.
- azure-cloud-binding-sheet + create-agent-deployment-sheet (New run).

Task overview (/deployments) kept and fixed:
- status now uses Manager-judged display_status (completed / needs_codegen /
  completed_without_deliverable=fail / running ...) instead of raw phase, so
  success vs failure is legible.
- dropped New run, scope/budget/secret_ref pills, permission-manifest grants
  table, Simulate; kept the task list, per-task audit timeline and artifacts.

Cleaned all menu/nav/dashboard references to the deleted routes (sidebar,
top-nav, footer, cockpit, home-hero, agent-hub, task-card-view) and
regenerated routeTree.gen.ts. tsc -b and rsbuild build both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:39:49 +08:00
chenchenandClaude Opus 4.8 6fd294635f fix(swarm): align Manager swarm-runtime default routes to HeiCode-Swarm standard
Per the HeiCode-Swarm 蜂群对接文档 §3.1, the swarm runtime standard routes are
/api/agent/swarm/* (legacy /api/swarms/* still works but is no longer default).
Swarm-only change; sub-agile (AGENT_RUNTIME_*) defaults and the agent_management
wiring are untouched.

- SWARM_RUNTIME default create/stop/approval -> /api/agent/swarm/deployments[...]
- swarm status: the runtime has no /status subpath, so status is read from the
  deployment detail endpoint GET /api/agent/swarm/deployments/{deployment_id};
  StatusPath/ArtifactContentPath defaults made mode-aware (sub keeps legacy).
- update the swarm HTTP smoke test to assert the standard runtime paths.

Verified reachable: HeiCode-Swarm at 52.139.240.116:8000 returns the standard
POST /api/agent/swarm/deployments (401 needs token, endpoint exists).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:45:00 +08:00
chenchenandClaude Opus 4.8 78048858b9 docs(heicode): add sub-agile end-to-end demo walkthrough for client cross-check
Step-by-step walkthrough of a real client-simulated run (task dep_39e53ee4c692)
with actual request/response at each stage: create -> poll workflow -> artifacts
-> manifest -> files -> archive. Confirms agent_management delivers real code
files + nested folders (backend/main.py, backend/models.py) surfaced as a
project_folder. Includes a client self-check list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:47:53 +08:00
chenchenandClaude Opus 4.8 b4c3104dda docs(heicode): document sub-mode workflow/list rich fields (gap A/B/D/F)
Workflow now documents top-level mode (sub_agile|swarm) + sub_mode, phases[],
per-agent tokens/tools/elapsed_seconds/artifact_ids, and metrics/aggregates,
with a note that per-agent metrics + artifact source role await
agent_management runtime support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:25:28 +08:00
chenchenandClaude Opus 4.8 65ce549fb2 fix(heicode): sub-mode workflow/list gaps from desktop client (A/B/D/F)
Address the desktop client sub-mode gap report:
- B: /tasks list items now carry Manager-judged display_status (were raw
  status, so completed could really be needs_codegen).
- D: list + detail expose top-level mode (sub_agile|swarm) derived from the
  runtime mode, so a task recovered from /list after restart routes to the
  right mode instead of defaulting to sub-agile.
- F: unify mode naming on sub_agile everywhere (capabilities/list/detail/
  workflow); workflow keeps sub_mode for the agile/waterfall cadence.
- A: /workflow gains phases[], per-agent tokens/tools/elapsed_seconds/
  artifact_ids, and top-level metrics + aggregates, filled from runtime
  status diagnostics. Per-agent tokens/tools, artifact source role and phase
  breakdown surface as 0/[] until agent_management reports them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:20:38 +08:00
chenchenandClaude Opus 4.8 fd4ea0335b docs(heicode): finalize client integration doc for gap-analysis closure
- Add 2026-06-02 changelog summarizing P0/P1 closure at the top.
- New error codes: ARTIFACT_ARCHIVE_NOT_READY/_FAILED, FILE_PATH_REQUIRED,
  with retryable column and the unified error envelope note.
- §9 auth bullet now reflects encrypted-body writes + no-body signed GET.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:04:54 +08:00
chenchenandClaude Opus 4.8 856c184421 fix(heicode): order accepted-revision lookup by real column created_at_ms
LatestAcceptedRevisionForDeployment ordered by a non-existent column
(created_at), so the query errored and /messages and /execute never picked
up the latest accepted local-edit revision. Found via live VM smoke test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:57:27 +08:00
chenchenandClaude Opus 4.8 2613c5763c fix(heicode): close client gap-analysis P0/P1 items for unified API
Address the desktop client team gap analysis on the unified /api/heicode/*
surface:

- P0-1 GET device auth: no-body V2 signed-GET path (fetch forbids GET body),
  same Ed25519 canonical with empty-body hash; UserOrV2DeviceAuth +
  OptionalV2DeviceAuth dispatch on signature headers. Unit test added.
- P0-2 approval inbox: GET .../tasks/{id}/approvals?status=pending.
- P0-3 project_folder: artifacts list normalizes the primary code deliverable
  to display_artifact_type=project_folder + is_project + manifest/files/
  archive/revisions subpaths.
- P0-4 archive contract: real application/zip + Content-Disposition +
  Content-Length; ARTIFACT_ARCHIVE_NOT_READY (retryable) when no files yet.
- P1-1 file path: GET .../files?path=<url-encoded> (no segment ambiguity).
- P1-2/P1-3 revision: local edits stored as accepted baseline; /messages and
  /execute consume the latest accepted revision (-> applied), return
  active_project_revision.
- P1-4 doc: Swarm same-shape routes stated explicitly.

Doc updated to match. Build + middleware/controller tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:48:03 +08:00
chenchenandClaude Opus 4.8 f903fd0c19 docs(integration): self-sufficient unified API doc for desktop client
以客户端开发视角通读后补齐缺口,使客户端可仅凭本文对接:
- §0.1 推荐完整流程(capabilities->create->poll workflow->artifacts->manifest/files/archive)
- §3.1 创建响应(deployment_id=task_id 来源)+ 列表响应
- §5 artifacts 列表响应
- §6 local-edit 单/批量请求 + 成功 + 冲突响应示例
- §7 云部署请求/响应示例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:45:18 +08:00
chenchenandClaude Opus 4.8 0f1d3d2009 feat(agent): three-layer status on workflow, error retryable, V2/envelope doc alignment
按桌面客户端 spec 重审后补齐:
- workflow 端点补三层状态(cloud_deployment_status/runtime_execution_status/display_status/
  conversation_id/last_synced_at,§10.2),作为客户端右侧面板状态源。
- agentError 补 error.retryable(§14)。
- 文档:明确 V2 body 加密与模型调用一致(directive)、真实响应 envelope、轮询刷新(无 SSE,
  按 §17.3 兜底)、需求包创建 + messages/execute/delete。

V2 加密链核验:新 /api/heicode/* 全走 UserOrV2DeviceAuth(同模型调用),
heicode-aead-v1/ChaCha20/Ed25519 常量未被改名误伤,中间件测试通过——加密未丢失。

go build ./... + go test 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:37:05 +08:00
chenchenandClaude Opus 4.8 7f3806c510 feat(agent): client requirement-package create + messages/execute/delete (spec §5.1/§8)
补齐桌面客户端对接缺口:之前 POST /tasks 只收 orchestration_plan、且缺 messages/execute/delete,
客户端无法照 spec §8 需求包直接对接。本次:

- POST /api/heicode/{sub-agile,swarm}/tasks 改收需求包 {mode,conversation_id,requirement,model_selection,roles},
  Manager 翻译成 orchestration_plan(per_role/default/primary 模型),仍兼容直传 orchestration_plan。
- POST .../tasks/{id}/messages:持续对话,记录用户消息 + 回当前 display_status。
- POST .../tasks/{id}/execute:确保派发 Runtime + reconcile。
- DELETE .../tasks/{id}:停止任务。
- 重构 createAgentDeploymentFromPlan,供原始 plan 与需求包两条路径复用。
- 更新 docs/integration/heicode-desktop-unified-api.md(创建改需求包 + 新路由)。

go build ./... + go test(controller/router)全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:26:57 +08:00
chenchenandClaude Opus 4.8 56eef58b3c feat(agent): project-folder artifacts, local-edit revisions, cloud-deploy control plane
完成统一方案 v0.1 剩余客户端要求(#10/#11/#12)+ 对接文档。

- #10 项目文件夹产物(§12):Manager 解析 runtime 的 markdown 多文件 artifact 成项目文件树,
  新增 .../artifacts/{id}/manifest、/files/{path}、/archive 三接口(按需解析,zip 打包)。
- #11 本地修改 revision 协议(§12.7):新模型 AgentArtifactRevision + 迁移;
  .../local-edits、/local-edits/batch、/revisions;base_revision 冲突检测返回
  ARTIFACT_REVISION_CONFLICT;Manager 持有 accepted 基线,回调 Runtime(审计事件)。
- #12 云部署控制面(§18):新模型 AgentCloudDeployment + 迁移;
  GET /api/heicode/deployment-targets;.../tasks/{id}/deployments(创建/列表);
  生产环境进 waiting_approval;客户端只传 resource_binding_id(禁 inline secret);
  真实云执行留 executor=pending_worker,等 Deploy Worker 接入。
- 文档:新增 docs/integration/heicode-desktop-unified-api.md(取代旧 sub-agile 文档,
  覆盖 capabilities/统一任务路由/display_status/项目文件夹/本地修改/云部署/客户端约束/错误码)。

验证:go build ./... + go test(controller/router/model/middleware)全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:23:02 +08:00
chenchenandClaude Opus 4.8 443f552917 fix(agent): reconcile deployment from runtime status on read
真实生产模拟暴露:runtime 完成极快时,artifact.created/status callback 会与
runtime_swarm_id 映射的异步持久化竞态——callback 先到、按 swarm_id 匹配不到
deployment 而成孤儿,导致 deployment 永远停在 accepted、/artifacts 为空,
尽管 runtime 已 completed 且可直查到。

修复:detail / timeline / workflow / artifacts 读取时,对非终态且有 runtime
映射的 deployment 调 reconcileDeploymentFromRuntime——复用 diagnostics 的
runtime 状态直查,把终态 + 真实 artifact 反写落库,不再单依赖会丢/会竞态的 callback。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:05:43 +08:00
chenchenandClaude Opus 4.8 0fe1d20d67 feat(agent): unify agnet→agent and implement client/runtime unification spec v0.1 core
按桌面客户端统一方案 v0.1 + agent_management Sub Mode Runtime 对接,强制全量统一,不留兼容。

命名统一(强制,无兼容):
- 全仓 agnet/Agnet/AGNET → agent/Agent/AGENT:后端 Go(路由 /api/agent/*、env AGENT_*、
  结构体/函数、19 个文件改名)、前端(agent-console/agent-hub、/api/agent 调用、i18n)、
  DB(表 agent_*、列 agent_id)、compose/.env、文档、脚本。
- DB 加幂等迁移 renameAgnetTablesToAgent():启动时 rename 老 agnet_* 表/列,保住生产数据。

统一方案核心(10 项):
- callback 统一 /api/agent/callbacks/runtime-events(路由/广播URL/函数名)。
- artifact 兜底判定改用 Runtime 权威信号 metadata.synthesized(§7.2)+ 结构化 artifact_type。
- Manager→Runtime 路径对齐 /api/agent/sub-agile/deployments(§2.2),{deployment_id} 回退 swarm_id。
- 状态裁决 display_status:Manager 唯一裁判,completed 无有效产物→needs_codegen/
  completed_without_deliverable(§10.6),接入 detail/timeline/workflow。
- GET /api/heicode/capabilities 能力发现(§6)。
- 模型策略 per_role(role_models)+ 收集 allowed_model_ids(§9)。
- resource_binding_id→secret_ref 服务端解析,客户端不再 inline secret_ref(§17.6)。
- 客户端统一路由层 /api/heicode/sub-agile|swarm/*(task≡deployment,复用控制面)+ workflow 投影。
- 日志分层 user_logs/debug_logs(§13)。

验证:go build ./... + go test(controller/router/model/middleware)全绿;前端 tsc -b + rsbuild build 通过。
待部署:VM .env 的 AGNET_*→AGENT_*;启动迁移自动 rename 表;其他三仓库需同步切到 /api/agent。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 23:45:10 +08:00
chenchenandClaude Opus 4.8 12602ebcd6 fix(agnet): structured deliverable judgment, secret_ref validation, single default model
代码评审(2026-06-01 全链路报告)中 Manager 侧自主可修项:

- P2 交付物判定: runtimeArtifactsAreSummaryOnly 改为优先读结构化字段
  (artifact_type + files_modified 信号),修复 Runtime 新 uri scheme 与
  artifact_type=document 被旧 /artifacts/summary 字符串启发式漏判的回归。
- P6a: Resource CRUD(normalizeResourcePayload)强制 secret_ref 必须 azkv://,
  与 agnet 部署/审批路径一致,堵住直写任意 secret_ref 的旁路。
- P6b: 明文密钥检测从仅按字段名升级为同时扫字符串值(sk-/ghp_/AKIA/JWT/PEM
  等高置信模式),containsPlaintextSecret 与 containsSensitiveGrantField 均覆盖。
- P5 默认模型收敛: 新增单一来源 defaultAgnetModelID()(env AGNET_DEFAULT_MODEL_ID,
  默认生产已验证的 gpt-5.4);移除 draft 构造器两处 agnet-model-<role> 占位回退
  (生产 NewAPI "No available channel" 根因)与角色模板硬编码 claude-* 默认。
- P3 文档: 对接文档状态枚举补 completed 终态、runtime_state 镜像说明与未知值兜底;
  role-templates 示例占位名改为 gpt-5.4。

新增 controller/agnet_deliverable_secret_test.go 覆盖以上行为。
go build ./... 与 go test ./controller/ 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:33:32 +08:00
chenchen 23ec354eb5 更新 2026-05-25 16:03:00 +08:00
chenchenandClaude Opus 4.7 da81c57db2 feat(cloud): Azure subscription binding via Service Principal (M2 phase 1)
Sprint 13. Lifts the resource-binding wizard's "Connect cloud account"
step from a disabled "Coming soon" button to a real binding flow,
scoped to Azure for now (AWS / GCP coming soon).

What ships:
  - New AzureCloudBindingSheet — Service Principal credentials form
    (subscription_id / tenant_id / client_id / client_secret + display
    name)
  - Creates a mcp-server ResourceBinding of type 'cloud_account' with
    provider=azure metadata, permission_scope=['azure:read'], status
    flips between 'active' (vault configured) and 'pending' (vault
    not yet wired)
  - Sheet shows a yellow warning when OpenBao isn't configured,
    explaining that client_secret will NOT be persisted server-side
    until vault is online — operators re-enter or rotate the secret
    once vault is up
  - Explicit "what Heicode will / will not do" footer card per
    product docs §13.9 — read-only ARM, never modify without desktop
    approval, never log client_secret
  - Cloud step "Connect" button now opens this sheet (was disabled)
  - Wizard summary description updated: AWS/GCP labelled coming soon
    instead of implying all three providers ship today

Phase 2 (Azure OAuth code flow) + phase 3 (ARM auto-discovery) need
Azure App Registration + OpenBao online first — separate sprints.

i18n localized en + zh.

Verification:
  - tsc --noEmit clean
  - no backend change — leverages existing mcp-server ResourceBinding
    endpoint which already supports cloud_account type

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:53:34 +08:00
chenchenandClaude Opus 4.7 cdcc864298 feat(states): unified QueryState wrapper covers permission + network errors
Sprint 11 — closes M13 ("七态完整性") on the three highest-traffic
pages (Deployments, Events, Audit). Earlier behaviour was binary
(loading skeleton OR card list); 401/403 and offline both fell
through to a toast or a blank page.

New /components/query-state.tsx:
  - One wrapper that takes isLoading + error + isEmpty + retry
  - classifyError reads axios + fetch shapes; maps to 'permission'
    / 'network' / 'generic'
  - Permission state: ShieldOff icon, no retry button (admins must
    grant access; retry would just 401 again)
  - Network state: WifiOff icon, retry wired
  - Generic state: AlertTriangle + retry + decoded error.message
  - Empty state: passthrough to existing EmptyState
  - Loading: passthrough to caller's loadingFallback or 3 skeleton rows

Applied to:
  - AgnetDeploymentsPage (replaces inline loading/empty ternary)
  - AgnetAuditPage (same)
  - AgnetEventsPage (wraps the events ol)

In all three, the existing useQuery now also surfaces error + refetch.
retry: false set so QueryState handles error display directly
instead of TanStack retrying 3 times silently.

i18n strings added for 5 new copy keys (permission denied, network
unreachable, retry, load failed) in en + zh.

Verification:
  - tsc --noEmit clean
  - no backend change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:48:31 +08:00
chenchenandClaude Opus 4.7 d8f61957f0 feat(manager): version 1.4.2 — H2 sk- hash + M9 audit drawer + M3 docs + M7 vault
Bundled release bumping Manager to 1.4.2 with four product-doc gap
closures lined up in a single deploy.

VERSION:
  - 1.2.0 → 1.4.2 (catches up after Sprints 1-5 shipped under 1.2.0)

H2 — sk- hash phase A (server-side, zero client impact):
  - tokens table: new key_hash varchar(64) index column
  - Token.Insert() dual-writes Key + KeyHash on every new token
  - BackfillTokenKeyHash() runs at startup, batches 500 rows at a
    time, idempotent. Fills legacy rows that pre-date the column
    without blocking app boot
  - 5 unit tests pin: sha256 correctness, dual-write on Insert,
    empty Key → empty hash, backfill behaviour, idempotency
  - Phase B (switch lookup index off plaintext + drop Key column)
    can ship later once telemetry shows key_hash IS NULL count is 0

M9 — task detail drawer with audit timeline:
  - Deployments page click → Sheet drawer with RunDetailPanel +
    new RunAuditTimeline component
  - Timeline pulls from existing /api/agnet/deployments/:id/events
    which Sprint 1 already wired to the persistent
    agnet_audit_events table — no new backend
  - Vertical timeline w/ coloured dots (primary / amber / rose by
    classifyEventLevel), occurred_at + correlation_id per row,
    max-height + overflow for long traces
  - 15s polling; empty/loading/error states all rendered

M3 — project_doc as a first-class binding step:
  - Resource binding wizard split "SK or project docs" into two
    distinct steps: "Connect project docs" + "Connect SK skill packs"
  - Each step's Connect button pre-selects the matching type in
    the advanced sheet so users don't accidentally tag a doc repo
    as Git or SK
  - Summary dialog still receives the combined skOrDocSources view
    to keep the recommendation-card contract unchanged

M7 — secret vault status (admin panel):
  - controller/secret_store.go: new GetSecretStoreStatus handler
    + fetchHealth() method. Hits OpenBao /sys/health (token-less
    upstream endpoint), maps to a sanitized response — NEVER
    returns secret names or values per product docs §13.9
  - Graceful degradation: env vars unset → "not configured" pill;
    network error → "unreachable"; sealed → amber warning; healthy
    → green
  - Mounted at GET /api/secret-store/status behind middleware.AdminAuth
  - New SecretStoreSection in system-settings/maintenance,
    registered before Performance. Read-only card with refresh
    button, 7 status fields, message line, "how to enable" hint

Verification:
  - go vet ./... clean
  - go test ./controller/... ./middleware/... ./model/... all green
  - tsc --noEmit clean
  - Backend M7 endpoint deliberately tolerant — production may not
    have OPENBAO_ADDR set yet, UI shows "not configured" instead of
    500ing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:19:42 +08:00
chenchenandClaude Opus 4.7 3fa345ff5e feat(privacy): per-session consent modal on first authenticated entry
Sprint 5. Pops a non-dismissable modal once per browser tab session
after the user lands on any /_authenticated/* route. Covers two
user-visible policy points the product team called out:

  1. Project data: Heicode does NOT guarantee against project loss.
     Users must back up to their own Git / local storage.
  2. Model privacy: when calling third-party models (OpenAI /
     Anthropic / Google etc.), each vendor's privacy, retention,
     and training-use terms apply. Heicode does NOT modify those
     terms and makes no privacy promises on the vendors' behalf.

Design:
  - Pure frontend, no schema migration, no backend endpoint. The
    "show every login" requirement is satisfied by sessionStorage
    (cleared when the tab closes); persisting acceptance server-
    side would force a forced-consent log we don't need yet.
  - Modal is intentionally non-dismissable (no overlay close, no
    Escape key, no X button). User must explicitly Agree or
    Decline.
  - Decline triggers auth.reset() + redirect to /sign-in — same
    logout path the sidebar uses.
  - Agree button stays disabled until the acknowledgement checkbox
    is ticked.
  - i18n localized en + zh.

If we later need an auditable consent trail (e.g. regulator asks
"prove user X clicked agree on date Y"), promote this to a DB-backed
flow with a users.accepted_privacy_at column and a POST endpoint.
Until then sessionStorage is the right scope.

Verification:
  - tsc --noEmit clean
  - mounted at AuthenticatedLayout — every authenticated route hits
    it; sign-in / sign-up / public pages do not
  - sessionStorage flag survives navigation within a tab, clears on
    tab close — matches "弹一次每次登录" requirement

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:38:58 +08:00
chenchenandClaude Opus 4.7 131639f672 feat(deploy): structured execution-confirm card replaces raw JSON dump
Sprint 4. Closes M6 from the product-doc gap analysis.

Previously the "Review" tab of the new-deployment sheet showed a
JSON.stringify of the full orchestration plan + a single checkbox.
Two problems:
  1. The raw JSON is nearly unreadable to non-engineering users
     (the typical operator of this surface).
  2. Product docs §10 "执行前确认卡" require five named sections,
     not a freeform payload dump:
       - 本次会做 (this run will do)
       - 本次允许使用 (resources this run may use)
       - 本次不会做 (this run will NOT do — static off-limits list)
       - 高危规则 (high-risk policy reminder)
       - 预计消耗 (token / cost / duration triple)

New layout puts each section in its own card pulled directly from
previewPlan so what the user confirms is byte-identical to what
gets submitted. The "本次不会做" card is static red-bordered text:
production deploys, prod DB writes, long-lived credential
extraction, any resource not listed.

The raw JSON is preserved behind a <details> disclosure so power
users debugging the payload still have access. Same checkbox
behaviour gates submit — no policy regression.

All strings localized en + zh.

Verification:
  - tsc --noEmit clean
  - data binds directly to previewPlan — no drift risk between
    preview and actual submit
  - no backend change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:25:22 +08:00
chenchenandClaude Opus 4.7 4bab918808 feat(manifest): collapsible permission-manifest preview on run detail
Sprint 3. Closes M5 from the product-doc gap analysis — gives users
a safe, opt-in view of "which agents got which permissions over which
resources" without exposing plaintext credentials.

Per docs/product-package/10-frontend-detail-spec.md §"执行前确认卡"
and §13.9:
  - Default folded. Only a "Show manifest" button surfaces.
  - When expanded, shows the five policy-safe columns per resource
    grant: resource (type + id), allowed actions, constraints,
    secret_ref, status.
  - secret_ref is rendered via maskSecretRef() which keeps the
    vault path scheme + first 6 chars of the leaf and ellipsises
    the rest. The full plaintext value (if anyone ever puts one
    there by mistake) is NEVER rendered.
  - Constraints render as compact key=value chips, value truncated
    at 24 chars to keep the row scannable.
  - Status pill colors mirror the device-binding active/revoked
    palette established in May.
  - Footer note reminds the reader: "Plaintext credentials are
    never shown. The secret_ref column is a vault pointer, not
    the secret itself."

Existing "Permission manifest" stub card grew the new toggle in
place (no new card column added); the Events / Audit usage cards
stay on the same grid. M5 unblocks Sprint 4 (M6 execution-confirm
card) which depends on the same data layout.

Verification:
  - tsc --noEmit clean
  - go test ./controller/... ./middleware/... ./model/... all green
  - no backend change in this commit — pure frontend work over the
    existing AgnetDeployment payload shape
  - i18n additions for en + zh

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:55:51 +08:00
chenchenandClaude Opus 4.7 d022518a0a feat(agnet): canonical six-role catalog + role-templates API
Sprint 2. Materialises the six platform-recommended Agnet roles
documented in docs/product-package/13-platform-description.md §3.

Backend:
  - controller/agnet_role_template.go (new): AgnetRoleTemplate type
    + canonical catalog returned by GET /api/agnet/role-templates.
    Six roles: product / architect / frontend / backend / reviewer / ops.
    Stored as constants (not DB rows) because they are platform
    contracts, not user-editable data. Each entry carries:
      - stable key (frontend dispatches on this — never rename)
      - display name + summary (translatable)
      - default model recommendation
      - default permission scope hints
      - risk classification (low/medium/high) — Ops alone is high,
        matching the production-deploy-needs-approval rule
  - router/api-router.go: mount GET /api/agnet/role-templates inside
    the existing /api/agnet group (same auth as the other endpoints)
  - controller/agnet_role_template_test.go (new): 4 tests pin the
    six-role set, risk-level matrix, HTTP envelope shape, and the
    closed-set helper that will gate validation later

Frontend:
  - features/agnet-console/api.ts: new AgnetRoleTemplate type + a
    module-level cached listAgnetRoleTemplates() helper. Caching
    means the picker doesn't refetch every time the deployment sheet
    opens.
  - features/agnet-console/create-agnet-deployment-sheet.tsx:
    - Replace free-text role_template Input with a Select bound to
      the catalog; falls back to Input if the catalog is empty so
      the form stays usable when the endpoint is down.
    - Fix two informal role names in built-in presets (debugger →
      reviewer, executor → backend) so presets reference only
      canonical keys.

Verification:
  - go test ./controller/... ./middleware/... ./model/... all green
    (4 new role-template tests + existing suite)
  - frontend tsc --noEmit clean
  - zero touch on the token / device-signature hot paths

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:23:43 +08:00
chenchenandClaude Opus 4.7 8f70115c6a feat(audit): persist Agnet control-plane audit events to DB
Sprint 1 of the post-product-doc gap closure. Replaces the previous
in-process `agnetEvents map[string][]agnetEvent` (lost on every
container restart) with a real GORM table `agnet_audit_events`.

What changed:
  - model/agnet_audit.go (new): AgnetAuditEvent model + InsertAgnetAuditEvent
    / ListAgnetAuditEvents / ListAgnetAuditEventsByDeployment helpers.
    Indexes picked for the dashboard queries: user_id, deployment_id,
    binding_scope, occurred_at desc.
  - model/main.go: AutoMigrate &AgnetAuditEvent{} alongside the existing
    schema (SQLite/MySQL/PostgreSQL compatible per CLAUDE.md Rule 2).
  - controller/agnet_control_plane.go: drop agnetEvents map; the 3
    producer sites (deployment accepted / stop / sk_snapshot_refreshed)
    now call recordAgnetAuditEvent which writes to DB best-effort.
    The 3 reader sites (events list / logs / audit-logs) now query
    the table; AgnetListAuditLogs also supports limit/offset pagination.
  - controller/agnet_control_plane_test.go: reset helper no longer
    touches the deleted map.
  - model/agnet_audit_test.go (new): 4 tests covering persistence,
    nil-guard production safety, filter+paginate, chronological reads.

Sidebar UX:
  - "Preparation checklist" → "Resource binding"
    Per product-package doc README §统一表述 — user-facing term is
    "资源绑定" not "准备清单". URL /sk-sources kept to preserve
    bookmarks; can rename in a later pass with redirect.

Verification:
  - go test ./controller/... ./middleware/... ./model/... all green
  - go vet clean
  - frontend tsc --noEmit clean
  - audit writes are best-effort: errors log via SysLog but never
    fail the user API call; DB nil-guards in place

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:42:34 +08:00
chenchenandClaude Opus 4.7 5081289b65 fix(devices): hide revoked rows from user-facing Devices list
User feedback: "都已经已撤销了为什么还有记录" — once a user clicks
revoke they expect the row gone from the list, not lingering with
a "已撤销" badge. The old behaviour treated the page as a security
audit log, which conflicts with its primary use as an active-device
management surface.

GetUserDeviceBoundTokens now filters `revoked_at = 0`. The row stays
in DB (soft-delete) so:
  - audit trail (RevokedAt / RevokedReason / DeviceLastSeenIp /
    DeviceFingerprint) remains inspectable by admins
  - re-pair from the same physical device still self-heals the row
    via the reactivate branch in PairDevice — covered by existing
    TestPairDevice_RepairAfterRevokeReactivates

If a user wants security audit history in the UI, that should be a
separate "Security activity" page; not the device-management list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:24:20 +08:00
chenchenandClaude Opus 4.7 b48be15d8b fix(http): set TrustedPlatform=Cloudflare so c.ClientIP reads CF-Connecting-IP
Previous SetTrustedProxies commit (407dbb7) was necessary but
insufficient. In production Manager sits behind Cloudflare in
proxy mode, which:
  - strips the inbound X-Forwarded-For header
  - sets CF-Connecting-IP with the real client IP

Gin's default ClientIP() only knows about X-Forwarded-For + X-Real-IP
— it does NOT recognize CF-Connecting-IP. So every request showed the
docker bridge peer (10.2.3.4) in audit fields and rate-limit buckets
even after we added private ranges to TrustedProxies.

Setting TrustedPlatform = gin.PlatformCloudflare instructs Gin to
read CF-Connecting-IP as ground truth, bypassing the XFF parser.
When the header is absent (health checks, direct non-CF probes)
Gin falls back through TrustedProxies → XFF → RemoteAddr as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:56:11 +08:00
chenchenandClaude Opus 4.7 407dbb7200 fix(http): trust private-range proxies so c.ClientIP returns real IP
Manager was created via gin.New() without calling SetTrustedProxies,
which in Gin v1.7+ defaults to trusting NOTHING — c.ClientIP() returned
the docker bridge peer (e.g. 10.2.3.4) instead of the real client IP
populated in X-Forwarded-For by the front reverse proxy.

Symptoms observed in production:
  - Devices page showed every user's "Last IP" as 10.2.3.4 / 10.2.3.5
  - tokens.device_last_seen_ip audit field useless for security review
  - Token IP allowlists effectively bypassed (always saw docker IP)
  - Rate-limit buckets keyed on docker IP — all users share a bucket

Fix: SetTrustedProxies with the standard RFC1918 + loopback ranges.
Covers every realistic Manager topology (docker compose, k8s ClusterIP,
reverse proxy on same VM). Cloudflare-direct topologies still need the
CF published ranges added; document that inline rather than auto-fetch
since we currently always front with Caddy/nginx.

UI cosmetic: When device_name is empty (pre-0.3.3 desktop clients
didn't always send it), Devices page now synthesises a label like
"Windows · 4f3a" from platform + last 4 chars of device_id instead
of the generic "Unnamed device", so users can tell their devices
apart at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:45:53 +08:00
chenchenandClaude Opus 4.7 b615f3dc67 fix(devices): V2 device-binding hardening + Web Devices UI
Server-side bug fixes (zero client-impact):
- fix(devices): re-pair after revoke now reactivates the row instead of
  returning a stale "reused:true" response. Before this, a user who
  revoked a device in Web UI then re-launched the desktop app got
  HTTP 200 from /pair but every subsequent V2 request 401'd with
  ErrDeviceRevoked, leaving them locked out.
- feat(v2): V2 auth failures now carry X-Heicode-Server-Time and
  X-Heicode-Auth-Error response headers. Lets the desktop client
  distinguish clock drift (timestamp_drift) from revoke/signature
  failures and show actionable messages instead of "Token invalid".
- fix(devices): RenameUserDevice rejects whitespace-only names (400)
  and truncates by rune count instead of bytes, so multi-byte UTF-8
  names (Chinese / Japanese) don't get mangled at the 64-byte boundary.
- feat(devices): RevokeUserDevice writes a SysLog audit line with
  user_id / token_id / device_id / device_name / operator IP / reason.
  Symmetric with the existing "reactivated revoked device" log so
  admins can trace both transitions when investigating lockouts.
- fix(devices): GetUserDeviceBoundTokens sort uses
  CASE WHEN device_last_used_at = 0 THEN device_bound_at ELSE
  device_last_used_at END DESC so a freshly-paired device doesn't
  sink below older but actively-used machines in the Devices list.
  Portable across SQLite / MySQL / PostgreSQL.

Web UI (web/default):
- New /devices route + features/devices/ page with table, revoke
  AlertDialog, rename Dialog, greyed-out revoked rows, empty state.
- Sidebar "Personal" group now shows "Devices" between Models and
  Account security (Smartphone icon).
- i18n strings added to zh.json + en.json.

Tests:
- 8 new tests covering re-pair reactivation, rename validation
  edge cases, sort order, audit log shape, V2 error code mapping,
  and diagnostic header emission. Full controller / middleware /
  model suite remains green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:46:07 +08:00
chenchenandClaude Opus 4.7 683ecc7470 feat(heicode): GET /api/heicode/self for desktop balance + usage
The cc-haha desktop client used to read its balance from
/v1/dashboard/billing/{subscription,usage}. Those endpoints honor the
token row's UnlimitedQuota flag — and device-bound tokens have that
flag set true because they are an auth mechanism, not a billing
boundary. Result: the balance pill always showed 100_000_000 USD
regardless of the user's real balance.

The right source is the user row (User.Quota / UsedQuota /
RequestCount), which is what /api/user/self surfaces to the web
dashboard. But that endpoint is UserAuth-only (session cookie / JWT),
which the desktop client doesn't carry — it holds a sk- bearer or
signs requests with its V2 device key.

This commit adds a slim sibling endpoint /api/heicode/self mounted on
TokenAuth so either sk- or V2 signature authenticates. Returns only
the fields the desktop balance pill + usage panel consume (quota,
used_quota, request_count, plus username/group/role for the title
bar) — no PII beyond what relay calls already expose. Quota numbers
go through the same QuotaPerUnit / display-type normalization that
billing.go uses, so the desktop pill and web dashboard show the same
number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:31:21 +08:00
chenchenandClaude Opus 4.7 e2cac00c14 fix(devices): pair is idempotent, cap only counts new devices
Old behavior: any (user_id, device_id) duplicate returned 409 and the
per-user device cap was checked BEFORE the dup-check. Combined effect:
a client that already paired once but lost the Manager row (or just
wants to re-confirm on every startup) hit 409 or 403 forever, with no
way to recover except an admin DELETE.

New behavior:
- Same (user_id, device_id, pubkey) tuple → 200 with reused:true.
  Lets bootstrap call pair on every login as an idempotent liveness
  probe.
- Same (user_id, device_id) but different pubkey → 409 with explicit
  "already paired with different key" message. Client treats this as
  a signal to clear local identity and regenerate.
- Cap check moved AFTER the dup check so re-pair of an existing
  device is never blocked by "device limit reached".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:13:50 +08:00
chenchen 16cec0ee9d fix(devices): pair tokens are unlimited-quota (inherit from User row)
After 67225fd fixed the V2 chat 403 caused by missing SetupContextForToken,
the next probe call surfaced a new 403:
  "token quota is not enough, token remain quota: \$0.000000,
   need quota: \$0.001590"

Root cause: PairDevice initialised the new tokens row with
UnlimitedQuota=false and didn't set RemainQuota, so it defaulted to 0.
Every subsequent V2 chat then failed at pre-consume since the token had
no spendable budget — even though the user's actual User.Quota was
positive.

Device tokens aren't a billing boundary in our model; they're the
Ed25519 binding for a single client install. Quota belongs on the User
row. Flip UnlimitedQuota=true so the relay path consumes from
User.Quota directly, matching exactly what the legacy sk- bearer was
already doing (legacy tokens in this deployment are unlimited too).

Verified end-to-end via /tmp/v2_probe2.js after deploy: POST
/v1/messages with full V2 envelope returns HTTP 200 with the model's
reply.
2026-05-21 03:55:17 +08:00
chenchen 67225fd67e fix(v2): TokenAuth V2 path must call SetupContextForToken
V2 chat returned HTTP 403 with body
  {"error":{"type":"new_api_error","message":"record not found ..."}}
even after Manager body_decrypt and Ed25519 verify both passed and the
device row was found. Root cause: the V2 dispatch in TokenAuth set
`id` + `token_id` directly via VerifyV2DeviceSignedRequest, called
applyTokenPolicyAndContext for IP/user/group checks, then jumped to
c.Next() — skipping SetupContextForToken entirely.

SetupContextForToken populates eight more keys the downstream relay
and billing pipeline expect:
  token_key, token_name, token_unlimited_quota, token_quota,
  token_model_limit_enabled, token_model_limit,
  ContextKeyTokenGroup, ContextKeyTokenCrossGroupRetry

Without them, channel distribute / pre-consume / log_consume
silently misroute and a generic "record not found" leaks out as 403.
The legacy bearer path didn't have this bug because it always
finished with SetupContextForToken before c.Next().

Verified with /tmp/v2_probe2.js (after this deploys): POST
/v1/messages with full V2 envelope returns HTTP 200 with the model's
reply body.
2026-05-21 03:50:04 +08:00
chenchen 52549bb2af fix(devices): /api/devices/pair accepts sk- bearer (TokenOrUserAuth)
Real test in C:\temp\v2_probe.js shows POST /api/devices/pair returning
HTTP 200 with body {"success":false, "message":"Unauthorized, invalid
access token"} when called with a Bearer sk- — the same sk- the OAuth
callback hands the client. Root cause: the route group used UserAuth(),
which only accepts a session cookie or a user JWT in Authorization, not
a relay-tier sk- bearer.

The OAuth-redirect flow (Heicode default) never produces a JWT — it
just hands cc-haha a sk-. So in production the pair call after
"一键登录" always 401'd, device-binding never activated, and V2
encryptedFetch silently fell back to legacy bearer for every request.

Fix: split the /devices route into two groups.
  - /devices/* (list, rename, revoke): still UserAuth(). A sk- must
    NOT be allowed to enumerate or revoke another device — that
    would let an attacker with a stolen sk- delete the legitimate
    owner's device binding.
  - /devices/pair: TokenOrUserAuth(). Pair is the bootstrap step, by
    definition no device key exists yet, so sk- IS the only credential
    available on the OAuth-redirect flow.

TokenOrUserAuth calls c.Set("id", token.UserId) via its TokenAuth
fallback, so the PairDevice controller's c.GetInt("id") keeps working.

Verified by re-running v2_probe.js after deploy: pair returns
HTTP 200 success:true.
2026-05-21 03:43:44 +08:00
chenchen 22ee18d2da feat(manager): V2 device-bound signed + body-encrypted protocol
Eliminate sk- bearer from the client wire entirely. V2 requests
authenticate via Ed25519 device signature (over a canonical that
binds method/path/timestamp/nonce/fingerprint/eph-pubkey/plaintext-
body-hash) and encrypt the request body with X25519 ECDH +
ChaCha20-Poly1305-AEAD. Server-issued sk- tokens still exist for
legacy callers during a 30-day deadline window; after the deadline
bare-bearer sk- on /v1/* is rejected.

What's new server-side:

- model/server_key.go + service/server_keys.go: long-lived X25519
  keypair persisted in DB. Private half is AES-256-GCM-sealed with a
  key derived from CRYPTO_SECRET so a SQL dump alone doesn't leak it.
  Generated on first launch by main.go::EnsureServerECDHKey.

- common/crypto.go: SealWithCryptoSecret / UnsealWithCryptoSecret
  helpers (AES-GCM); SafeWipe defense-in-depth zero-out.

- controller/server_pubkey.go + GET /api/server-pubkey: public
  endpoint clients fetch at startup to obtain the ECDH pubkey.

- middleware/body_decrypt.go: ChaCha20-Poly1305 decrypt of V2 bodies.
  AD binds device_id/timestamp/nonce/method/path so tampering any
  fails AEAD verify. Replaces c.Request.Body with plaintext for
  downstream relay handlers to consume unchanged.

- middleware/device_signature.go: new VerifyV2DeviceSignedRequest()
  looks up token by device_id (not bearer) and verifies an extended
  canonical that includes the ephemeral pubkey + plaintext body hash.

- middleware/auth.go::TokenAuth: dispatch on Content-Encoding header.
  V2 path skips ValidateUserToken entirely. Legacy path adds a 30-day
  /v1/* deadline knob.

- model/token.go::FindTokenByDeviceId: V2 lookup helper.

- controller/device.go::PairDevice: stops returning the sk in
  responses. Client identifies itself by device_id + signature from
  now on, no bearer needed.

- setting/operation_setting/device_binding_setting.go: new
  LegacySkV1DeadlineMs knob (0 = disabled until operator sets it).

Backward compatibility: V1 device-signed tokens (those issued by
the earlier PairDevice that DID return a sk-) keep working through
the legacy bearer path; the existing V1 signature middleware still
runs for them. The 30-day deadline is opt-in until ops sets it.

Tests: V1 regression suite passes (middleware + common).
V2-specific tests come in a follow-up commit alongside the client
encryptedFetch wiring; deferring lets us land the server-side
plumbing first without coupling.
2026-05-20 16:43:36 +08:00
chenchen 9e852d788f 删除网站 2026-05-20 14:39:58 +08:00
chenchen 2a1d8d8191 chore: initial import — heicode manager + website 2026-05-20 14:07:30 +08:00