Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a backend 500 occurring around agent/actions configuration flows by tightening payload persistence and schema validation, and by relaxing a post-save global-selected-agent constraint.
Changes:
- Removes post-save enforcement that required an agent matching
global_selected_agentafter add/edit. - Fixes plugin JSON schema validation by enabling
$refresolution during validation. - Ensures
save_personal_agent()persists the sanitizedcleaned_agentpayload to Cosmos.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| application/single_app/route_backend_agents.py | Removes the post-save global_selected_agent match enforcement in add/edit agent routes. |
| application/single_app/json_schema_validation.py | Adds a schema resolver so $ref (e.g., #/definitions/AuthType) resolves during plugin validation. |
| application/single_app/functions_personal_agents.py | Persists the sanitized agent payload (cleaned_agent) and keeps defaults applied consistently to that object. |
| .gitignore | Ignores additional local/temp artifacts. |
Comments suppressed due to low confidence (1)
application/single_app/route_backend_agents.py:508
- Since this PR is a bug fix, please add a short fix note under
docs/explanation/fixes/describing the root cause and validation steps (and include the current VERSION fromapplication/single_app/config.py). This makes it much easier to track why the 500 happened and how we verified it won’t regress.
result = save_global_agent(cleaned_agent)
if not result:
return jsonify({'error': 'Failed to save agent.'}), 500
log_event("Agent added", extra={"action": "add", "agent": {k: v for k, v in cleaned_agent.items() if k != 'id'}, "user": str(get_current_user_id())})
# --- HOT RELOAD TRIGGER ---
setattr(builtins, "kernel_reload_needed", True)
return jsonify({'success': True})
| # Store sensitive keys in Key Vault if enabled | ||
| agent_data = keyvault_agent_save_helper(agent_data, agent_data.get('id', ''), scope="user") | ||
| if agent_data.get('max_completion_tokens') is None: | ||
| agent_data['max_completion_tokens'] = -1 | ||
| result = cosmos_personal_agents_container.upsert_item(body=agent_data) | ||
| cleaned_agent = keyvault_agent_save_helper(cleaned_agent, cleaned_agent.get('id', ''), scope="user") | ||
| if cleaned_agent.get('max_completion_tokens') is None: | ||
| cleaned_agent['max_completion_tokens'] = -1 | ||
| result = cosmos_personal_agents_container.upsert_item(body=cleaned_agent) |
There was a problem hiding this comment.
This change fixes persistence to use the sanitized cleaned_agent, but there isn't an existing functional test that asserts save_personal_agent() actually persists sanitized fields (e.g., trimmed/normalized names) rather than the raw input. Please add a regression test under functional_tests/ that would have failed before this change and passes now, so we don't reintroduce the 500 later.
| # Validate required fields | ||
| required_fields = ['name', 'display_name', 'description', 'instructions'] | ||
| for field in required_fields: | ||
| if field not in agent_data: | ||
| agent_data[field] = '' | ||
| if field not in cleaned_agent: | ||
| cleaned_agent[field] = '' | ||
|
|
||
| # Set defaults for optional fields | ||
| agent_data.setdefault('azure_openai_gpt_deployment', '') | ||
| agent_data.setdefault('azure_openai_gpt_api_version', '') | ||
| agent_data.setdefault('azure_agent_apim_gpt_deployment', '') | ||
| agent_data.setdefault('azure_agent_apim_gpt_api_version', '') | ||
| agent_data.setdefault('enable_agent_gpt_apim', False) | ||
| agent_data.setdefault('reasoning_effort', '') | ||
| agent_data.setdefault('actions_to_load', []) | ||
| agent_data.setdefault('other_settings', {}) | ||
| cleaned_agent.setdefault('azure_openai_gpt_deployment', '') | ||
| cleaned_agent.setdefault('azure_openai_gpt_api_version', '') | ||
| cleaned_agent.setdefault('azure_agent_apim_gpt_deployment', '') | ||
| cleaned_agent.setdefault('azure_agent_apim_gpt_api_version', '') | ||
| cleaned_agent.setdefault('enable_agent_gpt_apim', False) | ||
| cleaned_agent.setdefault('reasoning_effort', '') | ||
| cleaned_agent.setdefault('actions_to_load', []) | ||
| cleaned_agent.setdefault('other_settings', {}) | ||
|
|
There was a problem hiding this comment.
The required/optional field defaulting is now applied in multiple places in this function (e.g., setdefault loops earlier, then required_fields loop, then more setdefault calls). Since this code was just touched, it would be good to consolidate the defaulting into a single block to reduce the chance of future drift between the different lists.
| if not found: | ||
| return jsonify({'error': 'There must be at least one agent matching the global_selected_agent.'}), 400 | ||
|
|
||
|
|
There was a problem hiding this comment.
With the post-save global_selected_agent enforcement removed, edit_agent can effectively rename an agent without updating settings['global_selected_agent']. If the edited agent is currently selected, this will leave global_selected_agent pointing at a non-existent name and can cause the runtime to silently fall back to a different agent. Consider either disallowing renames (require payload name == URL param) or, if renaming is allowed, update global_selected_agent to the new name when it matches the old one.
| # If the edited agent is currently selected as the global agent, | |
| # update the selection to point at the (potentially) new name. | |
| try: | |
| settings = get_settings() | |
| if settings.get('global_selected_agent') == agent_name: | |
| settings['global_selected_agent'] = cleaned_agent.get('name', agent_name) | |
| update_settings(settings) | |
| except Exception as settings_exc: | |
| # Log but do not fail the edit if updating selection settings fails. | |
| log_event( | |
| "Edit agent succeeded but updating global_selected_agent failed", | |
| level=logging.WARNING, | |
| extra={ | |
| "action": "edit", | |
| "agent_name": agent_name, | |
| "new_agent_name": cleaned_agent.get("name"), | |
| "error": str(settings_exc), | |
| }, | |
| ) |
| # Use RefResolver so $ref pointers (e.g. #/definitions/AuthType) resolve against the full schema | ||
| resolver = RefResolver.from_schema(schema) | ||
| validator = Draft7Validator(schema['definitions']['Plugin'], resolver=resolver) |
There was a problem hiding this comment.
RefResolver is deprecated in recent jsonschema releases, and this repo doesn't appear to pin or explicitly depend on jsonschema in application/single_app/requirements.txt. Since this code now relies on reference resolution for plugin validation, consider either (1) migrating to the newer referencing-based API supported by jsonschema 4.x, or (2) explicitly adding/pinning a compatible jsonschema version to requirements to avoid unexpected breakage on dependency upgrades.
(v0.237.049)
Bug Fixes
Plugin Schema Validation
$refResolution Fix$ref: '#/definitions/AuthType'because thePluginsub-schema was extracted without aRefResolver, losing access to the parent schema'sdefinitionsblock.validate_plugin()created aDraft7Validatorusing onlyschema['definitions']['Plugin'], which did not include thedefinitionssection containingAuthType. Thevalidate_agent()function already handled this correctly withRefResolver.from_schema(schema).RefResolvercreated from the full schema so that$refpointers resolve correctly during validation.json_schema_validation.py,plugin.schema.json,AuthTypedefinition,RefResolver)Personal Agent Missing
user_idFixuser_idfield, making them invisible to the user who created them.save_personal_agent()built acleaned_agentdict with the correctuser_id,id, and metadata, but the second half of the function switched to operating on the rawagent_dataparameter. The finalupsert_item(body=agent_data)saved the object that never haduser_idassigned.agent_datareferences after sanitization to usecleaned_agentconsistently, ensuringuser_idand all other fields are included in the persisted document.functions_personal_agents.py,save_personal_agent, Cosmos DB personal agents container)Global Agent Creation Blocked by
global_selected_agentCheck Fixglobal_selected_agentsetting. This check was incorrect for add operations (adding an agent can never remove the selected one) and had a side-effect bug where the agent was already persisted before the 400 error was returned.global_selected_agentenforcement from the add and edit routes. The delete route already correctly prevents deletion of the selected agent.route_backend_agents.py, global agent add/edit routes,global_selected_agentsetting)