Skip to content

Exempt enterprise accounts from the personal-only growth center - #65

Merged
maiphucgiang merged 1 commit into
maiphucgiang:mainfrom
moshouhot:fix/enterprise-account-travel
Oct 17, 2026
Merged

maiphucgiang merged 1 commit into
maiphucgiang:mainfrom
moshouhot:fix/enterprise-account-travel

Conversation

@moshouhot

@moshouhot moshouhot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

An enterprise credential whose account happens to sit on a domestic CodeBuddy host is scheduled for travel by 1.3.6, and every request comes back HTTP 403 / business code 403 / growth system is only available for personal users. The credentials card then renders that refusal as the account's live state — 目录同步中 · 同步正常 · 无模型冷却 next to 阶段:状态查询 · 上游 HTTP 错误 · HTTP 403 · 业务码 403 — which reads as a broken sync rather than a permanent entitlement boundary.

Reported-by a operator with 13 credentials; exactly one card showed it, and that credential is type=ultimate with enterpriseId set.

A — The region gate never described eligibility

travel.supported(profile) accepted any cn-cli / cn-work credential. The growth center is personal-only, so the product profile alone is not the predicate: the tenant type has to be honoured before any request is issued.

supported() now takes the tenant flag, and the flag travels onto the pool entry (converter.py) so the manual (app/credential_actions.py) and automatic (_sync_credits) paths agree with the admin row (app/gateway_management.py) and the scheduler (app/model_policy.py).

An enterprise account now issues zero upstream calls: travel.perform refuses before opening a client. Verified by a test that asserts no client is constructed — not merely that the result says skipped.

B — A capability boundary was recorded as a transient failure

Even with the gate fixed, the card would have kept showing the old 403, because the ledger row written by the last real attempt was never superseded. clear_unsupported() replaces that stale refusal with the terminal exemption notice, and is idempotent: the periodic sweep checks the stored state and message first, so an exempt account costs no writes after the first pass (asserted directly).

_request() also maps the upstream wording to a personal_only reason, so an enterprise account that reaches the endpoint by any future path is labelled as a boundary in every phase — including depart, where the old branch only relabelled phase == "depart" refusals and would otherwise have surfaced the raw 403 again.

C — Why the suite never caught it

The shared fixture in tests/test_region_routing.py stamped "enterpriseId": "synthetic-enterprise" on every synthetic account, so the whole travel suite ran against enterprise tenants while treating them as personal. No test asserted that value; removing it (leaving add_account(uid, profile, enterprise="") opt-in) is what makes the corrected gate testable.

Verification

  • 4 new tests/test_travel.py cases: gate truth table, enterprise opens no client, personal_only labelling, clear_unsupported drops the stale 403 and does not rewrite on a second pass.
  • tests/test_credential_automation.py: end-to-end replay of the reported symptom — seeds the 403 ledger row, runs the sweep, asserts the row clears, the admin row reports travel_supported=false / auto_travel=false, and manual travel returns the notice without a request. A companion case pins that a personal domestic account still travels.
  • web/src/automation.test.tsx: asserts the card explains the exemption instead of blaming the region.
  • Reverting app/ + converter.py makes all 17 new assertions fail, so they are load-bearing rather than tautological.
  • Full Python suite: 26 failures both before and after, all pre-existing on pristine main (Windows tempdir permissions and deployment-config tests) — no new failures, and the relevant files go 162 → 221 tests passing.
  • tsc --noEmit clean, 147 frontend tests pass, vite build succeeds.

Docs (README, advanced, webui, both languages) now state that the growth center is personal-only and that enterprise accounts never travel.

Patch applies cleanly to 7e6bff5.

Summary by Sourcery

Exclude enterprise accounts from growth-center travel and present their ineligibility clearly across automation, administration, and the WebUI.

Bug Fixes:

  • Prevent enterprise accounts from making growth-center travel requests and replace stale upstream 403 states with a clear terminal exemption.
  • Label personal-only upstream refusals consistently as capability boundaries rather than transient travel failures.

Enhancements:

  • Propagate enterprise tenant status through credential routing, automation, administration, and manual travel flows.
  • Make unsupported-account ledger cleanup idempotent and update the WebUI to explain enterprise travel exemptions.

Documentation:

  • Document that growth-center automation is limited to domestic personal accounts and enterprise accounts never travel.

Tests:

  • Add coverage for enterprise eligibility, zero-request gating, refusal labeling, idempotent stale-state cleanup, automation behavior, and WebUI messaging.

An enterprise credential with a domestic product profile was scheduled for travel
and answered every request with HTTP 403 / business code 403 and
"growth system is only available for personal users", so the card showed
阶段:状态查询 · 上游 HTTP 错误 · HTTP 403 · 业务码 403 while the account was
in fact permanently exempt.

The region/product profile alone never described eligibility: the growth center is
personal-only. Honour the tenant type before any request and stop recording a
capability bound as the account's current travel state.

- app/travel.py: supported()/unavailable() take the tenant flag, perform() refuses
  a tenant without opening a client, and the refusal is labelled personal_only so
  the message reads as a boundary in every phase rather than a retryable outage.
- app/model_policy.py, app/gateway_management.py, app/credential_actions.py,
  converter.py: carry the tenant flag onto the pool entry and through the manual
  and automatic travel paths.
- clear_unsupported() replaces a stale upstream refusal with the terminal notice
  once, so the periodic sweep stays write-free for exempt accounts.
- The shared test fixture no longer marks every synthetic account as enterprise;
  no test asserted that value, and it contradicted the real personal-only rule.
@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR introduces enterprise-aware travel eligibility for the personal-only growth center, propagates that capability boundary through manual, automatic, administrative, and UI flows, and replaces stale upstream 403s with an idempotent exemption state. Tests cover the no-request gate, ledger cleanup, end-to-end automation behavior, personal-account regression, and frontend messaging; documentation and fixtures are updated accordingly.

Sequence diagram for enterprise travel exemption and ledger cleanup

sequenceDiagram
    participant Scheduler
    participant PoolEntry
    participant Travel
    participant Ledger
    participant Upstream

    Scheduler->>PoolEntry: read enterprise
    Scheduler->>Travel: supported(profile, enterprise)
    Travel-->>Scheduler: false
    Scheduler->>Travel: unavailable(profile, enterprise)
    Travel-->>Scheduler: terminal exemption notice
    Scheduler->>Travel: clear_unsupported(ledger, cid, notice)
    Travel->>Ledger: entry(cid)
    alt stale refusal or changed notice
        Travel->>Ledger: update_travel(cid, notice)
        Ledger-->>Travel: changed
    else exemption already recorded
        Ledger-->>Travel: unchanged
    end
    Note over Scheduler,Upstream: travel.perform is not called - no upstream client or request is created
Loading

Flow diagram for enterprise-aware travel eligibility

flowchart TD
    Entry[Credential pool entry] --> Gate["travel.supported(profile, enterprise)"]
    Gate -->|personal domestic| Paths[Manual and automatic travel paths]
    Gate -->|enterprise or non-domestic| Exempt["unavailable(profile, enterprise)"]
    Exempt --> Ledger[clear_unsupported ledger state]
    Exempt --> UI[Admin and WebUI show exemption]
    Paths --> Upstream[travel.perform and growth-center requests]
Loading

File-Level Changes

Change Details Files
Added enterprise-aware eligibility throughout travel routing and automation so personal-only growth-center operations are blocked before any upstream request.
  • Extended the travel support predicate with enterprise status while preserving profile-based region checks.
  • Propagated the enterprise flag into credential pool entries and all manual, scheduled, administrative, and policy paths.
  • Disabled enterprise travel controls and scheduling, with explicit admin and user-facing exemption messaging.
  • Prevented client construction and upstream calls for enterprise accounts.
app/travel.py
converter.py
app/credential_actions.py
app/gateway_management.py
app/model_policy.py
web/src/pages/Credentials.tsx
Converted upstream enterprise refusals and stale ledger failures into a persistent, idempotent capability exemption.
  • Classified the upstream personal-only 403 as personal_only across all travel phases, including departure.
  • Added ledger cleanup that replaces stale failures with an unavailable notice and avoids repeated writes.
  • Added end-to-end coverage for stale-state cleanup, no-request behavior, personal-account preservation, and frontend rendering.
app/travel.py
converter.py
app/credential_actions.py
tests/test_travel.py
tests/test_credential_automation.py
web/src/automation.test.tsx
Corrected test fixtures and documented the enterprise exclusion from the growth center.
  • Changed synthetic accounts to be personal by default and made enterprise tenancy explicit in fixtures.
  • Updated English and Chinese documentation for automation defaults and enterprise behavior.
tests/test_region_routing.py
README.md
README.zh-CN.md
docs/advanced.md
docs/advanced.zh-CN.md
docs/webui.md
docs/webui.zh-CN.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. Enterprise accounts are now skipped before travel requests, and the ledger is updated to show that exemption instead of a stale 403. That status survives a revert, but it is bounded and can be refreshed by running travel again.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@maiphucgiang
maiphucgiang merged commit f7bfbc8 into maiphucgiang:main Oct 17, 2026
8 checks passed
LX2000WASD added a commit to LX2000WASD/codebuddy2api that referenced this pull request Oct 17, 2026
上游跟进(maiphucgiang/codebuddy2api 96f5c14 / PR maiphucgiang#65):
成长中心仅面向个人账号,企业账号会被上游以 403
"growth system is only available for personal users" 拒绝。上游把租户类型
物化到池 entry,在发请求之前判定豁免,并标成能力边界而非可重试故障。

- app/travel.py:supported(profile, enterprise) / unavailable(profile, enterprise)
  三级文案 / 新增 clear_unsupported() 幂等清台账 / _request 识别 personal_only
  / perform(enterprise=)
- converter.py:两处池 entry 装配点写入 enterprise(缺这一步门控永不生效,
  但看起来"移植成功");_sync_credits 在 auto_travel 之前插入能力闸
- app/model_policy.py / app/gateway_management.py / app/credential_actions.py:
  自动与手工两条旅行路径都传租户标志
- app/buddy.py:成长中心 5 个端点的 personal_only 映射(上游未做)
- web:Credentials.tsx 硬编码文案改走已存在的 isEnterprise()

缺陷修复(本轮审计发现,均与上游无关):
- B3 测试依赖 UTC 墙钟:tests/test_fusion_b.py 的小时序列用例在
  UTC 00:00-09:00 必挂(写 base+9h 记录但查询上限是 now)。此前一直是
  定时炸弹,上轮采集在 09:00 后所以是绿的。改为冻结 store 时钟到 UTC 正午,
  断言一条未改。
- B1 渠道容量信号被当成账号限流:上游 6021 queue.full 是临时性排队满,
  我们此前按账号级 429 处理 → 6 个账号各撞一次就把该模型对全池下线 600 秒。
  新增 _parse_queue_full 防御式识别(512 字节上限、非 JSON 退化字节匹配),
  拦在 429 分支之前:不写 _model_fail、不熔断账号,对外 503 + Retry-After: 5。
  503 在 FAILOVER_CODES 内,换号链保持(线上 FAILOVER_MAX=2 会重放)。
- B2 过期熔断原因常驻:last_error 只在显式 reset 时清除,冷却过期后
  sync_error / last_error_code / cooldown.reason 仍会报告已失效的故障,
  诊断页按 fail_until > 0 判定恒真。三处同改(gateway_management、
  diagnostic、Diagnostics.tsx 改用 until*1000 > now);sync_error 不门控
  (它是余额/目录故障,不是熔断原因)。

并发上限可在 UI 热调:
- max_concurrent 此前不在 SCHEMA,只能改 CLI/env 后重启。补进 SCHEMA
  (0..1024,0 不限制)。同时修正真实语义缺陷:_gate() 懒建 Semaphore
  导致容量在首个请求时冻结,改非零值不生效。新增 _ResizableGate 原地改
  容量(重建会让在途 permit 悬空)配合 _active/_bypass 记账,保证
  locked() ⟺ 在途 == limit,且旁路准入的请求排空后归还 permit。
- .env.example:该键改为注释行并说明——设了就是环境变量锁定、UI 不可改;
  compose 部署恒为锁定(docker-compose.yml 透传,未动)。

验证:后端 1800 passed / 0 failed / 4204 subtests,CI 逐脚本 75/75;
前端 vp check 0 错、vitest 180 passed、playwright 21 passed。
每个修复都有反证(先删修复确认变红再还原),其中两次抓到"反证假绿"
(测试钉的是夹具而非被测代码)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants