Skip to content

Unbind model rules automatically when deleting a bound credential - #50

Merged
maiphucgiang merged 6 commits into
mainfrom
fix/unbind-and-delete-credential
Sep 29, 2026
Merged

maiphucgiang merged 6 commits into
mainfrom
fix/unbind-and-delete-credential

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Changes

  • Add ControlStore.unbind_credential: remove an account key from every model rule's credential_ids in a single revision bump, returning each rule's previous list for rollback; identities nothing references leave the revision untouched.
  • The confirmed delete is a recoverable unit: admin_delete_guard(name, unbind=False) keeps the original 409 guard and otherwise returns the identity; the route unbinds first (capturing the rollback record), deletes the file, and on a failed removal restore_bindings puts the bindings back. restore_bindings re-adds only the deleted identity and only where a rule still matches its post-unbind state, so concurrent rebinds or rule deletions in between are preserved instead of overwritten.
  • Serialize the confirmed delete with model-rule edits through the shared admin_mutation_lock, and sweep the identity once more after a successful removal so a rebind racing from outside the lock cannot leave a rule referencing a deleted credential. The admin middleware honours the unbind=1 query flag and lets the delete route own the whole operation.
  • DELETE /admin/credentials/{name}?unbind=1 unbinds then deletes the file and pool entry; without the flag the 409 guard and its model list are unchanged.
  • WebUI delete drawer lists the bound rules, states that confirming removes those bindings and falls the rules back to automatic account selection, and renames the button to 解除绑定并删除凭证; unbound credentials keep the existing flow and copy.

Verification

  • tests/test_credential_unbind.py plus control-store and middleware regressions: only the target identity is removed with one revision bump; the guard itself never unbinds; a mocked failed removal returns 404 with bindings restored; a rollback after a concurrent rebind or rule deletion preserves the edit; a rebind racing the removal from outside the shared lock is swept; no-binding and unknown names stay on the fast path; the unflagged delete still returns 409 with the model list; the middleware skips its guard only for confirmed unbinds.
  • WebUI: 133 unit tests, formatting/lint/type checks, production build, and Playwright including a new bound-credential delete confirmation case (15 mocked + 2 real-backend integration flows) all pass; the full backend suite reports 1390 tests and 3889 subtests.
  • Local deployment on the live instance across all commits: a synthetic credential enters the pool, binding hy3 shows bindings: ["hy3"], the unflagged delete returns 409, ?unbind=1 returns 200 with the file removed and hy3 back to credential_ids: [] while still enabled; the pool returns to 8 credentials and zero-multiplier chats return 200 against the real upstream.

DELETE /admin/credentials/{name}?unbind=1 removes the credential's
account key from every model rule binding in one control-store revision
before deleting the file; without the flag the 409 guard is unchanged.
The WebUI delete drawer now lists the bound rules and asks for a single
confirmation instead of sending operators to unbind manually.

@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.

Sorry @maiphucgiang, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 18 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T22:27:29.634933Z a65a77e New commits
🔒 Security Review ✅ Completed 2026-09-28T21:45:00.362799Z ba57ae3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

本 PR 将凭证删除扩展为可确认的一次性自动解绑流程:控制库在单次事务中清理所有模型规则中的目标指纹,后端仅对显式 unbind 确认执行解绑并删除,默认删除仍阻止已绑定凭证;WebUI 同步展示影响范围并提供明确的解绑删除操作。

Sequence diagram for confirmed credential unbinding and deletion

sequenceDiagram
    actor Admin
    participant WebUI
    participant AdminAPI
    participant Management
    participant ControlStore
    participant CredentialPool

    Admin->>WebUI: Click 解除绑定并删除凭证
    WebUI->>AdminAPI: DELETE /admin/credentials/{name}?unbind=1
    AdminAPI->>Management: admin_delete_guard(name, unbind=true)
    Management->>ControlStore: unbind_credential(account_key)
    ControlStore-->>Management: Updated model rules and revision
    Management-->>AdminAPI: Allow deletion
    AdminAPI->>CredentialPool: remove_file(name)
    CredentialPool-->>AdminAPI: File removed
    AdminAPI-->>WebUI: 200 removed
Loading

Flow diagram for credential deletion confirmation

flowchart TD
    A[Open credential deletion drawer] --> B{Credential has model bindings?}
    B -- No --> C[DELETE credential]
    B -- Yes --> D[Show affected rules and automatic unbind warning]
    D --> E{Confirm automatic unbind?}
    E -- No --> F[Keep credential and bindings]
    E -- Yes --> G[DELETE credential?unbind=1]
    G --> H[unbind_credential removes account key from all rules]
    H --> I[remove_file deletes credential]
Loading

File-Level Changes

Change Details Files
实现凭证与模型规则绑定关系的一次性批量解除,并在无引用时保持控制库版本稳定。
  • 遍历所有模型规则,仅移除目标账号指纹并保留其他绑定与规则字段。
  • 将实际解绑合并为单次事务和一次 revision bump;无引用或未知指纹时无副作用。
  • 增加账号指纹校验。
app/control_store.py
tests/test_control_store.py
增加带确认参数的删除流程,在删除凭证前自动解除模型规则绑定,同时保留默认安全拦截。
  • 扩展删除守卫支持 unbind 标志;未确认的已绑定凭证仍返回 409。
  • 让管理 API 中间件识别 unbind=1 等布尔值,并将确认流程交由删除路由执行。
  • 删除路由确认后先解绑,再移除凭证文件;无绑定或未知凭证继续走原有路径。
  • 覆盖中间件绕过守卫、默认拦截、解绑后删除及快速路径行为。
app/gateway_management.py
app/admin_api.py
converter.py
tests/test_admin_api.py
tests/test_credential_unbind.py
更新 WebUI 删除确认交互,向用户展示绑定规则并明确自动解绑影响。
  • 根据凭证绑定信息展示模型规则列表及规则回退为自动选择账号的提示。
  • 绑定凭证使用 unbind=1 请求并显示“解除绑定并删除凭证”,未绑定凭证保持原确认流程。
  • 新增端到端测试验证提示内容和确认请求参数。
web/src/pages/Credentials.tsx
web/e2e/admin.spec.ts

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba57ae333a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py Outdated
pool = CONFIG.get("cred_pool")
if CONFIG.get("management") is not None:
CONFIG["management"].admin_delete_guard(os.path.basename(name))
CONFIG["management"].admin_delete_guard(os.path.basename(name), unbind=unbind)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve bindings when credential deletion fails

When unbind=true and pool.remove_file() returns false—for example, because os.unlink() encounters an OSError—this call has already committed the removal of the credential from every model rule. The endpoint then reports a failed deletion while the credential remains and its routing policy has silently broadened to automatic account selection. Make the unbind and file deletion a recoverable unit, such as by restoring the bindings when removal fails.

Useful? React with 👍 / 👎.

@maiphucgiang maiphucgiang changed the title feat: 删除被路由绑定的凭证时自动解绑(一次确认) Unbind model rules automatically when deleting a bound credential Sep 28, 2026
admin_delete_guard now only confirms the unbind and returns the
identity; the delete route strips the bindings after remove_file
succeeds, so a failed removal never silently widens routing to
automatic account selection.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 899d254679

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py Outdated
# Bindings stay untouched so a failed removal never widens routing to automatic.
raise HTTPException(status_code=404, detail={"error": {"message": f"凭据不在池中: {name}", "type": "invalid_request_error"}})
if unbind_identity is not None and CONFIG.get("management") is not None:
CONFIG["management"].admin_unbind_credential(unbind_identity)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make deletion and unbinding recoverable together

If control.unbind_credential() raises—for example because SQLite becomes read-only, full, or locked—pool.remove_file() has already unlinked the credential and pruned it from inventory. The request then fails while its credential_ids references remain; retrying cannot repair them because admin_delete_guard() no longer finds the deleted credential, leaving affected rules with an orphaned binding and no eligible account. The new post-delete ordering is fresh evidence beyond the earlier removal-failure issue: it fixes that failure direction but moves the partial update to control-store failures, so the file deletion needs to be staged/rolled back or otherwise made recoverable with the unbind.

Useful? React with 👍 / 👎.

unbind_credential now captures each rule's previous binding list and
restore_bindings puts them back in one revision bump, so a delete whose
file removal fails leaves routing exactly as it was; unbind failures
still happen before any file is removed.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 729567c47c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/control_store.py Outdated
Comment on lines +221 to +222
if rule is not None and rule.get("credential_ids") != ids:
rule["credential_ids"] = list(ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve concurrent binding edits during rollback

When credential removal fails after the unbind, another administrator can update an affected rule before this rollback runs; because _update(None) performs no revision check and this assignment restores the entire old ID list, that successful intervening edit is silently overwritten. Restore only the deleted identity when the current list still matches the post-unbind state, or make deletion, unbinding, and rollback participate in the same mutation transaction/lock.

Useful? React with 👍 / 👎.

restore_bindings re-adds only the deleted identity and only where a
rule still matches its post-unbind state, so an administrator edit or
rule deletion between the unbind and the rollback is preserved instead
of being overwritten.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47084ccaec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py Outdated
Comment on lines 2098 to 2099
rollback = CONFIG["management"].admin_unbind_credential(identity)
if pool is None or not pool.remove_file(os.path.basename(name)):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize unbinding through credential removal

When another administrator updates a model after admin_unbind_credential() returns but before remove_file() runs, checked_rule() still sees the credential in inventory and can bind it again; the deletion then succeeds and leaves that rule referencing an identity that no longer exists, so the route has no eligible credential. The confirmed delete needs to share the model mutation lock or otherwise prevent/recheck binding changes through successful file removal.

Useful? React with 👍 / 👎.

The confirmed delete now runs under the admin mutation lock shared with
model-rule updates, and a second identity sweep after a successful
removal catches rebinds from writers outside that lock, so no rule can
keep referencing a deleted credential.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a65a77e469

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread converter.py
identity = management.admin_delete_guard(os.path.basename(name), unbind=unbind)
if identity is not None:
rollback = management.admin_unbind_credential(identity)
if pool is None or not pool.remove_file(os.path.basename(name)):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Roll back unbinding when removal raises before unlink

When credential removal raises before unlinking, this expression bypasses the rollback branch and leaves the file present but its model bindings removed. This is reachable because CredentialPool.remove_file() catches only OSError, while credential_file_lock() can raise CredentialFileError for a non-regular lock file (app/credential_io.py:65). Fresh evidence in this revision is that rollback is now confined to the false-result branch; normalize pre-unlink failures to False or catch them here and restore the bindings.

Useful? React with 👍 / 👎.

@maiphucgiang
maiphucgiang merged commit 44d1244 into main Sep 29, 2026
9 checks passed
@maiphucgiang
maiphucgiang deleted the fix/unbind-and-delete-credential branch September 29, 2026 03:33
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.

1 participant