HM now mints a random per-agent access token at deploy, injects it into the
agent env (AGENT_ACCESS_TOKEN + HEICODE_AGENT_ID) and returns it to the
deploying client (agent list access_token). Only the owning user receives it,
so only they can drive the agent — closing the gap where any valid sk- could
drive any agent and exfiltrate its mounted resources.
AM authorizes the caller either locally (compare to its env token) or via the
new public POST /api/heicode/agent-access/verify {agent_id, access_token} ->
{valid, user_id} (constant-time compare, no info leak on miss). AM may opt out.
Docs: AM contract §3.1 + client API §6 updated; access_token no longer empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) Deploy Sub Agent: stop hiding resource types AM doesn't support yet (vm) —
show them but disabled with a "暂不支持" badge + tooltip, so users see their
bindings instead of wondering where the vm went.
2) Overview (概述/cockpit): replaced the old task-model status cards
(Running/Completed/Failed/裁决 + "code delivery runs") with template-agent
semantics — Agent 总数 / 运行中 / 启动中 / 异常, "最近部署 · 我的 Agent",
all Chinese. Dropped unused t()/useTranslation/PlayCircle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add a run-status menu (I shouldn't have removed it) but showing ONLY what the
template-agent model actually has — no old 待确认/sub_agile/子任务流/智能体任务图/
产物/SK快照/合并时间线. New /agent-status page:
- summary counts (total / running / starting / other)
- agent list (template Chinese name, live status badge, subdomain, #resources)
- detail panel: live status (polls /agents/{id}/status from AM), copyable
subdomain, mounted resources (names from /api/resources), runtime_id, timestamps.
Sidebar item "运行状态" + zh key added; build + tsc clean; route registered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new §0.1 联调结果: real production test outcomes — create POST /agents works
(returns access_info.domain/namespace), DELETE /agents/{id} 500s (AM
UnboundLocalError bug), POST /agents/{id}/stop 404 (no endpoint), agent stays
Pending / subdomain unreachable. The 3 AM-side blockers listed up top.
- OPENAI_API_KEY is now injected (a minted user new-api sk-, billed to the user,
revoked on delete; verified working at /v1) — §1.1 env + §2 updated.
- §3 client<->agent: now documents AM's A2A protocol (message/send · stream),
api_key auth, and flags the per-user isolation security gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old /deployments (运行总览) + /tasks/$id pages rendered the new template agents
with meaningless old sub-task fields (待确认/sub_agile/子任务流/智能体任务图/产物/
SK快照/合并时间线 — all empty). Removed the whole old-model UI cluster and pointed
the Overview dashboard at the real new endpoint:
- deleted features/agent-console, features/agent-hub, features/tasks and the
routes /deployments and /tasks/$id.
- cockpit (Overview) now lists deployed template agents from /api/heicode/agents
(status counts + recent agents), not the old listAgentDeployments.
- removed the "运行总览" sidebar item + dead /deployments links in footer /
top-nav / sidebar-config.
- routeTree regenerated; frontend build + tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the full desktop-client flow against production (real account) and updated
the doc to the verified responses:
- capabilities model = gpt-5.4; modes still returned but ignored.
- agent object: access_token is "" (AM issues none — connect via A2A api_key),
subdomain is a host (dep-xxx.taijiagnet.com), status starts Pending; real
binding_ids; shown as "生产实测" examples.
- §4: clarified the template `model` field is a role-tier hint (opus), not the
runtime model (gpt-5.4 injected at deploy).
- §5: stop currently fails / delete returns runtime_cleanup:"failed" (AM stop
missing + delete bug); HM removes the local record regardless.
- §6: must wait for status=running / health 200 before connecting; noted the
agent currently stays Pending and the subdomain isn't reachable yet (AM side).
- top banner: per-endpoint production verification note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploy now also passes OPENAI_API_KEY (the 3rd of OPENAI_BASE_URL/MODEL_NAME/
OPENAI_API_KEY AM expects). HM mints a hidden, unlimited-quota sk- token for the
user per agent ("sk-"+key, billed to the user), injects it as OPENAI_API_KEY, and
stores the token id on the deployment. The token is revoked on delete and rolled
back if AM start / persist fails (no leaked keys). gateway accepts Bearer
sk-<key> (middleware strips sk-).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preset templates carry a Claude-style frontmatter model (opus/sonnet) which is
NOT a model on the HM gateway. Passing it as MODEL_NAME would make the agent's
model calls fail. Now MODEL_NAME defaults to the gateway model gpt-5.4
(env AGENT_RUNTIME_DEFAULT_MODEL); a non-Claude-tier template model is honored.
Tests + AM contract doc updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/devices/logout (UserOrV2DeviceAuth): a device-signed client revokes its
OWN bound token via the signed X-Heicode-Device-Id (cannot touch other devices);
a session/JWT caller may pass {device_id}. Idempotent. The existing DELETE
/api/devices/:id revoke is session-only, so device clients had no self-logout —
this closes that gap. Documented in the client API doc §1.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real test showed AM's DELETE /agents/{id} currently 500s (AM bug), which left the
HM record permanently undeletable. HeicodeDeleteAgent now treats the AM delete as
best-effort: on AM failure it logs + still removes the local record and returns
runtime_cleanup:"failed" so the user can always clear their list. (Stop stays
strict — AM has no /stop endpoint yet, pending their confirmation.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real production test (user account) confirmed HM->AM POST /agents creates a real
agent (returned subdomain + status). But stop/delete still used the old
/api/agent/agents/{id} defaults and 404'd. Aligned status/stop/delete defaults to
the same namespace as create: /agents/{id}, /agents/{id}/stop. AM contract doc
notes these are HM's best guess pending AM's confirmation of the real lifecycle
endpoints (their doc only specified create).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adapt HM's template-agent integration to AM's actual CODING_A2A API (per their
doc), keeping it isolated in agent_template_runtime.go:
- start payload -> AM's POST /agents { name, template:"coding_a2a_agent",
framework:"A2A", config:{user_id,...}, env } with the template .md folded into
env.AGENT_INSTRUCTION_TEXT, template_key -> AGENT_ROLE_NAME, model gateway via
OPENAI_BASE_URL + MODEL_NAME (OPENAI_API_KEY left to the client per A2A request).
- response parse -> access_info.domain/external_ip -> subdomain, namespace/name
-> runtime_id; AM issues no access_token (client uses A2A api_key).
- env names aligned to AM: GIT_DEFAULT_BRANCH, POSTGRES_* (was PG_*),
AZURE_BLOB_ACCOUNT_NAME/CONTAINER/ACCOUNT_KEY (was BLOB_*); source keys aligned
to the resource-binding form (db_name/username/database_password/access_key).
Only AM-supported types (git/mysql/postgres/azure-blob); vm/redis/mongo/bucket
now rejected as unsupported until AM adds them.
- frontend: resources page splits DB into MySQL/PostgreSQL (correct provider),
drops vm; deploy page hides unsupported resource types.
- docs: AM contract + client doc updated to the real env names, payload, and the
A2A direct-connect (message/send · message/stream) + api_key auth.
- tests updated for the new env names + AM payload/response shape. All green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hand-off doc for the agent_management team: the 4 endpoints AM must implement
(start/status/stop/delete) with exact request/response (grounded in the isolated
adapter agent_template_runtime.go), the env naming convention AM templates must
read (git/vm/db/blob/bucket), AM's responsibilities (inject .env, validate the
agent access token, models via HM /v1/*, no secret leakage), the client<->agent
direct SSE contract AM needs to define, security requirements, the env-overridable
paths, and a joint integration checklist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the last review finding: the AM-issued access_token was stored in plaintext
in agent_deployments. Now sealed with common.SealWithCryptoSecret on write and
unsealed on read — the client still receives the plaintext token (no contract
change), but a DB dump no longer exposes live agent tokens. Legacy/plaintext
values and an unset CryptoSecret degrade gracefully (pass-through). Round-trip test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two review agents (backend + frontend) audited the template-agent feature. Real
findings fixed (verified the one false-positive — okOrThrow already reads the
top-level success because the response interceptor returns response, not
response.data):
Backend:
- buildAgentEnvFromBindings now filters status='active' so a revoked/disabled
binding can't still have its KV secret resolved + injected.
- same-type binding env collision (e.g. two git repos -> GIT_REPO_URL) now errors
instead of silently overwriting.
- refreshAgentStatus uses a field-level Updates (status/updated_at) instead of
Save(full row) to avoid clobbering a concurrent stop/delete.
- tests added: same-type collision rejected, inactive binding rejected.
Frontend:
- deploy/stop/delete now pass noBusinessError so the global interceptor stops
double-toasting the raw English backend message; okOrThrow+onError give one
friendly Chinese error. Extracted okOrThrow/noBusinessError into lib/api.ts.
- resources page revoke + create + KV-secret-write now check success (okOrThrow)
so a failed unbind / credential write no longer falsely reports success.
- clipboard copy wrapped in try/catch (no false "copied" on failure).
- resources group label shows Chinese resource-type names.
Go + frontend builds/tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review of the client doc against the real code found and fixed:
- §1 auth was not self-contained (deferred the canonical to the deprecated doc).
Inlined the full signing contract verified against middleware/device_signature.go:
the exact header set, the fixed-order canonical string (method/path/ts/nonce/
fingerprint/eph_pubkey/sha256(body)), ed25519(sha256(canonical)), the heicode-aead-v1
encrypted-body rules, and the X-Heicode-Auth-Error / X-Heicode-Server-Time failure
headers.
- §2 auth mismatch (accuracy bug): /api/user/self is UserAuth (session/JWT), NOT
device-signed — a device-only client cannot call it. Marked it optional and
clarified the two different auth schemes (/api/user/self* vs /api/heicode/*).
- §8: documented that failures return HTTP 200 with success:false (client MUST
read success), and that error.retryable is always false (decide retry by code).
- §10 inventory: corrected /api/user/self auth + added /self/models.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 普通 sub task-orchestration model was replaced by the template-agent model
and its backend deleted. Removed the now-obsolete docs describing it:
- heicode-desktop-sub-agile-api.md, heicode-desktop-subagile-e2e-demo.md
- heicode-desktop-unified-api.md, heicode-sub-mode-flow-spec.md
- 普通sub敏捷模式-AgentManager对接任务清单.md
- AgentManager普通sub{产物回调缺失问题,剩余补充要求,联调整改要求}.md
Fixed dangling references in the new docs (client-api / template-agent-model).
Swarm (蜂群) docs kept — different mode, out of scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full review of the template-agent code surfaced and fixed:
- [frontend, critical] deploy/stop/delete treated HTTP-200-with-success:false as
success (backend agentError returns 200 per new-api convention) -> a failed
call wrongly toasted success. Added okOrThrow() that inspects the envelope and
throws the server (or a friendly Chinese) message so onError fires.
- [frontend] "我的 Agent" showed the raw template key (architect) instead of the
Chinese name; now resolves name via the templates list.
- [backend] ensureAgentTemplatesSeeded consumed sync.Once even when model.DB was
nil (would permanently skip seeding) -> DB check moved outside the Once.
- [backend] AdminUpdateAgentTemplate could wipe name_zh/definition with empty
values -> guard those critical fields.
- [security] warn when starting an agent with secret-bearing env over a non-HTTPS
AM URL (secrets must not transit the network in clear).
Go + frontend build/tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New authoritative client doc (heicode-desktop-client-api.md): the desktop client
lists its agents from HM, gets each agent's subdomain + access_token, and
connects to the agent directly over SSE; models for both client and agent go
through HM /v1/*. Grounded in the production-verified responses (19 Chinese
templates, agent list/deploy/status shapes, error codes). Marks the old
unified-api doc (sub task-orchestration) as superseded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Customer-facing, plain Chinese (no env/technical jargon). New sidebar item
"部署 Sub Agent" -> /deploy-agent. The page:
- Step 1: pick which bound resources the agent may use (multi-select cards from
/api/resources).
- Step 2: pick an agent template (cards show Chinese name + description from
/api/heicode/agent-templates).
- Deploy -> POST /api/heicode/agents {template_id, binding_ids}.
- "我的 Agent" list (/api/heicode/agents) with status, copy-address, stop, delete.
Frontend builds clean (tsc + rsbuild; routeTree regenerated). zh label added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Templates now live in HM (not AM). A template is a Claude-Code style subagent
.md definition; HM passes it to AM at deploy time.
- model AgentTemplate (agent_templates): template_key, name_zh / description_zh
(Chinese display for the console), model, definition (full .md), source, status.
- 19 presets from oh-my-claudecode (MIT, NOTICE.md attribution) embedded via
go:embed and idempotently seeded; Chinese name+desc mapping in code.
- GET /api/heicode/agent-templates now reads HM's library (Chinese name/desc),
not AM. Admin CRUD at /api/agent-templates (AdminAuth).
- deploy loads the chosen template and sends {template_key, agent_definition (md),
model, env, callback_url} to AM via a generic /api/agent/agents/start; removed
the AM-template-listing path. AM adapter still isolated (amStartArgs).
- tests: frontmatter parse, seed (19 + 架构顾问), start round-trip asserts
agent_definition in payload. All green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After phase A removed the old client task layer, AgentArtifactRevision and
AgentCloudDeployment became unreferenced (their only consumers — the deleted
heicode_task_create/project_artifacts/artifact_edits/cloud_deploy — are gone).
Removed the structs + their AutoMigrate entries. Existing tables are left in
place (harmless, no model) and can be dropped later if desired.
Option B teardown: kept the shared platform backend still used by the web
console / runtime callbacks (admin /api/agent/*, approvals, leases, callbacks,
AgentArtifact, sk-snapshots, withDisplayStatus). Build + controller/model/router
tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The template-agent model replaces the old sub/swarm task orchestration. Removed
the entire client-facing task surface:
- routes: registerHeicodeTaskRoutes (all /heicode/{sub-agile,swarm}/tasks/*) and
GET /heicode/deployment-targets.
- controllers deleted: heicode_task_create, heicode_client_routes,
heicode_project_artifacts, heicode_artifact_edits, heicode_cloud_deploy
(+ the agent_deliverable_secret_test that covered the deleted markdown-project
parsing).
Build + controller/router/model tests green. Shared backend (AgentDeployment,
agent_runtime_client helpers, admin /api/agent/*) intentionally kept — trimmed in
the next phase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the user-facing env_map mistake with a built-in env convention keyed by
resource type + provider — users never see/edit env names; they only fill plain
resource fields. Supports git (gitea/github/gitlab), vm, database
(mysql/pg/redis/mongo, with alias normalisation), storage (azure blob / bucket).
Lenient: missing optional fields are skipped; only unsupported type or a KV read
failure errors.
Other issues found in review and fixed:
- start timeout: template-agent start now uses a longer timeout (default 60s,
AGENT_RUNTIME_START_TIMEOUT_SECONDS) since AM provisions synchronously — 5s
would time out. amTemplateDo takes a per-call timeout.
- orphan agent: if AM start succeeds but the Manager record fails to persist, the
orphan is rolled back (best-effort amDeleteTemplateAgent).
- findUserTemplateAgent now guards template_id<>'' so the new endpoints can't
touch a legacy task deployment.
- binding_ids defaults to [] (not null).
- removed ResourceBinding.EnvMap field entirely.
Tests rewritten for the built-in convention (blob metadata-only, db provider
prefixes incl pg/mg aliases, git provider-agnostic names, ownership, unsupported
type, empty); adapter round-trip + router tests still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- amGetAgentStatus: HM pulls a running agent's status from AM on demand
(is it alive / crashed), isolated in the AM adapter like the rest.
- GET /api/heicode/agents/:id/status + status refresh on GET /:id detail,
best-effort (keeps last-known status if AM is unreachable; never blocks read).
- tests: httptest-backed round-trip tests for amStartTemplateAgent and
amGetAgentStatus against a fake AM (validates the full request/parse path);
router registration still clean. All pass.
Note: no required-resource validation added — HM injects whatever bindings the
user selected into the agent .env (per env_map); AM reads what it needs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HM-side logic for the template-agent model, written against a proposed AM
contract that is fully isolated in agent_template_runtime.go — when AM ships its
real API, only the default paths + response field names in that one file change.
- agent_template_runtime.go: AM adapter (list templates, start template agent
with env, stop/delete) reusing the existing runtime config/url/envelope helpers.
- agent_template_handlers.go: GET /api/heicode/agent-templates; POST /api/heicode/
agents (resolve bindings -> env, start via AM, persist subdomain+token);
GET /agents, GET/:id, POST/:id/stop, DELETE/:id. Owner-scoped; env never logged.
- reuses AgentDeployment as the agent record (TemplateID/Subdomain/AccessToken/
BindingIDsJSON) and buildAgentEnvFromBindings for env assembly.
- routes wired under /api/heicode (UserOrV2DeviceAuth), verified no registration
panic (router tests pass).
- agent_template_test.go: 7 independent unit tests (env assembly metadata-only,
ownership, secret-without-ref, unknown-source, empty; path substitution;
response mapping) — all pass without AM/KV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildAgentEnvFromBindings resolves selected resource bindings into a flat env map
for a template agent's .env at start: non-secret values from Metadata, secret
values resolved from Key Vault (getJSONSecret) only here, lazily, never persisted.
AM-independent foundation for the deploy-agent flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additive, AutoMigrate-friendly (new columns only, cross-DB safe). Foundation for
the template-agent + direct-connect model:
- AgentDeployment: TemplateID, Subdomain, AccessToken, BindingIDsJSON — reuse the
existing deployment record as the deployed-agent record instead of a new table.
- ResourceBinding.EnvMap: declares which env vars a binding exposes to a template
agent at start (non-secret from Metadata; secret resolved from KV only at start).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified the old-model code surface against the repo. Found the new template-agent
model is an EVOLUTION of the existing deployment control plane, not greenfield —
so several files are mixed and must NOT be whole-deleted:
- agent_runtime_client.go: AM HTTP client (config/url/headers/create/stop/status/
health) is reused by the new StartAgent/StopAgent AND by capabilities; only
withDisplayStatus + artifact/budget helpers are old.
- agent_control_plane.go: AgentCreate/List/Get/StopUserDeployment + AgentDeployment
table are the backbone the new agent record reuses; only per-deployment task
sub-handlers and the withDisplayStatus calls (1191/1200/1222) are old.
- agent_task_bridge.go: resolveResourceBindingIntoGrant is reused for env assembly.
- AgentDeployment table: KEEP and extend (subdomain/access_token/binding_ids)
instead of new heicode_agent table + DropTable. Reconciled the model doc to match.
- Added agent_runtime_http_smoke_test.go to the sync-fix list.
Adds a §0.0 dependency/impact analysis answering: coverage, shared-feature impact,
and impact on the new APIs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concrete deletion plan for the old sub task model now that sub uses the
template-agent + direct-connect model. Maps real code surface (verified against
the repo) into delete / trim / evaluate buckets:
- routes (registerHeicodeTaskRoutes, deployment-targets, old user/deployments)
- controllers (heicode_task_create/client_routes/project_artifacts/artifact_edits/
cloud_deploy delete; agent_runtime_client/task_bridge/callback/control_plane trim)
- models + DB tables/columns (AgentArtifact/Revision/CloudDeployment drop;
AgentDeployment/SKSnapshot/CallbackEvent evaluate) with GORM/cross-DB drop notes
- frontend pages/routes/sidebar/i18n
- ordered teardown steps + verification + open questions to confirm before deleting
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New simplified model: user deploys a template agent (resources injected into
its .env by HM from KV) via the HM web console; AM returns a unique subdomain +
access token; the desktop client connects DIRECTLY to the agent over SSE (HM out
of the data path). Both ends still call HM /v1/* for models.
Doc organizes the HM-side changes into: keep / add / modify / delete, plus the
agent record data model, the AM contract points, auth & secret handling, the
locked decisions, and rollout order. The old sub task-orchestration stack
(tasks/workflow/display_status/git_ref/artifact-download/lease/revision) is
marked for removal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the remaining coverage gaps in the client-facing API doc (verified against
real routes in router/api-router.go):
- §2.1 NEW "sub 可用性就绪闸门": the 4-gate composite the client must pass before
enabling sub (capabilities.enabled + device-bound + active git binding + balance),
with pseudo-code and per-gate data source — directly answers "客户端判断 sub 是否可用".
- §2.2 NEW account/balance: GET /api/user/self (+/self/groups,/self/models),
quota/used_quota/request_count, model intersection rule.
- §2 capabilities: documented every field (runtime_kind/model_selection/supports_*).
- §4.2 NEW run-info interfaces with response shapes: logs(user/debug),
timeline(events incl delivery.pushed), events, metrics, diagnostics, sk-snapshots.
- §12 NEW full endpoint inventory by flow stage (①startup ②create ③monitor
④approve ⑤artifacts ⑥iterate ⑦deploy ⑧resources) with 🟢/🟡/🔴 status — a
coverage checklist mapping every real route so nothing is silently missing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- /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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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.