fix(lark): preserve app secrets during credential switches (#4820)

This commit is contained in:
Daoyuan Li 2026-09-01 00:37:19 -07:00 committed by GitHub
parent b552b5015c
commit df57f8e269
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 24 additions and 6 deletions

View File

@ -886,6 +886,8 @@ is per-user (it never touches another user's credentials), validates the new
credentials through the official CLI's live tenant-token probe before replacing credentials through the official CLI's live tenant-token probe before replacing
the active app, and revokes/removes the previous app's OAuth tokens. A rejected the active app, and revokes/removes the previous app's OAuth tokens. A rejected
credential change does not supersede an in-progress setup or authorization flow. credential change does not supersede an in-progress setup or authorization flow.
The previous OAuth data is cleared before the CLI stores the replacement app,
so the new file-backed keychain secret remains available during reconnection.
DeerFlow then immediately opens browser authorization for the newly bound app so DeerFlow then immediately opens browser authorization for the newly bound app so
the switch ends in a usable connection. the switch ends in a usable connection.

View File

@ -7,6 +7,16 @@ Request trace correlation is controlled by `logging.enhance.enabled` at **both**
The same ContextVar value is injected into enhanced log records as `trace_id` and into Langfuse metadata as `deerflow_trace_id`. The same ContextVar value is injected into enhanced log records as `trace_id` and into Langfuse metadata as `deerflow_trace_id`.
### Managed Lark CLI credentials (`integrations/lark_cli.py`)
App registration and direct app switching replace the per-user Lark credential
tree transactionally. Clear the old OAuth data before running `lark-cli config
init`: on Linux that command writes the new app secret into the file-backed
keychain under the data directory, so clearing the directory afterward would
leave `config.json` with a dangling keychain reference. The transaction snapshot
still supplies the previous OAuth data for logout and restores the complete old
tree if any switch step fails.
`logging` is registered as a **restart-required** field `logging` is registered as a **restart-required** field
(`STARTUP_ONLY_FIELDS["logging"]`): `configure_logging()` installs the trace-context (`STARTUP_ONLY_FIELDS["logging"]`): `configure_logging()` installs the trace-context
filter and enhanced formatter on root handlers only during app.py lifespan startup, filter and enhanced formatter on root handlers only during app.py lifespan startup,

View File

@ -1484,8 +1484,8 @@ def _replace_lark_app_credentials_locked(user_id: str, *, app_id: str, app_secre
ensure_lark_cli_credential_tree(user_id) ensure_lark_cli_credential_tree(user_id)
root = _lark_cli_credential_root(user_id) root = _lark_cli_credential_root(user_id)
with _lark_credential_transaction(user_id, root) as snapshot: with _lark_credential_transaction(user_id, root) as snapshot:
_save_lark_app_config_with_cli(user_id, app_id=app_id, app_secret=app_secret, brand=brand)
_clear_directory_contents(lark_cli_data_dir(user_id)) _clear_directory_contents(lark_cli_data_dir(user_id))
_save_lark_app_config_with_cli(user_id, app_id=app_id, app_secret=app_secret, brand=brand)
_revoke_lark_auth_from_snapshot(snapshot) _revoke_lark_auth_from_snapshot(snapshot)

View File

@ -1390,6 +1390,8 @@ def test_complete_lark_config_saves_app_credentials_and_returns_status(monkeypat
(config_dir / "config.json").write_text("old-config", encoding="utf-8") (config_dir / "config.json").write_text("old-config", encoding="utf-8")
token_file = data_dir / "token.json" token_file = data_dir / "token.json"
token_file.write_text("old-token", encoding="utf-8") token_file.write_text("old-token", encoding="utf-8")
master_key = data_dir / "master.key"
app_secret_file = data_dir / "appsecret_cli_mock.enc"
generation = _advance_lark_flow() generation = _advance_lark_flow()
monkeypatch.setattr( monkeypatch.setattr(
@ -1401,11 +1403,13 @@ def test_complete_lark_config_saves_app_credentials_and_returns_status(monkeypat
"user_info": {"tenant_brand": "feishu"}, "user_info": {"tenant_brand": "feishu"},
}, },
) )
monkeypatch.setattr(
lark_cli, def _save(user_id, **kwargs):
"_save_lark_app_config_with_cli", captured.update({"user_id": user_id, **kwargs})
lambda user_id, **kwargs: captured.update({"user_id": user_id, **kwargs}), master_key.write_text("new-master-key", encoding="utf-8")
) app_secret_file.write_text("encrypted-app-secret", encoding="utf-8")
monkeypatch.setattr(lark_cli, "_save_lark_app_config_with_cli", _save)
monkeypatch.setattr( monkeypatch.setattr(
lark_cli, lark_cli,
"_revoke_lark_auth_from_snapshot", "_revoke_lark_auth_from_snapshot",
@ -1445,6 +1449,8 @@ def test_complete_lark_config_saves_app_credentials_and_returns_status(monkeypat
assert result.generation == generation assert result.generation == generation
assert revoked == ["old-token"] assert revoked == ["old-token"]
assert not token_file.exists() assert not token_file.exists()
assert master_key.read_text(encoding="utf-8") == "new-master-key"
assert app_secret_file.read_text(encoding="utf-8") == "encrypted-app-secret"
assert captured == { assert captured == {
"user_id": "alice", "user_id": "alice",
"app_id": "cli_mock", "app_id": "cli_mock",