From 06ff626efe49cef3ff8212062785b3e283bd1831 Mon Sep 17 00:00:00 2001 From: Adrian Lyjak Date: Thu, 14 May 2026 16:17:08 -0400 Subject: [PATCH] fix: projects use/get cross-org lookup (#638) Tab completion lists projects from all orgs the user can access, but `projects use ` validated against only the default org. A project from a non-default org would tab-complete fine then fail with "Project not found". The issue is `_discover_organization` running before the direct-ID path, scoping the validation query to whichever org has `is_default=True`. Moved org discovery to the interactive listing branch only. When the user passes a project ID directly, validation now queries all accessible orgs (unless they explicitly passed `--org`). Same fix applied to `projects get `. --- .changeset/fix-project-cross-org.md | 5 +++ .../src/llama_agents/cli/commands/projects.py | 27 ++++++++------ packages/llamactl/tests/test_auth_cli.py | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 .changeset/fix-project-cross-org.md diff --git a/.changeset/fix-project-cross-org.md b/.changeset/fix-project-cross-org.md new file mode 100644 index 00000000..229c7e85 --- /dev/null +++ b/.changeset/fix-project-cross-org.md @@ -0,0 +1,5 @@ +--- +"llamactl": patch +--- + +Fix `projects use ` and `projects get ` failing with "not found" for projects outside the default org diff --git a/packages/llamactl/src/llama_agents/cli/commands/projects.py b/packages/llamactl/src/llama_agents/cli/commands/projects.py index 2f2ca4e4..2ec10e1c 100644 --- a/packages/llamactl/src/llama_agents/cli/commands/projects.py +++ b/packages/llamactl/src/llama_agents/cli/commands/projects.py @@ -44,18 +44,23 @@ def get_projects(project_id: str | None, org_id: str | None, output: str) -> Non try: auth_svc = _get_service().current_auth_service() profile = validate_authenticated_profile() - if org_id is None: - org = _discover_organization(auth_svc) - if org is not None: - org_id = org.org_id - projects = _list_projects(auth_svc, org_id=org_id) if project_id: + # Look up a specific project across all accessible orgs, + # unless the user explicitly scoped with --org. + projects = _list_projects(auth_svc, org_id=org_id) projects = [ project for project in projects if project.project_id == project_id ] if not projects: raise click.ClickException(f"Project {project_id} not found") + else: + # Scope the listing to the default org when --org is not given. + if org_id is None: + org = _discover_organization(auth_svc) + if org is not None: + org_id = org.org_id + projects = _list_projects(auth_svc, org_id=org_id) if not projects and output == "text": status("no projects found") @@ -93,16 +98,13 @@ def use_project(project_id: str | None, org_id: str | None) -> None: profile = validate_authenticated_profile() try: - if org_id is None: - org = _discover_organization(auth_svc) - if org is not None: - org_id = org.org_id - if project_id and profile.project_id == project_id: return if project_id: if auth_svc.env.requires_auth: + # Validate across all accessible orgs (matching tab completion), + # unless the user explicitly scoped with --org. projects = _list_projects(auth_svc, org_id=org_id) if not next( ( @@ -117,6 +119,11 @@ def use_project(project_id: str | None, org_id: str | None) -> None: status(f"switched project {project_id}") return + # Scope the interactive listing to the default org when --org is not given. + if org_id is None: + org = _discover_organization(auth_svc) + if org is not None: + org_id = org.org_id projects = _list_projects(auth_svc, org_id=org_id) if not projects: diff --git a/packages/llamactl/tests/test_auth_cli.py b/packages/llamactl/tests/test_auth_cli.py index 2a6a3110..7f538b83 100644 --- a/packages/llamactl/tests/test_auth_cli.py +++ b/packages/llamactl/tests/test_auth_cli.py @@ -181,6 +181,42 @@ def test_auth_logout_missing() -> None: assert "Profile 'missing' not found" in result.output +def test_projects_use_validates_across_all_orgs() -> None: + """projects use should not scope validation to the default org. + + Tab completion lists projects from all orgs, so the validation path + must also search all orgs — otherwise a project from a non-default org + completes but then fails with 'Project not found'. + """ + runner = CliRunner() + cross_org_project = MagicMock(project_id="cross-org-proj-id") + with ( + patch( + "llama_agents.cli.commands.projects.validate_authenticated_profile", + return_value=MagicMock(name="p", project_id="current-proj"), + ), + patch("llama_agents.cli.config.env_service.service") as mock_service, + patch( + "llama_agents.cli.commands.projects._list_projects", + return_value=[cross_org_project], + ) as mock_list, + patch( + "llama_agents.cli.commands.projects._discover_organization", + return_value=MagicMock(org_id="default-org-id"), + ), + ): + mock_auth_svc = MagicMock() + mock_auth_svc.env.requires_auth = True + mock_service.current_auth_service.return_value = mock_auth_svc + + result = runner.invoke(app, ["projects", "use", "cross-org-proj-id"]) + assert result.exit_code == 0 + # Must call _list_projects without org scoping (org_id=None), + # not with the auto-discovered default org. + mock_list.assert_called_once_with(mock_auth_svc, org_id=None) + mock_auth_svc.set_project.assert_called_once() + + def test_projects_use_non_interactive_lists_options_and_hints() -> None: runner = CliRunner() with (