Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
58a9c43
feat(vault): seat_group cache columns + retrofit migrations
Jakegogo Jun 25, 2026
3343aa7
feat: seat-group (OAuth account pool) end-to-end + live-E2E fixes
Jakegogo Jun 26, 2026
21986e3
feat: seat-group invite-time group join + master OAuth egress proxy +…
Jakegogo Jun 26, 2026
aac0405
fix: group-VK no-local-material handling + reject OAuth direct-bind +…
Jakegogo Jun 26, 2026
80d0bf9
refactor: rename seat_group → oauth_group across all repos
Jakegogo Jun 26, 2026
eefdf4e
refactor: complete seat_group → oauth_group rename (comments / string…
Jakegogo Jun 26, 2026
7bdb95d
fix: update oauth group key cache handling
Jun 28, 2026
51c0447
feat: OAuth account pool per-member token model + seat-group integration
Jun 30, 2026
a84f425
fix(cli): strip trailing slash from browse base URL (root cause of ak…
Jun 30, 2026
c589db1
feat: OAuth account-pool per-member token model + hot-swappable egres…
Jul 1, 2026
c603ccb
fix: group-route degrade returns fail-fast status + buildBaseEvent ni…
Jul 1, 2026
1a1b02e
feat: employee OAuth self-contribute (email+password) + multi-group V…
Jul 1, 2026
0cf352c
fix: unify seat→pool-account routed pick behind poolroute single sour…
Jul 2, 2026
bdc488e
fix: proxy reboot PID-reuse lockout + surface routed OAuth account in…
Jul 2, 2026
1eb6689
fix: routingwire contract + member login prompt, cross-app language h…
Jul 3, 2026
9b1967f
fix: SyncRail framework — control-plane rails re-evaluate per cycle, …
Jul 3, 2026
3158d9f
feat: macOS notarization + aikey service/web status; fix delivery-sha…
Jul 4, 2026
d3582c7
feat: OAuth pool codex provider support (R34) + gateway-unreachable d…
Jul 4, 2026
28e6824
feat: Windows platform-compat hardening + Kimi/Codex CLI integration …
Jul 6, 2026
46e3c2a
fix(cli): accept statusless exchange response in aikey login --token
Jul 6, 2026
289de69
Merge pull request #4 from aikeylabs/group-support-codex
raysonmeng Jul 6, 2026
9e69970
fix(proxy): unattended password resolution for `aikey proxy start` (s…
Jul 7, 2026
3ff977c
feat: vault binding-material guard + web unlock-state full sync + Win…
Jul 7, 2026
08a0eb5
fix(login): never block on stdin without a TTY (observed 1h+ hang)
Jul 7, 2026
85693cc
feat: vault binding-material guard + web unlock-state full sync + Win…
Jul 7, 2026
39ff194
fix(proxy): stop condemning an elevated-session proxy as OrphanedPort
Jul 7, 2026
695561a
fix(proxy): permission-denied branch must preserve the reboot-lockout…
Jul 7, 2026
ed753ad
fix(proxy-lifecycle): add CREATE_NO_WINDOW to the background proxy spawn
Jul 7, 2026
93bfadb
fix(cli): guard remaining non-TTY stdin reads that could hang automation
Jul 7, 2026
4dc28a7
perf(vault): gate pre-dispatch schema replay on a per-build marker
Jul 7, 2026
c4be686
fix(env): detect injected provider configs semantically, not by marke…
Jul 7, 2026
86224d8
fix(kimi): unify all kimi wiring readers on one semantic predicate
Jul 7, 2026
93debef
feat: Windows/Mac parity (alpha4) — installer/service hardening + web…
Jul 7, 2026
a1a3f09
fix(vault): schema-marker probe must not use a read-only connection o…
Jul 7, 2026
c5116ff
fix(cli): tolerate BOM'd JSON inputs + never corrupt a UTF-16 $PROFILE
Jul 7, 2026
b333adf
perf(sync): cross-process debounce for the implicit background snapsh…
Jul 7, 2026
30e3d0d
fix: codex OAuth callback error handling + Windows detector/parity fixes
Jul 7, 2026
695dbe3
fix(sync): exempt _internal from the implicit snapshot sync — it neve…
Jul 7, 2026
417f436
fix(windows): resolve PowerShell profile via Documents known folder (#5)
LuckyGoMichael Jul 8, 2026
2ecbb07
refactor: shared ModalShell UI component + HookWireRcModal; activatio…
Jul 8, 2026
7399dae
feat: egress auto-follows system proxy (new sysproxy pkg) + moonshot-…
Jul 8, 2026
ff5cfa5
fix: proxy startup/lifecycle hardening (early-SIGTERM, port-drift pre…
Jul 8, 2026
91bc2ab
feat: hook wiring visibility — unwired-hook warning + readiness banne…
Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ regex = "1.11" # Text pattern matching (commands_internal/parse.rs)
bitflags = "2" # LineFlags 属性矩阵 (parse/line_class.rs, v4.1 spike 迁移 Stage 1)
crfs = "0.4" # Pure-Rust CRFsuite port (Stage 3 parse Layer 2; ablation-spike verified determinism)
serde_yaml = "0.9" # Provider fingerprint registry (Stage 3 parse Layer 3; embedded via include_str!)
toml_edit = "0.19" # Format-preserving TOML parse-merge for third-party CLI config injection (kimi/codex). Already in Cargo.lock as a transitive dep, so promoting it adds zero new transitive deps. Why not string-append: a foreign writer (e.g. kimi's own `hooks = []`) plus our blind append produced duplicate-key invalid TOML — see workflow/CI/bugfix. Why toml_edit over `toml`: preserves the user's comments/key-ordering when we edit their file.
md-5 = "0.10" # MD5 for Kimi session_dir lookup (matches kimi-cli's WorkDirMeta hash)
hex = "0.4" # Hex encoding for HMAC display
bincode = "1.3" # Binary serialization for .akb format
Expand Down Expand Up @@ -178,6 +179,10 @@ windows-sys = { version = "0.52", features = [
"Win32_System_Time", # FileTimeToSystemTime / SystemTimeToTzSpecificLocalTime (commands_statusline.rs event_time_hm)
"Win32_System_Console", # GetStdHandle / ReadConsoleW / GetConsoleMode / SetConsoleMode / GetConsoleScreenBufferInfo
# (lib.rs prompt_hidden / ui_select.rs raw input / ui_frame.rs term_width — Stage 1 windows-compat)
"Win32_UI_Shell", # SHGetKnownFolderPath(FOLDERID_Documents) — PowerShell $PROFILE must follow Documents
# known-folder REDIRECTION (Parallels shared profile / OneDrive / roaming); compat
# layer only, hardcoded <home>\Documents kept as fallback (bugfix 2026-07-08)
"Win32_System_Com", # CoTaskMemFree for the SHGetKnownFolderPath out-buffer
] }

[build-dependencies]
Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ AIKEY_BUILD_VERSION ?= $(VERSION)
BUILD_ENV = AIKEY_BUILD_VERSION=$(AIKEY_BUILD_VERSION) AIKEY_BUILD_REVISION=$(GIT_REVISION)$(GIT_DIRTY) AIKEY_BUILD_ID=$(BUILD_ID) AIKEY_BUILD_TIME=$(BUILD_TIME)

.PHONY: all release dev rebuild run test test-integration test-unit test-verbose \
test-cli-config-injection \
test-import-recall test-proxy-lifecycle-e2e build-mock-proxy \
e2e-import-personal e2e-import-trial e2e-import-production \
security-check-batch-import \
Expand Down Expand Up @@ -78,6 +79,18 @@ test-unit:
test-verbose:
$(CARGO) test -- --nocapture

## Third-party CLI config-injection regression (kimi/codex toml_edit merge).
## Guards against the 2026-07-04 kimi `hooks = []` duplicate-key corruption
## (workflow/CI/bugfix/2026-07-04-kimi-config-hooks-duplicate-key.md): a foreign
## key + our blind append produced invalid TOML. Covers merge/self-heal/idempotency
## for kimi and structural upsert/conflict for codex.
test-cli-config-injection:
$(CARGO) test --lib 'commands_account::shell_integration::hook_tests::kimi_merge'
$(CARGO) test --lib 'commands_account::shell_integration::hook_tests::kimi_remove'
$(CARGO) test --lib 'commands_account::shell_integration::hook_tests::codex_merge'
$(CARGO) test --lib 'commands_account::shell_integration::hook_tests::codex_remove'
$(CARGO) test --lib 'commands_account::shell_integration::hook_tests::codex_conflict'

## Stage 6 — end-to-end Quick Import smoke (per edition).
##
## These targets assume the corresponding installer has been run already
Expand Down Expand Up @@ -268,6 +281,7 @@ help:
@printf " %-22s %s\n" "make test-unit" "Unit tests only"
@printf " %-22s %s\n" "make test-integration" "Integration tests only"
@printf " %-22s %s\n" "make test-verbose" "Tests with stdout visible"
@printf " %-22s %s\n" "make test-cli-config-injection" "kimi/codex config-injection merge + self-heal regression (2026-07-04)"
@printf " %-22s %s\n" "make test-import-recall" "Stage 3 parse engine recall + fingerprint gates"
@printf " %-22s %s\n" "make e2e-import-personal" "Stage 6 e2e: status + aikey import smoke"
@printf " %-22s %s\n" "make e2e-import-trial" " (alias — same binary as personal)"
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Vault is unlocked once per shell session — subsequent `aikey run` calls reuse
| `aikey route` | vault.db (read-only) | nothing | stdout (for copying into third-party tool config) |
| `aikey test [<alias>]` | vault.db | nothing (probe-only via `X-Aikey-Probe: 1`) | proxy → upstream `/v1/models` |
| `aikey web [page]` | nothing | nothing | spawns browser → `aikey-local-server` |
| `aikey doctor` | proxy port, vault path, hooks | nothing | stdout report |
| `aikey doctor` | edition, proxy, vault, hooks, plugins (trust-local / compliance) | nothing | stdout report (`--detail` adds edition-aware ODS panels) |
| `aikey audit status` | collector completeness endpoint (+ proxy local state) | nothing | stdout per-source delivery report |
| `aikey audit reconcile` | collector gaps + proxy WAL | known-loss ledger (server) | stdout verdict; re-sends recoverable gaps, confirms losses |

Expand Down Expand Up @@ -220,12 +220,18 @@ aikey doctor # diagnose PATH / hook / proxy / vau
aikey test --all # connectivity test all credentials
aikey proxy restart # restart the local proxy

# Service status (which daemons are up)
aikey service status # one line each: web / proxy / trust-local
aikey web status # local web console: running? port? vault state?
aikey proxy status # proxy: running? pid? listen addr?
aikey service status trust-local # a single service in detail

# Delivery audit (financial-grade usage completeness)
aikey audit status # per-source: allocated / confirmed / gaps / known-loss / quarantine
aikey audit reconcile # actively reconcile now: re-send recoverable gaps, confirm true losses
```

Run `aikey --help` for the full subcommand list (display order = frequency, frequent first).
Run `aikey --help` for the full subcommand list (alphabetical, with a "Frequently used" shortcut section at the end).

## 10. Error Codes

Expand Down
10 changes: 8 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Vault 每个 shell session 只解锁一次 — 后续的 `aikey run` 用缓存
| `aikey route` | vault.db(read-only) | 无 | stdout(可复制到第三方工具配置) |
| `aikey test [<alias>]` | vault.db | 无(probe-only,带 `X-Aikey-Probe: 1`) | proxy → upstream `/v1/models` |
| `aikey web [page]` | 无 | 无 | spawn 浏览器 → `aikey-local-server` |
| `aikey doctor` | proxy 端口 / vault 路径 / hooks | 无 | stdout 诊断报告 |
| `aikey doctor` | 版型 / proxy / vault / hooks / 插件(trust-local / 合规过滤) | 无 | stdout 诊断报告(`--detail` 增加按版型区分的 ODS 面板) |
| `aikey audit status` | collector completeness 端点(+ proxy 本地状态)| 无 | stdout per-source 投递报告 |
| `aikey audit reconcile` | collector 缺口 + proxy WAL | 已知丢失台账(服务端)| stdout 对账结论;补传可恢复缺口、确认丢失 |

Expand Down Expand Up @@ -220,12 +220,18 @@ aikey doctor # 诊断 PATH / hook / proxy / vault
aikey test --all # 连通性测试所有凭据
aikey proxy restart # 重启 local proxy

# 服务状态(哪些后台进程在运行)
aikey service status # 一行一个: web / proxy / trust-local
aikey web status # 本地 web 控制台: 是否运行? 端口? vault 状态?
aikey proxy status # proxy: 是否运行? pid? 监听地址?
aikey service status trust-local # 单个服务的详细状态

# 投递审计(财务对账级用量完整性)
aikey audit status # 按源看:已分配 / 已确认 / 缺口 / 已知丢失 / 隔离
aikey audit reconcile # 立即主动对账:补传可恢复缺口、确认真实丢失
```

`aikey --help` 看全部子命令(显示顺序 = 使用频率,高频在前)。
`aikey --help` 看全部子命令(按字母序排列,末尾附「Frequently used」高频命令快捷区)。

## 10. 错误码

Expand Down
31 changes: 23 additions & 8 deletions data/provider_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,30 @@ providers:
# / KIMI_BASE_URL,kimi CLI 上游只读这一组。区分 platform 由 proxy_path 决定:
# moonshot 走 /moonshot/v1 → 上游 api.moonshot.cn;kimi_code 走 /kimi/v1 →
# 上游 api.kimi.com/coding。
# ② extra_env_vars KIMI_MODEL_NAME 从 kimi-k2.5 改为 moonshot-v1-8k,
# ② extra_env_vars KIMI_MODEL_NAME 从 kimi-k2.5 改为 moonshot-v1-128k,
# Moonshot 上游不支持 kimi-k2.5(原值是从 kimi_code 复制的 bug)。
# ③ MOONSHOT_API_KEY / MOONSHOT_BASE_URL 三层证据(kimi-cli runtime grep /
# Moonshot 官方 env-vars 文档 / 项目内 grep)实证无消费方,直接停写。
# ④ KIMI_MODEL_MAX_CONTEXT_SIZE = 8192(不是 131072): Moonshot 模型族名字直接
# 编码 context 上限(moonshot-v1-8k=8192 / -32k=32768 / -128k=131072),
# 默认配最便宜的 8k 模型 + 匹配的 8192 context,首单成本最低,长文用户
# 自行升 -32k/-128k。原 131072 与 8k 模型互相矛盾,会让 kimi-cli 端做错
# 截断预估。
# ④ KIMI_MODEL_MAX_CONTEXT_SIZE = 131072(moonshot-v1-128k 上限 128K)。
#
# 2026-07-08 默认模型 8k→moonshot-v1-128k 反转(详见 update/20260708-moonshot-
# 默认模型-8k-改-kimi-latest.md;bugfix/2026-07-08-moonshot-default-model-8k-
# breaks-kimi-cli.md):
# 决策 #9 原拍板 B(moonshot-v1-8k / 8192,"配最便宜的 8k,长文自行升级")的
# 前提是"用户从短请求起步"。但 Kimi CLI 光系统提示词就 ~12.4K token > 8192,
# moonshot 协议下从第一句 "hello" 就必然 400 token-limit-exceeded —— 等于
# 开箱即废,且用户无任何 UI 入口改模型(model 写死在本 registry)。前提不成立,
# 故抬高默认到 128K 上下文模型。
# ⚠️ 为什么是 moonshot-v1-128k 而不是"更新/更智能"的 kimi-latest / kimi-k2*:
# kimi-latest 等新模型在 api.moonshot.cn 上是**账号权限门控**的 —— 实测某
# 账号跑 kimi-latest 直接 404 "Not found the model or Permission denied",
# 而基础的 moonshot-v1 家族(8k/32k/128k,同一权限档)所有账号都够得着。
# DEFAULT 的目标是"最大范围账号开箱可用",要的是**最广兼容**而非**最新**。
# => 任何"自动追最新模型"的想法(含构建期拉取)在这里都是反模式:越新的
# 模型越可能权限门控,会让默认对更多账号 404。
# ⚠️ 未来若见此处想改回 8k 省钱,请先确认 Kimi CLI 最小提示词是否已 <8K,
# 否则会再次退化为开箱即废(regression: provider_extra_env_vars_kimi_
# family_per_platform_model 围栏测试守住此值)。
family: kimi
proxy_path: moonshot/v1
env_api_key: KIMI_API_KEY
Expand All @@ -146,8 +161,8 @@ providers:
picker: true
display: kimi(moonshot)
extra_env_vars:
- {var: KIMI_MODEL_NAME, value: "moonshot-v1-8k"}
- {var: KIMI_MODEL_MAX_CONTEXT_SIZE, value: "8192"}
- {var: KIMI_MODEL_NAME, value: "moonshot-v1-128k"}
- {var: KIMI_MODEL_MAX_CONTEXT_SIZE, value: "131072"}

# ──────────────────────────────────────────────────────────────────
# P0 additions (2026-04-24) — widely-adopted OpenAI-compatible Western
Expand Down
65 changes: 61 additions & 4 deletions src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ pub enum AuditOperation {
Export,
Import,
Exec,
/// A provider-binding write (Primary switch / auto-assign / reconcile
/// replacement). Added 2026-07-06: the incident binding write had NO audit
/// row, making "who bound this key" unanswerable — every write through
/// `write_bindings_canonical` now signs one Bind row when the caller holds
/// a `VerifiedVaultKey`. Pure enum addition — operation is a TEXT column,
/// no migration; `verify_audit_log` recomputes HMAC over the raw string.
Bind,
}

impl AuditOperation {
Expand All @@ -39,10 +46,37 @@ impl AuditOperation {
AuditOperation::Export => "export",
AuditOperation::Import => "import",
AuditOperation::Exec => "exec",
AuditOperation::Bind => "bind",
}
}
}

/// A 32-byte vault key PROVEN to match this vault's stored password_hash.
///
/// Why a newtype (2026-07-06, B-2 protocol hardening): audit rows are
/// HMAC-signed with an audit_key derived from the vault key. A row signed with
/// a WRONG key doesn't fail at write time — it surfaces later as a bogus
/// "tampered" verdict from `verify_audit_log`, poisoning the whole chain's
/// trustworthiness. The ONLY constructor runs `storage::verify_vault_key`, so
/// the type system guarantees every signer input was checked against the vault
/// (one cheap SQLite read per command; same chokepoint contract as the
/// 2026-05-11 snapshot-sync strict-verify fix).
pub struct VerifiedVaultKey([u8; 32]);

impl VerifiedVaultKey {
/// Verifies `key` against the vault's stored password_hash. Err when the
/// key doesn't match (stale web session, wrong derivation) — callers must
/// surface that, not sign with it.
pub fn new(key: [u8; 32]) -> Result<Self, String> {
storage::verify_vault_key(&key)?;
Ok(Self(key))
}

pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}

/// Initialize audit log table
pub fn initialize_audit_log() -> Result<(), String> {
let conn = storage::open_connection()?;
Expand Down Expand Up @@ -198,6 +232,21 @@ pub fn log_audit_event(
/// Returns the number of entries verified and any tampered entries.
pub fn verify_audit_log(password: &SecretString) -> Result<(usize, Vec<i64>), String> {
let audit_key = derive_audit_key(password)?;
// V2 acceptance (bugfix 2026-07-06, exposed by the B-2 bind-audit tests):
// rows written through `log_audit_event_from_vault_key` (the `_internal`
// web-bridge path since 2026-05, and B-2 bind rows) are signed with
// HMAC(vault_key, "AK_AUDIT_V2") — a DIFFERENT key than V1's
// Argon2(password, AK_AUDIT_SALT_V1). The verifier only ever tried V1, so
// every V2-signed row read as "tampered" (false positive). A row now
// verifies if EITHER derivation matches; both keys come from the same
// password, so the tamper-evidence property is unchanged. Best-effort on
// the V2 key: absent salt/kdf params (pre-vault state) just skips V2.
let audit_key_v2: Option<[u8; 32]> = (|| {
let salt = storage::get_salt().ok()?;
let (m, t, p) = storage::get_kdf_params().ok()?;
let vault_key = crypto::derive_key_with_params(password, &salt, m, t, p).ok()?;
derive_audit_key_from_vault_key(&vault_key).ok()
})();
let conn = storage::open_connection()?;

let mut stmt = conn
Expand Down Expand Up @@ -232,16 +281,24 @@ pub fn verify_audit_log(password: &SecretString) -> Result<(usize, Vec<i64>), St
.get(5)
.map_err(|e| format!("Failed to get hmac: {}", e))?;

// Recompute HMAC
let computed_hmac = compute_audit_hmac(
// Recompute HMAC — V1 first (bulk of historic rows), then V2
// (vault_key-signed rows from the web bridge / B-2 bind rows).
let v1_hmac = compute_audit_hmac(
&*audit_key,
timestamp,
&operation,
alias.as_deref(),
success != 0,
)?;

if computed_hmac != stored_hmac {
let mut matches = v1_hmac == stored_hmac;
if !matches {
if let Some(k2) = &audit_key_v2 {
let v2_hmac =
compute_audit_hmac(k2, timestamp, &operation, alias.as_deref(), success != 0)?;
matches = v2_hmac == stored_hmac;
}
}
if !matches {
tampered_ids.push(id);
}

Expand Down
Loading
Loading