diff --git a/application/single_app/config.py b/application/single_app/config.py index d28611fe9..606e97d33 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.340" +VERSION = "0.261.341" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/functions_action_credential_runtime.py b/application/single_app/functions_action_credential_runtime.py index 797405ed7..9be8e73e9 100644 --- a/application/single_app/functions_action_credential_runtime.py +++ b/application/single_app/functions_action_credential_runtime.py @@ -18,9 +18,11 @@ def configure_action_credential_runtime(): from content_screening import access def resolve_manifest(user_id, action_ref): + scope_type, scope_id, action_id = action_ref.split(":", 2) + catalog_ref = functions_action_catalog._action_ref(scope_type, scope_id, action_id) user = functions_authentication.get_current_user_info() or {} return functions_action_catalog.resolve_action_manifest( - user_id, action_ref, settings=functions_settings.get_settings(), user_roles=user.get("roles") or [], + user_id, catalog_ref, settings=functions_settings.get_settings(), user_roles=user.get("roles") or [], ) def resolve_agent(reference, *, user_id): diff --git a/docs/explanation/features/YAMCS_ACTION.md b/docs/explanation/features/YAMCS_ACTION.md index 22604d15b..d7c2e52d0 100644 --- a/docs/explanation/features/YAMCS_ACTION.md +++ b/docs/explanation/features/YAMCS_ACTION.md @@ -179,6 +179,13 @@ back to an administrator credential. See [Personal action credentials](PERSONAL_ACTION_CREDENTIALS.md) for the framework, storage behavior, continuation, and limitations. +Fixed in version **0.261.341**: runtime credential references are translated to +the action catalog's canonical format before reauthorization. This prevents a +correctly configured personal credential source from failing with a generic +validation error before its chat card appears. Existing actions, identities, and +private bindings do not need migration. See the +[personal credential prompt fix](../fixes/YAMCS_PERSONAL_CREDENTIAL_PROMPT_FIX.md). + ## Usage Instructions Create a new action from a personal, group, or admin action surface and choose **Yamcs**. Enter the Yamcs server URL and the instance name, and optionally change the processor from the `realtime` default. diff --git a/docs/explanation/fixes/YAMCS_PERSONAL_CREDENTIAL_PROMPT_FIX.md b/docs/explanation/fixes/YAMCS_PERSONAL_CREDENTIAL_PROMPT_FIX.md new file mode 100644 index 000000000..587884b5f --- /dev/null +++ b/docs/explanation/fixes/YAMCS_PERSONAL_CREDENTIAL_PROMPT_FIX.md @@ -0,0 +1,61 @@ +# YAMCS Personal Credential Prompt Fix (0.261.341) + +Fixed in version: **0.261.341**, recorded in +`application/single_app/config.py`. + +## Issue + +A correctly configured YAMCS action using each user's personal proxy identity +could fail in chat without displaying its credential card. The agent instead +reported `Yamcs configuration or response is invalid.` No pending credential +request was created. The YAMCS server and authenticating proxy could be healthy. + +## Root Cause + +Private credential requests and bindings identify actions as +`::`. The shared action catalog resolves a canonical +`action:v1:::` reference. + +The initialized credential runtime passed its stored reference directly to the +catalog. The catalog rejected it with a `ValueError` before personal credential +resolution could create a request. The YAMCS plugin correctly kept that internal +exception out of the model response, but its generic validation result concealed +the reference mismatch from users. + +## Change + +`application/single_app/functions_action_credential_runtime.py` now splits the +stored reference at its first two separators and calls the existing catalog +reference builder before resolving the action. Splitting only twice preserves +action IDs that contain a colon. + +The catalog still reauthorizes the exact action against the current actor, +workspace ownership or membership, governance, and enabled state. Stored private +references and fingerprints are unchanged, so existing identities, bindings, and +pending interactions do not require migration. No credential is copied into a +shared action, and no frontend or route changes are needed. + +## Validation + +`functional_tests/test_action_credentials_bootstrap.py` exercises the real +runtime, catalog, and credential owners in fresh processes with network access +blocked and installed-SDK CosmosDict responses. The regression failed at the +catalog parser before the fix and passed afterward. + +Coverage includes global and personal proxy-only actions, creation of the +private pending credential challenge, colon-bearing action IDs, foreign personal +action rejection, and rejection of a group the actor has not joined. Existing +normal/optimized Python, import-order, web startup, and scheduler checks remain +in the same suite. + +Related coverage remains in `functional_tests/test_action_personal_credentials.py` +and `functional_tests/test_action_credentials_sk_continuation.py` for identity +storage, secret redaction, credential submission, SDK transport, and continuation. + +## Deployment + +Deploy version **0.261.341** or later, then invoke the saved action in chat. When +the invoking user has no confirmed personal proxy binding, chat should display +the credential card instead of returning the generic validation failure. The +hosted UI must be retested after deployment; offline regression coverage is not +a claim that an older hosted build has changed. diff --git a/functional_tests/test_action_credentials_bootstrap.py b/functional_tests/test_action_credentials_bootstrap.py index 8cb973c6a..82941c6d5 100644 --- a/functional_tests/test_action_credentials_bootstrap.py +++ b/functional_tests/test_action_credentials_bootstrap.py @@ -1,8 +1,9 @@ # test_action_credentials_bootstrap.py """Fresh-process application bootstrap and actual chat SDK mapping ownership. -Version: 0.261.337 +Version: 0.261.341 Implemented in: 0.261.333 +Runtime catalog reference regression added in: 0.261.341 Normal/optimized Python imports real modules in both orders with network blocked. The initialized web app owns private state; backup/transfer inventories exclude it. @@ -92,6 +93,57 @@ def check(value, message): pass else: raise AssertionError("Private credentials bypassed actual conversation ownership.") + from flask import g + from unittest.mock import patch + from functions_action_manifest import bind_action_origin + from action_credential_contracts import ActionCredentialsRequired + import functions_settings + catalog_settings = { + **functions_settings.get_settings(), "enable_semantic_kernel": True, + "allow_user_plugins": True, "enable_user_workspace": True, + "allow_group_plugins": True, "enable_group_workspaces": True, + "per_user_semantic_kernel": False, + } + g.conversation_id = "credential-chat" + with patch.object(functions_settings, "get_settings", return_value=catalog_settings): + for scope_type, scope_id in (("global", "global"), ("personal", "owner")): + action_id = f"credential-{scope_type}:mission" + action = { + "id": action_id, "name": "mission", "type": "yamcs", "is_enabled": True, + "endpoint": "https://mission.example.invalid", "metadata": {}, + "auth": {"type": "NoAuth"}, + "additionalFields": { + "instance": "sim", "auth_method": "none", "enable_basic_auth": True, + }, + "credential_requirements": {"version": 1, "slots": { + "proxy": {"source": "personal", "auth_type": "username_password"}, + }}, + } + if scope_type == "personal": + action["user_id"] = scope_id + action_container = getattr(config, f"cosmos_{scope_type}_actions_container") + action_container.upsert_item(action) + bound = bind_action_origin(action, scope_type, scope_id) + reference = credentials.manifest_reference(bound) + resolved = credentials.credential_services().resolve_manifest("owner", reference) + check(resolved["id"] == action_id, "Credential runtime resolved the wrong catalog action.") + with credentials.credential_continuation({"mode": "chat", "checkpoint_id": action_id}): + try: + credentials.resolve_personal_credentials(bound) + except ActionCredentialsRequired as error: + check(error.credential_request["status"] == "pending", "Credential challenge was not pending.") + requirements = error.credential_request["requirements"] + check(len(requirements) == 1 and requirements[0]["slot"] == "proxy", + "Proxy-only YAMCS requested the wrong credentials.") + else: + raise AssertionError("Missing personal proxy credentials did not create a challenge.") + for forbidden_reference in ("personal:other:mission", "group:unjoined:mission"): + try: + credentials.credential_services().resolve_manifest("owner", forbidden_reference) + except PermissionError: + pass + else: + raise AssertionError("Credential reference translation bypassed catalog authorization.") saved = chats._persist_screened_assistant({ "id": "credential-answer", "conversation_id": "credential-chat", "user_id": "owner", "role": "assistant", "content": "The retained answer.",