Lays down the server side of a per-request Ed25519 signature scheme that
binds a token to a specific desktop install, so the bearer key can't be
extracted from ~/.claude/cc-haha/providers.json and resold. Plan lives
at ~/.claude/plans/peaceful-sprouting-crane.md.
Compatibility: legacy bare-bearer sk- callers (CLI/SDK) pass through
unchanged until P3 (30-day deadline) flips RequireGlobal=true. No
existing token rows are modified — pubkey is nullable and defaults to
null.
Pieces:
- model.Token gains DeviceId, DevicePubkey, DeviceFingerprint, DeviceName,
DevicePlatform, DeviceAppVersion, DeviceBoundAt, DeviceLastSeenIp,
DeviceLastUsedAt, RequireDeviceBinding, RevokedAt, RevokedReason.
Pure additive columns, GORM AutoMigrate handles SQLite/MySQL/PG.
- common.VerifyEd25519Signature: thin wrapper around crypto/ed25519
stdlib, used by the new middleware. No new external deps.
- service.MarkNonceUsed: Redis SETNX-based nonce store with an
in-memory sync.Map fallback for single-instance dev. TTL = setting.
- middleware.VerifyDeviceSignatureIfRequired: wired into TokenAuth as
a fail-fast dispatch right after model.ValidateUserToken. Verifies
canonical = METHOD\nPATH\nTS_MS\nNONCE\nFINGERPRINT\nSHA256(BODY),
signed as Ed25519(sha256(canonical)). 120s timestamp window, 300s
nonce window, fingerprint stored at pair time must match the header.
- controller.PairDevice / ListUserDevices / RenameUserDevice /
RevokeUserDevice, mounted at /api/devices/* behind UserAuth().
PairDevice enforces 5-per-user cap and returns the raw sk- once,
to be stored in the client's OS keychain (not providers.json).
- operation_setting.DeviceBindingSetting: MaxDevicesPerUser=5,
TimestampWindowMs=120000, NonceTTLSec=300, RequireGlobal=false.
Tests:
- common/crypto_test.go covers round-trip + tamper + malformed inputs.
- middleware/device_signature_test.go covers all error-path branches
(expired ts, wrong sig, tampered body, fingerprint mismatch, replay,
revoked, missing headers, legacy fallthrough).
- testdata/device_signature_vectors.json is the cross-language contract
Rust+TS sides will load to assert byte-identical canonical strings.
Untouched but reserved for follow-up phases:
- Anomaly detection / IP-diversity flagging (P1)
- 30-day deprecation banner + email notifications (P2)
- Hard cutover RequireGlobal=true (P3, day 31)
1. /api/heicode-auth/* proxy: CriticalRateLimit (20/20min) → GlobalAPIRateLimit
(180/180s). The Heicode external-identity proxy is hit on every page
render for /me + /refresh plus the login burst — CriticalRateLimit is
sized for sensitive ops (password reset, 2FA) and trips at ~5 quick
page loads, returning 429 to a normal user. APIM upstream rate-limits
itself, so a second tight layer here adds no security and just
manufactures 429s.
2. JIT-create user group: seed "default" instead of me.Data.ChannelID.
Companion to 578a68f which only patched the every-login overwrite
path. New users (yj2824269760@gmail.com et al, JIT-created after
578a68f) still landed in a UUID group → empty /v1/models response →
desktop client showed the static 3-Claude fallback list.
Both fixes were applied on the production VM directly today (sed +
python patch) — committing them so the next docker rebuild on VM keeps
them instead of reverting to the buggy file via git checkout.
DB hot-fix already applied: 6 affected users moved to group=default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CORS unblock — add /api/heicode-auth/*proxyPath backend proxy to
HEICODE_AUTH_BASE_URL. Frontend defaults to same-origin path so
the browser never hits APIM directly.
Sidebar — replace backend jargon (Git sources / Deployments /
Events / Wallet / Available models / Profile) with the user-facing
labels docs §10 mandates: 总览 / 准备清单 / 任务总览 / 审计 /
模型与余额 / 客户端 / 账号安全.
/sk-sources rewritten as 4-card preparation wizard with progress
meter; full Git form moves into a 高级补充 sheet. Drops JSON
editor, permission manifest, snapshots and resource-grant pills.
/deployments simplified to 任务总览: objective + status + last
update. Drops risk / budget / scope / secret_ref pills and the
RunDetailPanel; manifest details only in audit/advanced views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add userRoute.GET /api/user/desktop-downloads/file/:platform with
TokenOrUserAuth middleware. A plain browser <a href> can hit this with
just the session cookie; New-Api-User header isn't required (the SPA's
axios layer still injects it for the metadata endpoint on selfRoute).
Closes the 401 "无权进行此操作,未提供 New-Api-User" case from the
Heicode Manager desktop installer download path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manager now exposes a Resource Grant manifest and the Agnet control-plane response carries runtime state, agent instances, and permission_manifest so frontend runs can submit bounded resource grants without plaintext credentials.
Constraint: Manager remains the user console while NewAPI stays independent and OpenBao is referenced through secret_ref only.
Rejected: platform-side high-risk approval | client approval is the product boundary; Agnet only validates approval evidence.
Confidence: medium
Scope-risk: moderate
Directive: Do not mix child Agnet runtime model selection with NewAPI billing or expose OpenBao as a public route.
Tested: git diff --check; jq empty locale JSON; go vet ./controller ./model ./router; go test -count=1 ./controller ./model ./router
Not-tested: frontend typecheck/build because local node_modules tooling is absent and user requested builds happen on the VM.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Manager needs a platform-owned secret handoff path so resource bindings can keep only vault references while OpenBao stores tenant-scoped credential payloads.
Tested: go test ./controller ./model ./router && go vet ./controller ./model ./router
Co-authored-by: OmX <omx@oh-my-codex.dev>
- Add hide_from_user_ui on tokens; EnsureUserRelayToken on login and Agnet session
- List/search tokens: end-users see only visible keys; admins see all
- Add /available-models and sidebar entry; i18n en/zh + locales
- desktop download / router hooks if present under heicode/
Replace password-based /api/user/login bridge after external auth with POST /api/user/session/from-agnet: verify access (and optional refresh) against Agnet /api/auth/me, upsert local user by email, then issue the Manager session cookie. Frontend sends bearer tokens only.
Includes HEICODE_AUTH_BASE_URL in compose defaults and .env.example.
Made-with: Cursor