From df57f8e269e38bf0ff772f7874938d3e326cb3b6 Mon Sep 17 00:00:00 2001 From: Daoyuan Li <94409450+DaoyuanLi2816@users.noreply.github.com> Date: Tue, 1 Sep 2026 00:37:19 -0700 Subject: [PATCH] fix(lark): preserve app secrets during credential switches (#4820) --- README.md | 2 ++ backend/packages/harness/deerflow/AGENTS.md | 10 ++++++++++ .../harness/deerflow/integrations/lark_cli.py | 2 +- backend/tests/test_lark_cli_integration.py | 16 +++++++++++----- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bc25f9a79..362ebb3fe 100644 --- a/README.md +++ b/README.md @@ -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 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. +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 the switch ends in a usable connection. diff --git a/backend/packages/harness/deerflow/AGENTS.md b/backend/packages/harness/deerflow/AGENTS.md index 71acd9905..1c2e4b74c 100644 --- a/backend/packages/harness/deerflow/AGENTS.md +++ b/backend/packages/harness/deerflow/AGENTS.md @@ -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`. +### 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 (`STARTUP_ONLY_FIELDS["logging"]`): `configure_logging()` installs the trace-context filter and enhanced formatter on root handlers only during app.py lifespan startup, diff --git a/backend/packages/harness/deerflow/integrations/lark_cli.py b/backend/packages/harness/deerflow/integrations/lark_cli.py index 7fd371832..b5e67d950 100644 --- a/backend/packages/harness/deerflow/integrations/lark_cli.py +++ b/backend/packages/harness/deerflow/integrations/lark_cli.py @@ -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) root = _lark_cli_credential_root(user_id) 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)) + _save_lark_app_config_with_cli(user_id, app_id=app_id, app_secret=app_secret, brand=brand) _revoke_lark_auth_from_snapshot(snapshot) diff --git a/backend/tests/test_lark_cli_integration.py b/backend/tests/test_lark_cli_integration.py index 6491313ac..653c91595 100644 --- a/backend/tests/test_lark_cli_integration.py +++ b/backend/tests/test_lark_cli_integration.py @@ -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") token_file = data_dir / "token.json" 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() monkeypatch.setattr( @@ -1401,11 +1403,13 @@ def test_complete_lark_config_saves_app_credentials_and_returns_status(monkeypat "user_info": {"tenant_brand": "feishu"}, }, ) - monkeypatch.setattr( - lark_cli, - "_save_lark_app_config_with_cli", - lambda user_id, **kwargs: captured.update({"user_id": user_id, **kwargs}), - ) + + def _save(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( lark_cli, "_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 revoked == ["old-token"] 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 == { "user_id": "alice", "app_id": "cli_mock",