feat(proxy): seat_group resolve/select + persona + group runtime#1
feat(proxy): seat_group resolve/select + persona + group runtime#1Jakegogo wants to merge 63 commits into
Conversation
…runtime pull Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@Jakegogo 「override 谁写谁读」(动态决策引擎冲突 5)。分工:引擎当 背景引擎(master 调度大脑)会按风险/容量决定"某席位下次该走哪个号",写进 要你建的两块(引擎只当写者,不碰你的 seatassign)
安全网(贴合你现状)没 override 时(引擎没上线/没写)→ proxy 就是回退纯 seatassign = 你现在的行为。所以这是优雅叠加:"没 override = 你现状,有 override = 引擎指挥",引擎挂了系统照跑(不变量 1)。 一句话:引擎写 override 值(引擎内);你建 routing 端点 + proxy 加 override 优先读(带成员有效性校验)。 🤖 Generated with Claude Code |
|
@Jakegogo 引擎以外待建 · 信号底座(I5,承接前面 override 那条)。好消息:上报通道不用新建,搭现成用量管道即可。 proxy 现在对
即:你采+解析+搭车上报,引擎在 ODS 上消费。落点和 persona 归一同仓。 🤖 Generated with Claude Code |
|
@Jakegogo 更正一下上面那条 override 评论的口径 🙏 —— 我后来看了你 你 §5.4b / 760ea08 已经定义好的(我们完全照用,不改设计):
现状:你这套 §5.4b 设计已定,但 PR 代码里还是零(端点、proxy override 读都没接,proxy 现在纯 seatassign 顺序取可用、忽略 advisory)。所以引擎按你这套契约当 override 唯一写者,你把 §5.4b 落地即可,无需改任何设计。 (前一条评论说「你留空了、按我们 spec 建」口径不准,以这条为准——是落地你自己的 §5.4b。) 🤖 Generated with Claude Code |
|
@raysonmeng N12 override 分工收到(引擎写 1. 下发端点
|
|
@Jakegogo routing 契约逐条确认,可以开写: 1. 下发端点
|
[aikey-proxy] group-VK resolve/select + OAuth-pool routing with per-account window cap, cooldown and reset, persona normalization, group-runtime pull; fix group-VK 502 (resolve provider per selected account instead of the empty VK-level ProviderCode); drop the proxy-local impersonate client for broker's. Refs: - group-VK empty provider-code 502 bugfix: https://github.com/aikeylabs/workflow/blob/3eb36d250c0f02ed28ba4753b6fa468d1fcf1b59/CI/bugfix/2026-06-25-group-vk-empty-provider-code-502.md - seat-group / OAuth account pool requirement spec: https://github.com/aikeylabs/workflow/blob/3eb36d250c0f02ed28ba4753b6fa468d1fcf1b59/CI/requirements/2026-06-23-oauth-account-pool.md
The proxy half of I5 (parse upstream unified-* utilization → ship to master), on a NEW branch off jake's feature/seat-group (his branch untouched). Self-contained, main-link-safe by construction: - signalReporter: non-blocking enqueue (full buffer drops the sample — trend signal, never block forwarding), background batch upload, all errors logged not surfaced; nil reporter = feature off. - parseUnifiedUtil5h: clean-parse-only off the response header. - POSTs to ControlURL + /accounts/me/signals (the master receive endpoint built control-side, commit 99f033e). NOT yet wired (two remaining): (a) the hook in buildBaseEvent (parse + enqueue, trivial, nil-safe), (b) the AUTH bearer — /accounts/me/signals needs a master account-JWT, but the proxy's existing reporter credentials are collector-audience (CollectorToken / route creds), not a master account-JWT; grabbing the right account JWT is jake's seat_group account-auth wiring. That's the last integration point. route.CredentialID (types.go:72) supplies the sample key at the hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the proxy half of I5 on the spike branch (jake's feature/seat-group untouched): - Proxy gains a signalReporter field + EnableSignalReporting(controlURL, bearer). - buildBaseEvent hook (post-response): parseUnifiedUtil5h(resp.Header) → nil-safe, non-blocking signalReporter.enqueue(route.CredentialID, now, util). Off the hot path — a full buffer drops the sample, never stalls forwarding. - supervisor wires it where it sets the usage reporter, reusing the team account-JWT (buildCollectorCredentials(...)["team"].Bearer — the SAME credential the group-runtime poll authenticates with) → master /accounts/me/signals + readControlPanelURL(). Off unless a team credential + control URL exist. I5 now end-to-end: proxy parse → async POST → master receive (commit 99f033e) → engine_meta ring → engine SignalSource → DeriveSignals. Full proxy `go build ./...` green, vet clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… group-VK fixes [aikey-proxy] path-prefix route dispatches group VKs to the group handler (was 403 on empty provider — only the probe entry was affected); seat-group routing flag now defaults ON (AIKEY_PROXY_SEAT_GROUP_ENABLED=0 to disable); pass empty egress proxy (proxy node has its own egress, R25). Refs: - group-VK connectivity-test 503 bugfix: https://github.com/aikeylabs/workflow/blob/9dbdc448e094f7719d9cceb2e4138fe68e015276/CI/bugfix/2026-06-26-vault-test-group-vk-and-error-code.md - seat-group / OAuth pool requirement spec (R25): https://github.com/aikeylabs/workflow/blob/9dbdc448e094f7719d9cceb2e4138fe68e015276/CI/requirements/2026-06-23-oauth-account-pool.md
…lloc-engine-signal-emit
Table test for parseUnifiedUtil5h, nil-safe/non-blocking enqueue, and post() against an httptest server asserting POST + Bearer + JSON body. Covers the fault-isolated emit path that ships unified-5h-utilization to master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an upstream 401 indicates the credential's OAuth token was hard-revoked
(isHardRevoked: status 401 + 'revoked' substring in the already-parsed error
type/message), the proxy best-effort emits {credential_id,reason} to master's
/accounts/me/signals revoked[] array, riding the existing I5 30s flush (no POST
storm). Detection lives in recordEvent (the universal upstream-error path) reusing
captureUpstreamErrorBody's parse — zero extra body reads, the 401 is never altered.
Self-test probes are excluded so they can't trigger a quarantine. Narrow keyword
match avoids false-positive quarantines on routine token-expiry 401s.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… loopback probe hijack [aikey-proxy] strip the provider prefix before handing a group VK to the group handler — it forwards r.URL.Path verbatim, so an unstripped /anthropic/v1/... hit api.anthropic.com/anthropic/... → 404 (regression from the earlier path-prefix group-dispatch 403 fix). Refs: - group-VK path-strip 404 root-cause chain (§78+): https://github.com/aikeylabs/workflow/blob/6306efcd67b597c44e3c87a0e16da61bd6bd9514/CI/bugfix/2026-06-26-vault-test-group-vk-and-error-code.md - seat-group / OAuth pool requirement spec: https://github.com/aikeylabs/workflow/blob/6306efcd67b597c44e3c87a0e16da61bd6bd9514/CI/requirements/2026-06-23-oauth-account-pool.md
…lloc-engine-signal-emit
…eed)
Feeds the engine's §5.1 w5 recent-rate-limit signal. recordEvent() increments a
per-credential 429/403 counter (mutex-guarded map, snapshot+cleared each 30s flush —
a counter not per-event POST, since 429s burst). The flush adds an optional
rate_limits[] array {credential_id,count,window_secs} alongside samples/revoked.
Self-test probes excluded; response untouched; never blocks the forward path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
util_7d: parseUnifiedUtil7d mirrors the 5h parser; the util sample now carries util_7d (omitempty, byte-identical when absent) for the engine's weekly squeeze. ConcurrencyPeak: track in-flight forwarded requests per credential (defer inc/dec at the serveRoute chokepoint, leak-proof), report the window peak in a new optional concurrency[] array. Both -race clean; forward path untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…na (§3.3) AST source scan asserting every pool-serving oauthInject call site also calls stashPoolPersona — catches the 归一真空 regression class (a future pool-serving path that forgets the persona stash) that neither the behavioral test nor the runtime WARN catches. Proven RED two ways. No GAP found: jake's NP-4 funnel routes all pooled traffic through handleSeatGroupRoute, which always stashes persona for anthropic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine computes risk_state → GET /accounts/me/routing healthy-pick overrides, but nothing applied them — the engine's whole routing influence was decorative. The proxy now polls /accounts/me/routing (mirror of the group-runtime poll: 60s, same account-JWT + control URL, keep-last-known on error) into a supervisor-scoped atomic.Value cache, and applies the seat→account override in resolveGroupCredential BEFORE the local ranked pick. Fault-isolated by construction: the override runs through the SAME resolveCandidate validity gate the ranked loop uses, so a stale override (account removed/disabled since the engine computed it — the §6.5 member-validity re-check), no-material, expired, exhausted, or undecryptable candidate falls through to the local seatassign pick. Empty cache (engine down / poll not landed / feature off) → local pick. The §11.1 invariant (engine down → local hash still serves) holds WITH the layer present. -race clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
[aikey-proxy] rename seat_group → oauth_group (env key, route fields, supervisor, vkeys flag); behavior unchanged. Refs: - oauth_group rename plan (旧→新 mapping, P1–P7): https://github.com/aikeylabs/roadmap20260320/blob/b8f2ab45258cc310f3b90eb03b6b931dcf8e66c6/技术实现/阶段7-企业生产版/20260626-oauth_group-重命名重构-分阶段实施计划.md - seat-group / OAuth pool requirement spec: https://github.com/aikeylabs/workflow/blob/7fc6a809a6af982c001188449dead5cbc3df7682/CI/requirements/2026-06-23-oauth-account-pool.md
…s / test names / trial migrate) [aikey-proxy] comment / log-string / identifier seat-group → oauth-group cleanup (handleSeatGroupRoute → handleOauthGroupRoute etc.). Refs: - oauth_group rename plan (旧→新 mapping, P1–P7): https://github.com/aikeylabs/roadmap20260320/blob/d5280ecb78d50d2539da97123d8b9a6957e74eb2/技术实现/阶段7-企业生产版/20260626-oauth_group-重命名重构-分阶段实施计划.md
…-engine-signal-emit-into-seat-group # Conflicts: # internal/supervisor/group_runtime_policy.go
… skip) Follow-up to PR #2 review (side-path items; main link unaffected). HIGH 1 — signalReporter goroutine leaked on every reload. newSignalReporter does `go r.loop()` with no stop, and EnableSignalReporting runs in buildGeneration for every generation, but generation.close() never stopped it. Each reload (aikey use, filter/quota/audit toggle, /admin/reload) leaked one loop() + 30s ticker holding a live bearer closure over the old vault reader. Add a stop channel + idempotent nil-safe Close() (final flush then return), expose Proxy.StopSignalReporting(), and call it from generation.close() alongside the existing reporter/canary teardown. HIGH 2 — routing_version == 0 first payload silently skipped. The cache's version atomic is zero-valued, so `Version() == version` treated master's first non-empty assignments at routing_version 0 as "unchanged" → never applied, no signal. Add a Stored() guard (false until the first Store) so the poll skips only once it has actually stored at this version; steady-state re-pulls skip as before. Chose the guard over the review's "WARN on non-empty skip" suggestion: in steady state master re-sends the same non-empty map at a stable version every poll and we correctly skip it, so that WARN would spam every cycle. Regressions: TestSignalReporterCloseIdempotentAndNilSafe (double/nil Close must not panic — guards the stopOnce); TestRoutingOverrideCache_StoredDistinguishes VersionZero (version-0 non-empty payload must apply). go build ./... clean; internal/proxy + internal/supervisor green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(proxy): 2 HIGH review findings from PR #2 (reporter leak + routing v0 skip)
…nto-seat-group Merge alloc-engine signal-emit + routing-override into seat-group
Delete the per-account AccountPersona normalization that disguised N pool users as one frozen SHA256(account) identity. A frozen synthetic fingerprint can't track Claude Code version bumps, so it drifts stale and becomes its own detection signal — worse than a transparent proxy. Pool requests now forward each user's real client identity unchanged; ban risk is bounded by a ≤3-users-per-account cap instead (control-master side). - delete oauth_pool_persona.go, oauth_pool_session.go + their tests - drop the stash/apply wiring (group_serve, forward_and_resolve, middleware, EventProxyGroupPersonaMissing) - add TestGroupServe_PoolPassesRealIdentity (transparent-proxy regression guard) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(P3c proxy) The proxy now consumes the engine's authoritative routing: the seat→account override IS the ledger binding, and a Blocked seat (pool at the ≤3-人/号 cap) gets 429 GROUP_POOL_FULL instead of falling back to the cap-blind local pick (which would route a 4th user onto a full account). Completes the hard ≤3 cap end-to-end. - RoutingOverrideCache: +StoreAll(version, assignments, blocked) + Blocked(seatID) (Store kept, delegates → backward compatible) - supervisor poll: parse the new `blocked` field, StoreAll it - group_serve: Blocked(seat) → 429 GROUP_POOL_FULL (+ EventProxyGroupSeatBlocked) - tests: poll blocked-parse + TestGroupServe_BlockedSeatReturns429 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gress Pass cfg.UpstreamProxy.URL (the same egress AI forwarding uses) to NewImpersonateChromeHTTPClient instead of "". launchd-spawned proxies don't inherit the login shell's HTTP_PROXY, so the "" client went DIRECT and got 403 "Request not allowed" from Anthropic's edge — only Chrome-TLS through the egress reaches the OAuth logic. Empty url ⇒ env fallback (no regression). Control-plane clients still deliberately bypass egress via internal/httpx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s proxy [aikey-proxy] /admin/upstream-proxy GET/PUT/probe with validate→persist(aikey-user.yaml)→hot-swap transport; per-member group login/writeback/runtime policy Refs: - Enterprise OAuth-pool user guide: https://github.com/aikeylabs/workflow/blob/edca0c93c646713c09c6893a76560c2df8a71edb/Docs/enterprise-delivery/OAuth账号池-使用引导.md - Update spec (vault login-status + current-routed): https://github.com/aikeylabs/roadmap20260320/blob/d3bfe4840becdd5bb9a60143577b184613678875/技术实现/update/20260630-保管库-登录态与当前路由账号实时显示.md - Update spec (seat-enroll auto-issue group VK): https://github.com/aikeylabs/roadmap20260320/blob/d3bfe4840becdd5bb9a60143577b184613678875/技术实现/update/20260630-席位组-入组自动签发组VK.md
…l-resp guard [aikey-proxy] map resolver failure to correct HTTP status (no-candidates→403, all-unusable→429, transient→503) so claude fails fast instead of backoff-hanging; nil-safe resp.StatusCode removes latent panic (SA5011) Refs: - Bugfix: proxy buildBaseEvent nil-resp panic: https://github.com/aikeylabs/workflow/blob/38364749b315fae856c6e7e896799c2aa3e95130/CI/bugfix/20260701-proxy-buildBaseEvent-nil-resp-panic.md - Bugfix: team-oauth still-syncing misleading copy: https://github.com/aikeylabs/workflow/blob/38364749b315fae856c6e7e896799c2aa3e95130/CI/bugfix/20260630-团队oauth-still-syncing-误导文案.md
…K disambiguation [aikey-proxy] fix detachedTransport dropping req context values (context.WithoutCancel) → N10 window-cap pre-cut never fired for non-streaming pool requests Refs: - Spec: RW8 reversal (self-add email+password): https://github.com/aikeylabs/roadmap20260320/blob/342a092b7865b07e16f406d75c763f0b830d1c37/技术实现/update/20260701-员工OAuth自助录入-反转RW8-email密码版.md - Spec: multi-OAuth-group one-VK-each + group-name passthrough: https://github.com/aikeylabs/roadmap20260320/blob/342a092b7865b07e16f406d75c763f0b830d1c37/技术实现/update/20260701-多OAuth组-每组一VK可选与组名透传.md - Bugfix: proxy detachedTransport drops window-cap pre-cut: https://github.com/aikeylabs/workflow/blob/edbb75c0b09b6e8a8f92627068d6a9c60750716c/CI/bugfix/20260701-proxy-detachedTransport-drops-window-cap-precut-dead.md
…fault-on sticky routing [aikey-proxy] self-heal stale control-plane after host network change: hot-swappable client registry (internal/httpx) + proactive netmon + failure-triggered selfheal, rebuilding a fresh *http.Client across all control-plane rails (CloseIdleConnections insufficient, golang/go#23427), with a guarded graceful self-restart (exit 75) backstop Refs: - Bugfix: proxy control-plane stale after network change: https://github.com/aikeylabs/workflow/blob/f99795d1ac999233133fc56cc55bd7546a5317b7/CI/bugfix/2026-07-01-proxy-control-plane-stale-after-network-change.md
…ce of truth [aikey-proxy] group runtime carries the resolved routed account consistently; add group_routed_consistency_test guarding the proxy-side pick against the snapshot Refs: - Bugfix: route-drift (part 2, split-truth → poolroute): https://github.com/aikeylabs/workflow/blob/c957d8ae791377abba7b9300ab55cf1a61cdf5e3/CI/bugfix/20260701-分配引擎默认开启-修复加号后路由漂移.md
… usage audit [aikey-proxy] stamp the selected pool account's email onto the usage event as oauth_identity (point-in-time audit; routing changes over time so a read-time join would misattribute history) Refs: - Spec: usage-audit selected-account display: https://github.com/aikeylabs/roadmap20260320/blob/9a738488d21e7e6249c8dddca4ccaa69c54ea438/技术实现/update/20260701-用量审计冗余选中账号显示.md
…andoff, install entry unification group member login-required bypass state file (statusline surface, cleared on next successful resolve) + routing-override self-heal persistence + routingwire adoption; TestMain guard Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WriteGroupRuntime / WriteConfigU64LE / WriteConfigString / ReadConfigU64LE / ReadConfigString opened the vault with the raw dbPath, bypassing the existing withBusyTimeoutDSN guard. modernc's default busy_timeout is 0, so any reader briefly holding the vault lock made these writes fail instantly with SQLITE_BUSY. The supervisor's 60s group-runtime pull cycle then aborted every round (proxy.group_runtime.write_failed "database is locked") and group VKs answered 503 "credentials are still syncing" for minutes — reproduced deterministically on the Ubuntu arm64 client leg of the seat-group regression (lock timing loses far more often on Linux than macOS, where this hid). With the guard applied the material lands on the first pull cycle and the full seat-group regression (select/sticky/switch via mock upstream) passes ALL assertions on the Ubuntu VM. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…degraded state surfaces to statusline [aikey-proxy] SyncRail declarative rail driver: re-check gate/generation/credential/control-URL every cycle (was evaluate-once-at-start → 7h silent stall on server IP drift), per-rail OK→STALE→OFFLINE transitions log with the underlying error, offline-first (state = visibility only, retries never stop); migrate routing_override + group_runtime rails (quota/compliance/audit stay legacy until Phase 2); routing-override self-heal persistence; write sync-health.json for the statusline Refs: - Spec: control-plane SyncRail framework design: https://github.com/aikeylabs/roadmap20260320/blob/ed04caeca75a932f53921f7a3c6ef12b8fa95780/技术实现/update/20260703-控制面同步框架SyncRail-技术方案.md - Spec: group routing-override self-heal persistence + settings linkage: https://github.com/aikeylabs/roadmap20260320/blob/ed04caeca75a932f53921f7a3c6ef12b8fa95780/技术实现/update/20260703-组路由override自愈持久化与settings联动.md - Spec: OAuth group-member login prompt (CLI + login_url): https://github.com/aikeylabs/roadmap20260320/blob/ed04caeca75a932f53921f7a3c6ef12b8fa95780/技术实现/update/20260703-OAuth组成员登录提示-CLI显示与login_url.md
…pe 503, pool cooldown persistence, chk_mpb_target guard [aikey-proxy] persist OAuth-pool cooldown across restart; event reporter write-reconcile-read Refs: - Principle: event-write-reconcile-read: https://github.com/aikeylabs/workflow/blob/fe705f43504cb9eaf9d50516984561dce71d37bd/CI/IDE/claude/principles/event-write-reconcile-read.md
…imeout guard
Review found the original fix's claim "all raw vault connections" was incomplete:
- internal/quota WriteLocalUsage (busiest vault writer, 5s ticks) + WriteSubjects
open the SAME vault DB file raw → still SQLITE_BUSY instantly under contention.
- events DB is genuinely multi-writer (collector Insert vs retention Prune on
separate pool conns, reload overlap, cross-process `wal vacuum`) → same gap.
- Export withBusyTimeoutDSN → vault.WithBusyTimeoutDSN (single source of truth;
no import cycle — vault has zero internal deps, quota/events now import it).
- quota/local_usage.go + policy_write.go + events/store.go: open via it (DSN-based,
since busy_timeout is per-connection and not persisted like journal_mode=WAL).
Verified: go build ./..., go test ./internal/{vault,quota,events} all pass; no
import cycle (go list -deps ./internal/quota lists vault; vault stays internal-free).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(vault): busy_timeout guard on all raw vault connections (fixes group-VK 503 on Linux)
…egradation [aikey-proxy] codex/openai provider routing + injection; orphan group-VK protocol_type fallback; group-VK pending-login防呆; cooldown test-pollution isolation fix Refs: - Spec: OAuth池 codex进池 (技术方案): https://github.com/aikeylabs/roadmap20260320/blob/845685224f712c9c1e511f13f8d32d3032b8d567/技术实现/阶段7-企业生产版/20260704-OAuth账号池-codex进池-技术方案.md - Spec: 孤儿组VK协议回落protocol_type: https://github.com/aikeylabs/roadmap20260320/blob/845685224f712c9c1e511f13f8d32d3032b8d567/技术实现/update/20260703-孤儿组VK协议回落protocol_type.md - Bugfix: group-serve 共享cooldown文件测试污染: https://github.com/aikeylabs/workflow/blob/b34b24263183289a6f1748be1dd1fcc5fc35b06a/CI/bugfix/2026-07-04-group-serve-shared-cooldown-file-test-pollution.md
…4 + gateway-unreachable degrade + SyncRail/service-status/delivery-503; over vault busy_timeout fix)
…I value) injectCodexOAuth fell back to originator=opencode (third-party tool identity) when the client set none. Align the fallback to the official codex CLI value (openai/codex codex-rs/login default_client DEFAULT_ORIGINATOR=codex_cli_rs). setIfAbsent still preserves a real codex CLI's own value; this only stops mislabelling originator-less traffic as the third-party opencode tool. Deliberately NOT injecting a synthetic UA/session here — that identity-forgery layer was removed 2026-06-29 for transparent proxy + <=3-users/account. Ref: roadmap 20260705-Codex池动态调度 D8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dexUtil) R34's reactive layer cools Codex after a 429, but the engine scored Codex blind — util_5h/util_7d were fed only from Anthropic unified-* headers, so Codex accounts read as always-healthy (never pre-switch, fanout stuck at max). parseCodexUtil reads the X-Codex-* window headers off the /responses 200 (verified live 2026-07-06) and normalizes to the SAME (util_5h, util_7d) fraction the engine already consumes: percent/100, and 5h/7d classified by window-minutes (NOT the primary/secondary name — not tied to a fixed window). Wired at the I5 enqueue by header-sniff (Anthropic vs Codex util headers are mutually exclusive), mirroring R34's reactive dispatch — no provider switch, no new endpoint/field/schema. Lights up UtilSlope/RateOverBaseline/Peak5h/Util7d for Codex (the engine-parity delta). Fixture test pins the real captured wire + an inverted-window adversarial case. Ref: roadmap 20260705-Codex池动态调度 5.1/5.2/6A. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ary one (D9) codexRateLimitReset picked the primary window's reset first, assuming primary=7d (the longer wall) per sub2api's comment. Live 2026-07-06 shows primary=5h, so when BOTH windows were exhausted it under-cooled to the shorter wall and immediately re-429'd. Compare reset DURATIONS directly and take the longest among exhausted windows — sidesteps the primary/secondary naming trap entirely. Adds a D9 regression case (both exhausted, longer wall on secondary). Ref: workflow bugfix 2026-07-06-codex-ratelimit-reset-window-by-name.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ban parity) windowPreCutDecision only read anthropic unified-* util, so Codex pool accounts got NO proactive 防封 pre-cut (reactive 429 only). Add a codex branch (header-sniff, anthropic path byte-identical): any X-Codex-*-used-percent >= the master cap → pre-cut until THAT window's reset, preferring the absolute X-Codex-*-Reset-At (verified live 2026-07-06) over the relative reset-after-seconds. No 5h/7d classification — cool until the over-cap window's own reset (same insight as the D9 fix). observedResetEpoch also gains a codex branch (soonest reset-at) so master re-rolls the randomized cap per window. Fixture tests pin the live wire. Pairs with the master change that rolls a cap for codex (D2). Ref: roadmap 20260705-Codex池动态调度 §5.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eQuotaUsage Codex /responses SSE usage frames omit the model, leaving TokenBreakdown.Model empty so the edge-summary usd lookup missed (usd=0) even though reportUsage already recorded the resolved model. Backfill breakdown.Model from the resolved model before quota accrual on both streaming and non-streaming paths. Server-side billing was already correct; this fixes the proxy's local in-flight usd estimate. Bugfix: workflow/CI/bugfix/2026-07-06-codex-pricing-summary-and-model-extraction.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex into OAuth account pool: mechanism + dynamic scheduling + pricing/login fixes
…ource-scan fence
WriteAssignmentOverride was the single raw sql.Open left behind by the
2026-07-03 busy_timeout sweep (R2). With modernc's default busy_timeout=0
the UPDATE fails instantly with SQLITE_BUSY under any concurrent vault
reader and the routing override is silently dropped.
Adds TestNoRawSQLOpenInVault: scans every production file in the package
and fails on any sql.Open("sqlite", ...) whose DSN is not wrapped with
WithBusyTimeoutDSN — this class regressed twice via hand-audit misses, so
the fence is structural rather than reviewer memory.
Found by 2026-07-07 Windows/Mac parity audit (P1-4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The proxy runs console-less on Windows, so a console-subsystem app-hook child (e.g. ai-compliance-detector) popped a visible terminal window for its whole lifetime. Applies aikeycompat.HideSpawnConsole (no-op on Unix; stdio pipes unaffected). Same class as the 2026-07-07 web-bridge window-flash bug; found by the lateral sweep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ormat Codex sends the Responses API (/v1/responses: instructions + input[], SSE frames response.output_text.delta, end marker response.completed) — but the conversation-audit extractor only understood classic Chat Completions (messages[], choices[].delta.content, [DONE]). Every Codex turn extracted to empty and OnRequestEnd skipped the record with CONTENT_EMPTY_EXTRACT (live incident 2026-07-07: team usage recorded, conversation audit empty — usage's own extractor already knew both formats, provider/openai.go documents the same split). Structure per user decision (多态 + 可扩展 + 旧 openai KEY 零影响): the openai_compatible family now owns an ordered wire-format probe table (openaiWireFormats) where each format is one entry with its three probes (prompt / delta / done). Chat Completions probes FIRST, keeping existing deployments byte-identical; the next OpenAI variant is one appended entry plus fixtures, no shared-flow edits. The completion marker fix also stops Responses turns from mislabeling as "partial". Fixtures: Responses item-array + string-input + delta/done frames, chat-completions-wins-when-messages-present, bare-model fallback; all pre-existing chat/completions fixtures untouched and green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ribution) RequestContext + ConversationRecord gain SeatID from route.SeatID — the exact field usage events already carry (reportable.go). Why: for a shared OAuth-pool VK, route.AccountID is the VK OWNER (pool creator), so audit records keyed only on owner_account_id filed an employee's codex conversations under a stranger seat row while their usage attributed correctly (live incident 2026-07-07, org 624a2488; same root as the session_xxx phantom seats seen 2026-06-17). Collector / query / web legs follow in their repos; legacy rows keep owner-only attribution (query falls back). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lind) Live incident 2026-07-08: a codex message containing "fuck" reached the model unscanned — proxy logged "no filterable content extracted; forwarded UNFILTERED" while the SAME message was captured by the conversation audit. Root cause: the compliance filter's content extractor (extractUserContent / extractFilterableContent) only understood Anthropic /v1/messages + OpenAI /v1/chat/completions (messages[]), but codex sends the Responses API (/v1/responses: instructions + input[] with input_text content parts). Unhandled → zero pieces → forwarded unfiltered → no masking, no compliance event. Same Responses-API wire-format class as the conversation-audit R20 fix, but the compliance filter is a SEPARATE code path that was still blind to it (audit and filter both needed the fix; only audit had it). - extractUserContent: falls back to input[] (item array) / input string when messages[] is absent, reusing the same user-turn skip policy via new collectUserTurns; instructions skipped like system. - collectContentField: accepts input_text blocks (Responses user parts) alongside text; output_text (model reply) still skipped by role. - extractFilterableContent (full extractor) kept coherent. - Fences: Responses input-array (scans user turns, skips instructions + assistant output), plain-string input, and mask write-back round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…solution) The master compliance-audit page resolves a seat alias/email from the event's seat identity, but the proxy only stamped tenant_id + virtual_key_id — the event's user_id is the detector's metadata.user_id (a Claude/session id), which for a shared OAuth-pool VK is NOT a seat, so pool-VK events showed a raw id instead of the employee's alias (same root as the conversation-audit seat fix). New injectSeat mirrors injectVirtualKey, stamping route.SeatID; applyInboundFilter gains a seatID param threaded from route.SeatID. Fence: TestInjectSeat (stamps seat_id, preserves detector user_id per decision A, empty/bad-json fail-safe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-link) injectSession stamps route's resolveSessionID onto team compliance events — the SAME session source the conversation-audit observer uses — so the compliance-audit drawer can deep-link to the exact conversation thread for a flagged prompt (event_id joins the turn, session_id opens its thread). Empty session (codex/no-session header) → unchanged; the UI keys those threads by event_id. applyInboundFilter gains a sessionID param. Fence: TestInjectSession. Decision 2a, 2026-07-08. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v1-128k default [aikey-proxy] new internal/sysproxy (read OS system-proxy darwin/windows/other); egress auto-follows system-proxy switch; upstream_proxy/admin/config wiring + egress integration test Refs: - Spec: 上游代理-系统代理自动感知刷新: https://github.com/aikeylabs/roadmap20260320/blob/051ecf5/技术实现/update/20260708-上游代理-系统代理自动感知刷新.md
…flight) + egress two-lane refinement [aikey-proxy] egress two-lane decision-chain (env-inherit degrade) refinement on sysproxy; admin/upstream_proxy wiring + egress integration test Refs: - Spec: 出口决策链-继承env降级细分: https://github.com/aikeylabs/roadmap20260320/blob/1b5f896/技术实现/update/20260708-出口决策链-继承env降级细分.md - Principle: outbound-egress-two-lane: https://github.com/aikeylabs/workflow/blob/8a922c0/CI/IDE/claude/principles/outbound-egress-two-lane.md
Part of the seat_group (席位组 / OAuth account pool) feature.
Group VK resolution, seatassign selection, AccountPersona normalization (pooled), group_runtime pull. Direct-bind path byte-unchanged.
🤖 Generated with Claude Code