diff --git a/plugins/nemo-agents/openapi/openapi.yaml b/plugins/nemo-agents/openapi/openapi.yaml index 5249662837..6fd8a1aea2 100644 --- a/plugins/nemo-agents/openapi/openapi.yaml +++ b/plugins/nemo-agents/openapi/openapi.yaml @@ -154,6 +154,150 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/compute-specs: + post: + tags: + - Agent Compute Specs + summary: Create Compute Spec + description: Create a new AgentComputeSpec. + operationId: create_compute_spec_apis_agents_v2_workspaces__workspace__compute_specs_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComputeSpecRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AgentComputeSpec' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agent Compute Specs + summary: List Compute Specs + description: List AgentComputeSpecs in the workspace. + operationId: list_compute_specs_apis_agents_v2_workspaces__workspace__compute_specs_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + title: Page + - name: page_size + in: query + required: false + schema: + type: integer + maximum: 100 + minimum: 1 + default: 20 + title: Page Size + - name: sort + in: query + required: false + schema: + type: string + default: -created_at + title: Sort + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/NemoListResponse_AgentComputeSpec_' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/compute-specs/{name}: + get: + tags: + - Agent Compute Specs + summary: Get Compute Spec + description: Get an AgentComputeSpec by name. + operationId: get_compute_spec_apis_agents_v2_workspaces__workspace__compute_specs__name__get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AgentComputeSpec' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - Agent Compute Specs + summary: Delete Compute Spec + description: Delete an AgentComputeSpec by name. + operationId: delete_compute_spec_apis_agents_v2_workspaces__workspace__compute_specs__name__delete + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '204': + description: Successful Response + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /apis/agents/v2/workspaces/{workspace}/deployments: post: tags: @@ -383,12 +527,13 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze: + /apis/agents/v2/workspaces/{workspace}/environment-specs: post: tags: - - Agents - summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_analyze_post + - Agent Environment Specs + summary: Create Environment Spec + description: Create a new AgentEnvironmentSpec. + operationId: create_environment_spec_apis_agents_v2_workspaces__workspace__environment_specs_post parameters: - name: workspace in: path @@ -401,14 +546,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AnalyzeJobRequest' + $ref: '#/components/schemas/CreateEnvironmentSpecRequest' responses: '201': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/AnalyzeJob' + $ref: '#/components/schemas/AgentEnvironmentSpec' '422': description: Validation Error content: @@ -417,9 +562,10 @@ paths: $ref: '#/components/schemas/HTTPValidationError' get: tags: - - Agents - summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_analyze_get + - Agent Environment Specs + summary: List Environment Specs + description: List AgentEnvironmentSpecs in the workspace. + operationId: list_environment_specs_apis_agents_v2_workspaces__workspace__environment_specs_get parameters: - name: workspace in: path @@ -432,59 +578,45 @@ paths: required: false schema: type: integer - exclusiveMinimum: 0 - description: Page number. + minimum: 1 default: 1 title: Page - description: Page number. - name: page_size in: query required: false schema: type: integer - exclusiveMinimum: 0 - description: Page size. - default: 10 + maximum: 100 + minimum: 1 + default: 20 title: Page Size - description: Page size. - name: sort in: query required: false schema: - allOf: - - $ref: '#/components/schemas/AnalyzeJobsSortField' - description: The field to sort by. To sort in decreasing order, use `-` - in front of the field name. + type: string default: -created_at - description: The field to sort by. To sort in decreasing order, use `-` in - front of the field name. - - in: query - name: filter - style: deepObject - required: false - explode: true - schema: - $ref: '#/components/schemas/AnalyzeJobsListFilter' - description: Filter jobs on various criteria. + title: Sort responses: '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/AnalyzeJobsPage' + $ref: '#/components/schemas/NemoListResponse_AgentEnvironmentSpec_' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{job}/results/{name}: + /apis/agents/v2/workspaces/{workspace}/environment-specs/{name}: get: tags: - - Agents - summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_analyze__job__results__name__get + - Agent Environment Specs + summary: Get Environment Spec + description: Get an AgentEnvironmentSpec by name. + operationId: get_environment_spec_apis_agents_v2_workspaces__workspace__environment_specs__name__get parameters: - name: workspace in: path @@ -492,12 +624,6 @@ paths: schema: type: string title: Workspace - - name: job - in: path - required: true - schema: - type: string - title: Job - name: name in: path required: true @@ -510,19 +636,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PlatformJobResultResponse' + $ref: '#/components/schemas/AgentEnvironmentSpec' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{job}/results/{name}/download: - get: + delete: tags: - - Agents - summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_analyze__job__results__name__download_get + - Agent Environment Specs + summary: Delete Environment Spec + description: Delete an AgentEnvironmentSpec by name. + operationId: delete_environment_spec_apis_agents_v2_workspaces__workspace__environment_specs__name__delete parameters: - name: workspace in: path @@ -530,12 +656,6 @@ paths: schema: type: string title: Workspace - - name: job - in: path - required: true - schema: - type: string - title: Job - name: name in: path required: true @@ -543,27 +663,21 @@ paths: type: string title: Name responses: - '200': + '204': description: Successful Response - content: - application/octet-stream: - schema: - type: string - format: binary - '404': - description: Not Found '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}: - get: + /apis/agents/v2/workspaces/{workspace}/environments: + post: tags: - - Agents - summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__get + - Agent Environments + summary: Create Environment + description: Create a new AgentEnvironment. + operationId: create_environment_apis_agents_v2_workspaces__workspace__environments_post parameters: - name: workspace in: path @@ -571,30 +685,31 @@ paths: schema: type: string title: Workspace - - name: name - in: path + requestBody: required: true - schema: - type: string - title: Name + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEnvironmentRequest' responses: - '200': + '201': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/AnalyzeJob' + $ref: '#/components/schemas/AgentEnvironment' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - delete: + get: tags: - - Agents - summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__delete + - Agent Environments + summary: List Environments + description: List AgentEnvironments in the workspace. + operationId: list_environments_apis_agents_v2_workspaces__workspace__environments_get parameters: - name: workspace in: path @@ -602,27 +717,50 @@ paths: schema: type: string title: Workspace - - name: name - in: path - required: true + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + title: Page + - name: page_size + in: query + required: false + schema: + type: integer + maximum: 100 + minimum: 1 + default: 20 + title: Page Size + - name: sort + in: query + required: false schema: type: string - title: Name + default: -created_at + title: Sort responses: - '204': + '200': description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/NemoListResponse_AgentEnvironment_' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/cancel: - post: + /apis/agents/v2/workspaces/{workspace}/environments/{name}: + get: tags: - - Agents - summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__cancel_post + - Agent Environments + summary: Get Environment + description: Get an AgentEnvironment by name. + operationId: get_environment_apis_agents_v2_workspaces__workspace__environments__name__get parameters: - name: workspace in: path @@ -642,95 +780,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AnalyzeJob' + $ref: '#/components/schemas/AgentEnvironment' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/logs: - get: + delete: tags: - - Agents - summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_analyze__name__logs_get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: name - in: path - required: true - schema: - type: string - title: Name - - name: limit - in: query - required: false - schema: - title: Limit - type: integer - - name: page_cursor - in: query - required: false - schema: - title: Page Cursor - type: string - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformJobLogPage' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/results: - get: - tags: - - Agents - summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_analyze__name__results_get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: name - in: path - required: true - schema: - type: string - title: Name - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformJobListResultResponse' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/status: - get: - tags: - - Agents - summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_analyze__name__status_get + - Agent Environments + summary: Delete Environment + description: Delete an AgentEnvironment by name. + operationId: delete_environment_apis_agents_v2_workspaces__workspace__environments__name__delete parameters: - name: workspace in: path @@ -745,120 +807,20 @@ paths: type: string title: Name responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformJobStatusResponse' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate: - post: - tags: - - Agents - summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_post - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/EvaluateJobRequest' - responses: - '201': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/EvaluateJob' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - get: - tags: - - Agents - summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_evaluate_get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: page - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page number. - default: 1 - title: Page - description: Page number. - - name: page_size - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page size. - default: 10 - title: Page Size - description: Page size. - - name: sort - in: query - required: false - schema: - allOf: - - $ref: '#/components/schemas/EvaluateJobsSortField' - description: The field to sort by. To sort in decreasing order, use `-` - in front of the field name. - default: -created_at - description: The field to sort by. To sort in decreasing order, use `-` in - front of the field name. - - in: query - name: filter - style: deepObject - required: false - explode: true - schema: - $ref: '#/components/schemas/EvaluateJobsListFilter' - description: Filter jobs on various criteria. - responses: - '200': + '204': description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/EvaluateJobsPage' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze: post: tags: - Agents summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite_post + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_analyze_post parameters: - name: workspace in: path @@ -871,14 +833,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateSuiteJobRequest' + $ref: '#/components/schemas/AnalyzeJobRequest' responses: '201': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/EvaluateSuiteJob' + $ref: '#/components/schemas/AnalyzeJob' '422': description: Validation Error content: @@ -889,7 +851,7 @@ paths: tags: - Agents summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite_get + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_analyze_get parameters: - name: workspace in: path @@ -922,7 +884,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/EvaluateSuiteJobsSortField' + - $ref: '#/components/schemas/AnalyzeJobsSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -934,7 +896,7 @@ paths: required: false explode: true schema: - $ref: '#/components/schemas/EvaluateSuiteJobsListFilter' + $ref: '#/components/schemas/AnalyzeJobsListFilter' description: Filter jobs on various criteria. responses: '200': @@ -942,19 +904,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateSuiteJobsPage' + $ref: '#/components/schemas/AnalyzeJobsPage' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{job}/results/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{job}/results/{name}: get: tags: - Agents summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__job__results__name__get + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_analyze__job__results__name__get parameters: - name: workspace in: path @@ -987,12 +949,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{job}/results/{name}/download: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{job}/results/{name}/download: get: tags: - Agents summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__job__results__name__download_get + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_analyze__job__results__name__download_get parameters: - name: workspace in: path @@ -1028,12 +990,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}: get: tags: - Agents summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__get + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__get parameters: - name: workspace in: path @@ -1053,7 +1015,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateSuiteJob' + $ref: '#/components/schemas/AnalyzeJob' '422': description: Validation Error content: @@ -1064,7 +1026,7 @@ paths: tags: - Agents summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__delete + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__delete parameters: - name: workspace in: path @@ -1087,12 +1049,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/cancel: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/cancel: post: tags: - Agents summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__cancel_post + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_analyze__name__cancel_post parameters: - name: workspace in: path @@ -1112,19 +1074,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateSuiteJob' + $ref: '#/components/schemas/AnalyzeJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/logs: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/logs: get: tags: - Agents summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__logs_get + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_analyze__name__logs_get parameters: - name: workspace in: path @@ -1163,12 +1125,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/results: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/results: get: tags: - Agents summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__results_get + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_analyze__name__results_get parameters: - name: workspace in: path @@ -1195,12 +1157,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/status: + /apis/agents/v2/workspaces/{workspace}/jobs/analyze/{name}/status: get: tags: - Agents summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__status_get + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_analyze__name__status_get parameters: - name: workspace in: path @@ -1227,12 +1189,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{job}/results/{name}: - get: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate: + post: tags: - Agents - summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate__job__results__name__get + summary: Create Job + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_post parameters: - name: workspace in: path @@ -1240,20 +1202,212 @@ paths: schema: type: string title: Workspace - - name: job - in: path - required: true - schema: - type: string - title: Job - - name: name - in: path + requestBody: required: true - schema: - type: string - title: Name + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateJobRequest' responses: - '200': + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agents + summary: List Jobs + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_evaluate_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page number. + default: 1 + title: Page + description: Page number. + - name: page_size + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page size. + default: 10 + title: Page Size + description: Page size. + - name: sort + in: query + required: false + schema: + allOf: + - $ref: '#/components/schemas/EvaluateJobsSortField' + description: The field to sort by. To sort in decreasing order, use `-` + in front of the field name. + default: -created_at + description: The field to sort by. To sort in decreasing order, use `-` in + front of the field name. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/EvaluateJobsListFilter' + description: Filter jobs on various criteria. + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateJobsPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite: + post: + tags: + - Agents + summary: Create Job + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateSuiteJobRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateSuiteJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agents + summary: List Jobs + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page number. + default: 1 + title: Page + description: Page number. + - name: page_size + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page size. + default: 10 + title: Page Size + description: Page size. + - name: sort + in: query + required: false + schema: + allOf: + - $ref: '#/components/schemas/EvaluateSuiteJobsSortField' + description: The field to sort by. To sort in decreasing order, use `-` + in front of the field name. + default: -created_at + description: The field to sort by. To sort in decreasing order, use `-` in + front of the field name. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/EvaluateSuiteJobsListFilter' + description: Filter jobs on various criteria. + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateSuiteJobsPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{job}/results/{name}: + get: + tags: + - Agents + summary: Get Job Result + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__job__results__name__get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: job + in: path + required: true + schema: + type: string + title: Job + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': description: Successful Response content: application/json: @@ -1265,12 +1419,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{job}/results/{name}/download: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{job}/results/{name}/download: get: tags: - Agents summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate__job__results__name__download_get + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__job__results__name__download_get parameters: - name: workspace in: path @@ -1306,12 +1460,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}: get: tags: - Agents summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__get + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__get parameters: - name: workspace in: path @@ -1331,7 +1485,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateJob' + $ref: '#/components/schemas/EvaluateSuiteJob' '422': description: Validation Error content: @@ -1342,7 +1496,7 @@ paths: tags: - Agents summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__delete + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__delete parameters: - name: workspace in: path @@ -1365,12 +1519,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/cancel: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/cancel: post: tags: - Agents summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__cancel_post + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__cancel_post parameters: - name: workspace in: path @@ -1390,19 +1544,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EvaluateJob' + $ref: '#/components/schemas/EvaluateSuiteJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/logs: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/logs: get: tags: - Agents summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__logs_get + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__logs_get parameters: - name: workspace in: path @@ -1441,12 +1595,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/results: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/results: get: tags: - Agents summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__results_get + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__results_get parameters: - name: workspace in: path @@ -1473,12 +1627,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/status: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate-suite/{name}/status: get: tags: - Agents summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__status_get + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_evaluate_suite__name__status_get parameters: - name: workspace in: path @@ -1505,12 +1659,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute: - post: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{job}/results/{name}: + get: tags: - Agents - summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_execute_post + summary: Get Job Result + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate__job__results__name__get parameters: - name: workspace in: path @@ -1518,133 +1672,37 @@ paths: schema: type: string title: Workspace - requestBody: + - name: job + in: path required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ExecuteJobRequest' + schema: + type: string + title: Job + - name: name + in: path + required: true + schema: + type: string + title: Name responses: - '201': + '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/ExecuteJob' + $ref: '#/components/schemas/PlatformJobResultResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - get: - tags: - - Agents - summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_execute_get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: page - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page number. - default: 1 - title: Page - description: Page number. - - name: page_size - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page size. - default: 10 - title: Page Size - description: Page size. - - name: sort - in: query - required: false - schema: - allOf: - - $ref: '#/components/schemas/ExecuteJobsSortField' - description: The field to sort by. To sort in decreasing order, use `-` - in front of the field name. - default: -created_at - description: The field to sort by. To sort in decreasing order, use `-` in - front of the field name. - - in: query - name: filter - style: deepObject - required: false - explode: true - schema: - $ref: '#/components/schemas/ExecuteJobsListFilter' - description: Filter jobs on various criteria. - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/ExecuteJobsPage' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{job}/results/{name}: - get: - tags: - - Agents - summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_execute__job__results__name__get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: job - in: path - required: true - schema: - type: string - title: Job - - name: name - in: path - required: true - schema: - type: string - title: Name - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformJobResultResponse' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{job}/results/{name}/download: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{job}/results/{name}/download: get: tags: - Agents summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_execute__job__results__name__download_get + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_evaluate__job__results__name__download_get parameters: - name: workspace in: path @@ -1680,12 +1738,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}: get: tags: - Agents summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__get + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__get parameters: - name: workspace in: path @@ -1705,7 +1763,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteJob' + $ref: '#/components/schemas/EvaluateJob' '422': description: Validation Error content: @@ -1716,7 +1774,7 @@ paths: tags: - Agents summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__delete + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__delete parameters: - name: workspace in: path @@ -1739,12 +1797,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/cancel: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/cancel: post: tags: - Agents summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__cancel_post + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__cancel_post parameters: - name: workspace in: path @@ -1764,19 +1822,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteJob' + $ref: '#/components/schemas/EvaluateJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/logs: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/logs: get: tags: - Agents summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_execute__name__logs_get + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__logs_get parameters: - name: workspace in: path @@ -1815,12 +1873,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/results: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/results: get: tags: - Agents summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_execute__name__results_get + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__results_get parameters: - name: workspace in: path @@ -1847,12 +1905,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/status: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{name}/status: get: tags: - Agents summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_execute__name__status_get + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_evaluate__name__status_get parameters: - name: workspace in: path @@ -1879,108 +1937,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize: - post: - tags: - - Agents - summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_optimize_post - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OptimizeJobRequest' - responses: - '201': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/OptimizeJob' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - get: - tags: - - Agents - summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_optimize_get - parameters: - - name: workspace - in: path - required: true - schema: - type: string - title: Workspace - - name: page - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page number. - default: 1 - title: Page - description: Page number. - - name: page_size - in: query - required: false - schema: - type: integer - exclusiveMinimum: 0 - description: Page size. - default: 10 - title: Page Size - description: Page size. - - name: sort - in: query - required: false - schema: - allOf: - - $ref: '#/components/schemas/OptimizeJobsSortField' - description: The field to sort by. To sort in decreasing order, use `-` - in front of the field name. - default: -created_at - description: The field to sort by. To sort in decreasing order, use `-` in - front of the field name. - - in: query - name: filter - style: deepObject - required: false - explode: true - schema: - $ref: '#/components/schemas/OptimizeJobsListFilter' - description: Filter jobs on various criteria. - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/OptimizeJobsPage' - '422': - description: Validation Error - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills: + /apis/agents/v2/workspaces/{workspace}/jobs/execute: post: tags: - Agents summary: Create Job - operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills_post + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_execute_post parameters: - name: workspace in: path @@ -1993,14 +1955,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJobRequest' + $ref: '#/components/schemas/ExecuteJobRequest' responses: '201': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJob' + $ref: '#/components/schemas/ExecuteJob' '422': description: Validation Error content: @@ -2011,7 +1973,7 @@ paths: tags: - Agents summary: List Jobs - operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_optimize_skills_get + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_execute_get parameters: - name: workspace in: path @@ -2044,7 +2006,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/OptimizeSkillsJobsSortField' + - $ref: '#/components/schemas/ExecuteJobsSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: -created_at @@ -2056,7 +2018,7 @@ paths: required: false explode: true schema: - $ref: '#/components/schemas/OptimizeSkillsJobsListFilter' + $ref: '#/components/schemas/ExecuteJobsListFilter' description: Filter jobs on various criteria. responses: '200': @@ -2064,19 +2026,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJobsPage' + $ref: '#/components/schemas/ExecuteJobsPage' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{job}/results/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{job}/results/{name}: get: tags: - Agents summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__job__results__name__get + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_execute__job__results__name__get parameters: - name: workspace in: path @@ -2109,12 +2071,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{job}/results/{name}/download: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{job}/results/{name}/download: get: tags: - Agents summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__job__results__name__download_get + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_execute__job__results__name__download_get parameters: - name: workspace in: path @@ -2150,12 +2112,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}: get: tags: - Agents summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__get + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__get parameters: - name: workspace in: path @@ -2175,7 +2137,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJob' + $ref: '#/components/schemas/ExecuteJob' '422': description: Validation Error content: @@ -2186,7 +2148,7 @@ paths: tags: - Agents summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__delete + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__delete parameters: - name: workspace in: path @@ -2209,12 +2171,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/cancel: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/cancel: post: tags: - Agents summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__cancel_post + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_execute__name__cancel_post parameters: - name: workspace in: path @@ -2234,19 +2196,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJob' + $ref: '#/components/schemas/ExecuteJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/logs: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/logs: get: tags: - Agents summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__logs_get + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_execute__name__logs_get parameters: - name: workspace in: path @@ -2285,12 +2247,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/results: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/results: get: tags: - Agents summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__results_get + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_execute__name__results_get parameters: - name: workspace in: path @@ -2317,12 +2279,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/status: + /apis/agents/v2/workspaces/{workspace}/jobs/execute/{name}/status: get: tags: - Agents summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__status_get + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_execute__name__status_get parameters: - name: workspace in: path @@ -2349,12 +2311,204 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{job}/results/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize: + post: + tags: + - Agents + summary: Create Job + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_optimize_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeJobRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agents + summary: List Jobs + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_optimize_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page number. + default: 1 + title: Page + description: Page number. + - name: page_size + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page size. + default: 10 + title: Page Size + description: Page size. + - name: sort + in: query + required: false + schema: + allOf: + - $ref: '#/components/schemas/OptimizeJobsSortField' + description: The field to sort by. To sort in decreasing order, use `-` + in front of the field name. + default: -created_at + description: The field to sort by. To sort in decreasing order, use `-` in + front of the field name. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/OptimizeJobsListFilter' + description: Filter jobs on various criteria. + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeJobsPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills: + post: + tags: + - Agents + summary: Create Job + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeSkillsJobRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeSkillsJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agents + summary: List Jobs + operationId: list_jobs_apis_agents_v2_workspaces__workspace__jobs_optimize_skills_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page number. + default: 1 + title: Page + description: Page number. + - name: page_size + in: query + required: false + schema: + type: integer + exclusiveMinimum: 0 + description: Page size. + default: 10 + title: Page Size + description: Page size. + - name: sort + in: query + required: false + schema: + allOf: + - $ref: '#/components/schemas/OptimizeSkillsJobsSortField' + description: The field to sort by. To sort in decreasing order, use `-` + in front of the field name. + default: -created_at + description: The field to sort by. To sort in decreasing order, use `-` in + front of the field name. + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/OptimizeSkillsJobsListFilter' + description: Filter jobs on various criteria. + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/OptimizeSkillsJobsPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{job}/results/{name}: get: tags: - Agents summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize__job__results__name__get + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__job__results__name__get parameters: - name: workspace in: path @@ -2387,12 +2541,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{job}/results/{name}/download: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{job}/results/{name}/download: get: tags: - Agents summary: Download Job Result - operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize__job__results__name__download_get + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__job__results__name__download_get parameters: - name: workspace in: path @@ -2428,12 +2582,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}: get: tags: - Agents summary: Get Job - operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__get + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__get parameters: - name: workspace in: path @@ -2453,7 +2607,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeJob' + $ref: '#/components/schemas/OptimizeSkillsJob' '422': description: Validation Error content: @@ -2464,7 +2618,7 @@ paths: tags: - Agents summary: Delete Job - operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__delete + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__delete parameters: - name: workspace in: path @@ -2487,12 +2641,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/cancel: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/cancel: post: tags: - Agents summary: Cancel Job - operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__cancel_post + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__cancel_post parameters: - name: workspace in: path @@ -2512,19 +2666,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeJob' + $ref: '#/components/schemas/OptimizeSkillsJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/logs: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/logs: get: tags: - Agents summary: Get Job Logs - operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_optimize__name__logs_get + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__logs_get parameters: - name: workspace in: path @@ -2563,12 +2717,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/results: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/results: get: tags: - Agents summary: List Job Results - operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_optimize__name__results_get + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__results_get parameters: - name: workspace in: path @@ -2595,12 +2749,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/status: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}/status: get: tags: - Agents summary: Get Job Status - operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_optimize__name__status_get + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_optimize_skills__name__status_get parameters: - name: workspace in: path @@ -2627,13 +2781,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/sessions: - post: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{job}/results/{name}: + get: tags: - - Agent Sessions - summary: Create Session - description: Create a logical conversation for an existing deployment. - operationId: create_session_apis_agents_v2_workspaces__workspace__sessions_post + - Agents + summary: Get Job Result + operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize__job__results__name__get parameters: - name: workspace in: path @@ -2641,32 +2794,37 @@ paths: schema: type: string title: Workspace - requestBody: + - name: job + in: path required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateSessionRequest' + schema: + type: string + title: Job + - name: name + in: path + required: true + schema: + type: string + title: Name responses: - '201': + '200': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/AgentSession' + $ref: '#/components/schemas/PlatformJobResultResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{job}/results/{name}/download: get: tags: - - Agent Sessions - summary: List Sessions - description: List sessions in a workspace, optionally filtered by deployment - ID. - operationId: list_sessions_apis_agents_v2_workspaces__workspace__sessions_get + - Agents + summary: Download Job Result + operationId: download_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize__job__results__name__download_get parameters: - name: workspace in: path @@ -2674,62 +2832,40 @@ paths: schema: type: string title: Workspace - - name: page - in: query - required: false - schema: - type: integer - minimum: 1 - default: 1 - title: Page - - name: page_size - in: query - required: false - schema: - type: integer - maximum: 100 - minimum: 1 - default: 20 - title: Page Size - - name: sort - in: query - required: false + - name: job + in: path + required: true schema: type: string - default: -created_at - title: Sort - - in: query - name: filter - style: deepObject - required: false - explode: true + title: Job + - name: name + in: path + required: true schema: - $ref: '#/components/schemas/SessionFilter' - description: 'Filter results on various criteria. Supports bracket notation - (?filter[field][$op]=value), JSON ({"field":{"$op":"value"}}), and text - syntax (field:"value"). Operators: $eq (exact match), $like (substring), - $lt, $lte, $gt, $gte (comparison), $in, $nin (set membership), $and, $or, - $not (logical). Default operator is $eq.' + type: string + title: Name responses: '200': description: Successful Response content: - application/json: + application/octet-stream: schema: - $ref: '#/components/schemas/NemoListResponse_AgentSession_' + type: string + format: binary + '404': + description: Not Found '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/sessions/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}: get: tags: - - Agent Sessions - summary: Get Session - description: Get a session by name. - operationId: get_session_apis_agents_v2_workspaces__workspace__sessions__name__get + - Agents + summary: Get Job + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__get parameters: - name: workspace in: path @@ -2749,7 +2885,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentSession' + $ref: '#/components/schemas/OptimizeJob' '422': description: Validation Error content: @@ -2758,10 +2894,9 @@ paths: $ref: '#/components/schemas/HTTPValidationError' delete: tags: - - Agent Sessions - summary: Delete Session - description: Permanently delete a session by name. - operationId: delete_session_apis_agents_v2_workspaces__workspace__sessions__name__delete + - Agents + summary: Delete Job + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__delete parameters: - name: workspace in: path @@ -2784,13 +2919,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/sessions/{name}/close: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/cancel: post: tags: - - Agent Sessions - summary: Close Session - description: Close a session. Closing an already-closed session is idempotent. - operationId: close_session_apis_agents_v2_workspaces__workspace__sessions__name__close_post + - Agents + summary: Cancel Job + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__cancel_post parameters: - name: workspace in: path @@ -2810,81 +2944,460 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentSession' + $ref: '#/components/schemas/OptimizeJob' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' -components: - schemas: - Agent: - properties: - name: - type: string - title: Name - description: Entity name within the workspace - default: '' - workspace: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/logs: + get: + tags: + - Agents + summary: Get Job Logs + operationId: get_job_logs_apis_agents_v2_workspaces__workspace__jobs_optimize__name__logs_get + parameters: + - name: workspace + in: path + required: true + schema: type: string - pattern: ^[\w\-\+.@:]+$ title: Workspace - description: Workspace identifier - project: - title: Project - description: The name of the project associated with this entity. - type: string - description: - type: string - title: Description - description: Human-readable description of the agent. - default: '' - config: - additionalProperties: true - type: object - title: Config - description: Agent config dict interpreted according to config_format. - config_format: - type: string - title: Config Format - description: platform-internal schema version tag for the agent config dict. - `nat-workflow-v1` is the default legacy NAT workflow format; `nemo-agents-spec-v1` - identifies the Platform-owned agent.yaml spec format. - default: nat-workflow-v1 - id: + - name: name + in: path + required: true + schema: type: string - title: Id - readOnly: true - created_at: - title: Created At - readOnly: true + title: Name + - name: limit + in: query + required: false + schema: + title: Limit + type: integer + - name: page_cursor + in: query + required: false + schema: + title: Page Cursor type: string - format: date-time - created_by: - title: Created By - readOnly: true - nullable: true + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobLogPage' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/results: + get: + tags: + - Agents + summary: List Job Results + operationId: list_job_results_apis_agents_v2_workspaces__workspace__jobs_optimize__name__results_get + parameters: + - name: workspace + in: path + required: true + schema: type: string - updated_at: - title: Updated At - readOnly: true + title: Workspace + - name: name + in: path + required: true + schema: type: string - format: date-time - updated_by: - title: Updated By - readOnly: true - nullable: true + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobListResultResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{name}/status: + get: + tags: + - Agents + summary: Get Job Status + operationId: get_job_status_apis_agents_v2_workspaces__workspace__jobs_optimize__name__status_get + parameters: + - name: workspace + in: path + required: true + schema: type: string - entity_id: + title: Workspace + - name: name + in: path + required: true + schema: type: string - title: Entity Id - description: Alias for id for backwards compatibility. - readOnly: true - parent: - title: Parent - description: Parent entity ID for nested entities. - readOnly: true + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformJobStatusResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/sessions: + post: + tags: + - Agent Sessions + summary: Create Session + description: Create a logical conversation for an existing deployment. + operationId: create_session_apis_agents_v2_workspaces__workspace__sessions_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSessionRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AgentSession' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - Agent Sessions + summary: List Sessions + description: List sessions in a workspace, optionally filtered by deployment + ID. + operationId: list_sessions_apis_agents_v2_workspaces__workspace__sessions_get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + title: Page + - name: page_size + in: query + required: false + schema: + type: integer + maximum: 100 + minimum: 1 + default: 20 + title: Page Size + - name: sort + in: query + required: false + schema: + type: string + default: -created_at + title: Sort + - in: query + name: filter + style: deepObject + required: false + explode: true + schema: + $ref: '#/components/schemas/SessionFilter' + description: 'Filter results on various criteria. Supports bracket notation + (?filter[field][$op]=value), JSON ({"field":{"$op":"value"}}), and text + syntax (field:"value"). Operators: $eq (exact match), $like (substring), + $lt, $lte, $gt, $gte (comparison), $in, $nin (set membership), $and, $or, + $not (logical). Default operator is $eq.' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/NemoListResponse_AgentSession_' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/sessions/{name}: + get: + tags: + - Agent Sessions + summary: Get Session + description: Get a session by name. + operationId: get_session_apis_agents_v2_workspaces__workspace__sessions__name__get + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AgentSession' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - Agent Sessions + summary: Delete Session + description: Permanently delete a session by name. + operationId: delete_session_apis_agents_v2_workspaces__workspace__sessions__name__delete + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '204': + description: Successful Response + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /apis/agents/v2/workspaces/{workspace}/sessions/{name}/close: + post: + tags: + - Agent Sessions + summary: Close Session + description: Close a session. Closing an already-closed session is idempotent. + operationId: close_session_apis_agents_v2_workspaces__workspace__sessions__name__close_post + parameters: + - name: workspace + in: path + required: true + schema: + type: string + title: Workspace + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/AgentSession' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + Agent: + properties: + name: + type: string + title: Name + description: Entity name within the workspace + default: '' + workspace: + type: string + pattern: ^[\w\-\+.@:]+$ + title: Workspace + description: Workspace identifier + project: + title: Project + description: The name of the project associated with this entity. + type: string + description: + type: string + title: Description + description: Human-readable description of the agent. + default: '' + config: + additionalProperties: true + type: object + title: Config + description: Agent config dict interpreted according to config_format. + config_format: + type: string + title: Config Format + description: platform-internal schema version tag for the agent config dict. + `nat-workflow-v1` is the default legacy NAT workflow format; `nemo-agents-spec-v1` + identifies the Platform-owned agent.yaml spec format. + default: nat-workflow-v1 + id: + type: string + title: Id + readOnly: true + created_at: + title: Created At + readOnly: true + type: string + format: date-time + created_by: + title: Created By + readOnly: true + nullable: true + type: string + updated_at: + title: Updated At + readOnly: true + type: string + format: date-time + updated_by: + title: Updated By + readOnly: true + nullable: true + type: string + entity_id: + type: string + title: Entity Id + description: Alias for id for backwards compatibility. + readOnly: true + parent: + title: Parent + description: Parent entity ID for nested entities. + readOnly: true + type: string + db_version: + type: integer + title: Db Version + description: Database version of the entity for optimistic locking. + readOnly: true + type: object + required: + - workspace + - id + - created_at + - created_by + - updated_at + - updated_by + - entity_id + - parent + - db_version + title: Agent + description: "An agent definition \u2014 stores agent config and metadata.\n\ + \nEntity type: ``agent``\nPrimary lookup: by ``name`` within a ``workspace``.\n\ + \nThe agent's spec files live at the locations returned by\n:func:`agent_spec_file_ref`\ + \ and :func:`agent_config_file_ref` \u2014 they\nare **not** stored on the\ + \ entity because the paths are fully derivable\nfrom ``(workspace, name)``." + AgentComputeSpec: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + resources: + allOf: + - $ref: '#/components/schemas/ComputeResources' + description: k8s-style resource requests/limits for the execute container. + name: + type: string + title: Name + description: Entity name within the workspace + default: '' + workspace: + type: string + pattern: ^[\w\-\+.@:]+$ + title: Workspace + description: Workspace identifier + project: + title: Project + description: The name of the project associated with this entity. + type: string + id: + type: string + title: Id + readOnly: true + created_at: + title: Created At + readOnly: true + type: string + format: date-time + created_by: + title: Created By + readOnly: true + nullable: true + type: string + updated_at: + title: Updated At + readOnly: true + type: string + format: date-time + updated_by: + title: Updated By + readOnly: true + nullable: true + type: string + entity_id: + type: string + title: Entity Id + description: Alias for id for backwards compatibility. + readOnly: true + parent: + title: Parent + description: Parent entity ID for nested entities. + readOnly: true type: string db_version: type: integer @@ -2902,12 +3415,13 @@ components: - entity_id - parent - db_version - title: Agent - description: "An agent definition \u2014 stores agent config and metadata.\n\ - \nEntity type: ``agent``\nPrimary lookup: by ``name`` within a ``workspace``.\n\ - \nThe agent's spec files live at the locations returned by\n:func:`agent_spec_file_ref`\ - \ and :func:`agent_config_file_ref` \u2014 they\nare **not** stored on the\ - \ entity because the paths are fully derivable\nfrom ``(workspace, name)``." + title: AgentComputeSpec + description: 'A reusable compute spec (k8s-style resource requests/limits). + + + Entity type: ``agent_compute_spec`` + + Referenced by an AgentEnvironment''s ``compute_spec`` (by name or inline).' AgentDeployment: properties: name: @@ -2933,8 +3447,33 @@ components: additionalProperties: true type: object title: Config - description: Resolved agent config with IGW URL injected, written when the - deployment is created. + description: Resolved agent config with IGW URL injected and any referenced + environment spec merged in, written when the deployment is created. + environment: + anyOf: + - type: string + title: Reference + description: A reference to AgentEnvironmentInline. + - $ref: '#/components/schemas/AgentEnvironmentInline' + title: Environment + description: '"workspace/name" ref to an AgentEnvironment, an inline environment, + or None. Snapshotted at create time for provenance; the resolved values + live in config/compute/secrets.' + compute: + allOf: + - $ref: '#/components/schemas/ComputeSpecInline' + description: Resolved compute snapshot from the referenced environment. + Compiled into the container resources for docker/k8s modes; ignored for + subprocess. + secrets: + additionalProperties: + type: string + type: object + title: Secrets + description: Resolved secret env references from the referenced environment, + as ENV_VAR_NAME -> 'workspace/secret-name'. Compiled into secret-backed + container env vars (never plaintext) for docker/k8s modes; ignored for + subprocess. status: type: string enum: @@ -2959,43 +3498,312 @@ components: default: subprocess endpoint: type: string - title: Endpoint - description: Subprocess loopback endpoint of the agent process (e.g. http://localhost:9001). - default: '' - endpoints: - items: - $ref: '#/components/schemas/Endpoint' - type: array - title: Endpoints - description: Routable endpoints for container modes, projected from the - deployments-plugin Deployment. Empty for subprocess mode (which uses 'endpoint'). - image: + title: Endpoint + description: Subprocess loopback endpoint of the agent process (e.g. http://localhost:9001). + default: '' + endpoints: + items: + $ref: '#/components/schemas/Endpoint' + type: array + title: Endpoints + description: Routable endpoints for container modes, projected from the + deployments-plugin Deployment. Empty for subprocess mode (which uses 'endpoint'). + image: + type: string + title: Image + description: Container image for docker/k8s modes. Empty for subprocess; + falls back to AgentsConfig.deployments.default_image. + default: '' + plugin_deployment: + type: string + title: Plugin Deployment + description: Name of the linked nemo-deployments Deployment entity. Defaults + to this deployment's name when empty (set by the controller on create). + default: '' + port: + type: integer + title: Port + description: Port the agent process is listening on. + default: 0 + pid: + type: integer + title: Pid + description: OS process ID of the agent subprocess. + default: 0 + error: + type: string + title: Error + description: Error message if status is 'failed'. + default: '' + id: + type: string + title: Id + readOnly: true + created_at: + title: Created At + readOnly: true + type: string + format: date-time + created_by: + title: Created By + readOnly: true + nullable: true + type: string + updated_at: + title: Updated At + readOnly: true + type: string + format: date-time + updated_by: + title: Updated By + readOnly: true + nullable: true + type: string + entity_id: + type: string + title: Entity Id + description: Alias for id for backwards compatibility. + readOnly: true + parent: + title: Parent + description: Parent entity ID for nested entities. + readOnly: true + type: string + db_version: + type: integer + title: Db Version + description: Database version of the entity for optimistic locking. + readOnly: true + type: object + required: + - workspace + - id + - created_at + - created_by + - updated_at + - updated_by + - entity_id + - parent + - db_version + title: AgentDeployment + description: "A running (or pending) deployment of an Agent.\n\nEntity type:\ + \ ``agent_deployment``\nLifecycle: pending \u2192 starting \u2192 running\ + \ | failed.\nThe :class:`~nemo_agents_plugin.runner.controller.AgentDeploymentController`\n\ + drives state transitions by reconciling this entity against the\n:class:`~nemo_agents_plugin.runner.backend.RunnerBackend`." + AgentEnvironment: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + environment_spec: + anyOf: + - type: string + title: Reference + description: A reference to EnvironmentSpecInline. + - $ref: '#/components/schemas/EnvironmentSpecInline' + title: Environment Spec + description: '"workspace/name" ref to an AgentEnvironmentSpec, an inline + spec, or None.' + compute_spec: + anyOf: + - type: string + title: Reference + description: A reference to ComputeSpecInline. + - $ref: '#/components/schemas/ComputeSpecInline' + title: Compute Spec + description: '"workspace/name" ref to an AgentComputeSpec, an inline spec, + or None.' + name: + type: string + title: Name + description: Entity name within the workspace + default: '' + workspace: + type: string + pattern: ^[\w\-\+.@:]+$ + title: Workspace + description: Workspace identifier + project: + title: Project + description: The name of the project associated with this entity. + type: string + id: + type: string + title: Id + readOnly: true + created_at: + title: Created At + readOnly: true + type: string + format: date-time + created_by: + title: Created By + readOnly: true + nullable: true + type: string + updated_at: + title: Updated At + readOnly: true + type: string + format: date-time + updated_by: + title: Updated By + readOnly: true + nullable: true + type: string + entity_id: + type: string + title: Entity Id + description: Alias for id for backwards compatibility. + readOnly: true + parent: + title: Parent + description: Parent entity ID for nested entities. + readOnly: true + type: string + db_version: + type: integer + title: Db Version + description: Database version of the entity for optimistic locking. + readOnly: true + type: object + required: + - workspace + - id + - created_at + - created_by + - updated_at + - updated_by + - entity_id + - parent + - db_version + title: AgentEnvironment + description: 'A composition of an environment spec and a compute spec. + + + Entity type: ``agent_environment`` + + The single thing an AgentDeployment references. Each part is a + + ``ref | inline | None`` union so specs can be authored once and reused.' + AgentEnvironmentInline: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + environment_spec: + anyOf: + - type: string + title: Reference + description: A reference to EnvironmentSpecInline. + - $ref: '#/components/schemas/EnvironmentSpecInline' + title: Environment Spec + description: '"workspace/name" ref to an AgentEnvironmentSpec, an inline + spec, or None.' + compute_spec: + anyOf: + - type: string + title: Reference + description: A reference to ComputeSpecInline. + - $ref: '#/components/schemas/ComputeSpecInline' + title: Compute Spec + description: '"workspace/name" ref to an AgentComputeSpec, an inline spec, + or None.' + type: object + title: AgentEnvironmentInline + description: 'Inline AgentEnvironment - a composition of environment + compute + specs. + + + Each part is a ``ref | inline | None`` union: a ``"workspace/name"`` string + + references a stored spec entity, an object provides the spec inline, and + + ``None`` omits it. (A ``sandbox_spec`` is out of scope for now and omitted.)' + AgentEnvironmentSpec: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + env: + additionalProperties: + type: string + type: object + title: Env + description: Plaintext, non-secret env vars. + secrets: + additionalProperties: + type: string + type: object + title: Secrets + description: ENV_VAR_NAME -> Secrets-service/plugin ref. + model_provider_override: + allOf: + - $ref: '#/components/schemas/ModelProviderOverride' + description: Set only to point at a non-IGW external model provider. + provider: + type: string + title: Provider + description: local | docker | opensandbox | k8s. + default: local + workspace_path: + title: Workspace Path + description: Workspace path visible to the harness. + type: string + artifacts_path: + title: Artifacts Path + description: Provider-specific artifact output location. + type: string + control_location: + title: Control Location + description: external_control | in_env_control. + type: string + ownership: + title: Ownership + description: caller_owned | fabric_owned. + type: string + connection: + additionalProperties: true + type: object + title: Connection + description: Provider connection metadata (server url, cred ref, namespace). + metadata: + additionalProperties: true + type: object + title: Metadata + description: Consumer-provided passthrough metadata. + settings: + additionalProperties: true + type: object + title: Settings + description: Provider-specific settings. + mcp: + additionalProperties: + $ref: '#/components/schemas/McpFulfillment' + type: object + title: Mcp + description: server-name -> fulfillment (url/env/secrets) for an Agent-declared + MCP dependency. + name: type: string - title: Image - description: Container image for docker/k8s modes. Empty for subprocess; - falls back to AgentsConfig.deployments.default_image. + title: Name + description: Entity name within the workspace default: '' - plugin_deployment: + workspace: type: string - title: Plugin Deployment - description: Name of the linked nemo-deployments Deployment entity. Defaults - to this deployment's name when empty (set by the controller on create). - default: '' - port: - type: integer - title: Port - description: Port the agent process is listening on. - default: 0 - pid: - type: integer - title: Pid - description: OS process ID of the agent subprocess. - default: 0 - error: + pattern: ^[\w\-\+.@:]+$ + title: Workspace + description: Workspace identifier + project: + title: Project + description: The name of the project associated with this entity. type: string - title: Error - description: Error message if status is 'failed'. - default: '' id: type: string title: Id @@ -3046,11 +3854,13 @@ components: - entity_id - parent - db_version - title: AgentDeployment - description: "A running (or pending) deployment of an Agent.\n\nEntity type:\ - \ ``agent_deployment``\nLifecycle: pending \u2192 starting \u2192 running\ - \ | failed.\nThe :class:`~nemo_agents_plugin.runner.controller.AgentDeploymentController`\n\ - drives state transitions by reconciling this entity against the\n:class:`~nemo_agents_plugin.runner.backend.RunnerBackend`." + title: AgentEnvironmentSpec + description: 'A reusable environment spec (the dependencies an agent reaches). + + + Entity type: ``agent_environment_spec`` + + Referenced by an AgentEnvironment''s ``environment_spec`` (by name or inline).' AgentSession: properties: name: @@ -3346,6 +4156,44 @@ components: - updated_at - -updated_at title: AnalyzeJobsSortField + ComputeResources: + properties: + limits: + additionalProperties: + type: string + type: object + title: Limits + description: k8s resource limits (e.g. cpu, memory, nvidia.com/gpu). + requests: + additionalProperties: + type: string + type: object + title: Requests + description: k8s resource requests. + type: object + title: ComputeResources + description: 'Kubernetes-style resource requests/limits. + + + Mirrors ``nemo_deployments_plugin.entities.ResourceRequirements`` so the + + agents entity schema does not depend on the deployments plugin. Compiled + + into the execute container''s resources for container deployment modes.' + ComputeSpecInline: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + resources: + allOf: + - $ref: '#/components/schemas/ComputeResources' + description: k8s-style resource requests/limits for the execute container. + type: object + title: ComputeSpecInline + description: Inline compute spec - the resources an invocation runs with. CreateAgentRequest: properties: name: @@ -3373,6 +4221,26 @@ components: - config title: CreateAgentRequest description: Request body for ``POST /v2/workspaces/{workspace}/agents``. + CreateComputeSpecRequest: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + resources: + allOf: + - $ref: '#/components/schemas/ComputeResources' + description: k8s-style resource requests/limits for the execute container. + name: + type: string + title: Name + description: Unique compute-spec name within the workspace. + type: object + required: + - name + title: CreateComputeSpecRequest + description: Request body for ``POST /v2/workspaces/{workspace}/compute-specs``. CreateDeploymentRequest: properties: agent: @@ -3398,11 +4266,130 @@ components: title: Image description: Container image for docker/k8s modes. Ignored for subprocess. default: '' + environment: + anyOf: + - type: string + title: Reference + description: A reference to AgentEnvironmentInline. + - $ref: '#/components/schemas/AgentEnvironmentInline' + title: Environment + description: 'Optional AgentEnvironment: a "workspace/name" ref, an inline + environment, or None. Resolved and snapshotted onto the deployment at + create time.' type: object required: - agent title: CreateDeploymentRequest description: Request body for ``POST /v2/workspaces/{workspace}/deployments``. + CreateEnvironmentRequest: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + environment_spec: + anyOf: + - type: string + title: Reference + description: A reference to EnvironmentSpecInline. + - $ref: '#/components/schemas/EnvironmentSpecInline' + title: Environment Spec + description: '"workspace/name" ref to an AgentEnvironmentSpec, an inline + spec, or None.' + compute_spec: + anyOf: + - type: string + title: Reference + description: A reference to ComputeSpecInline. + - $ref: '#/components/schemas/ComputeSpecInline' + title: Compute Spec + description: '"workspace/name" ref to an AgentComputeSpec, an inline spec, + or None.' + name: + type: string + title: Name + description: Unique environment name within the workspace. + type: object + required: + - name + title: CreateEnvironmentRequest + description: Request body for ``POST /v2/workspaces/{workspace}/environments``. + CreateEnvironmentSpecRequest: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + env: + additionalProperties: + type: string + type: object + title: Env + description: Plaintext, non-secret env vars. + secrets: + additionalProperties: + type: string + type: object + title: Secrets + description: ENV_VAR_NAME -> Secrets-service/plugin ref. + model_provider_override: + allOf: + - $ref: '#/components/schemas/ModelProviderOverride' + description: Set only to point at a non-IGW external model provider. + provider: + type: string + title: Provider + description: local | docker | opensandbox | k8s. + default: local + workspace_path: + title: Workspace Path + description: Workspace path visible to the harness. + type: string + artifacts_path: + title: Artifacts Path + description: Provider-specific artifact output location. + type: string + control_location: + title: Control Location + description: external_control | in_env_control. + type: string + ownership: + title: Ownership + description: caller_owned | fabric_owned. + type: string + connection: + additionalProperties: true + type: object + title: Connection + description: Provider connection metadata (server url, cred ref, namespace). + metadata: + additionalProperties: true + type: object + title: Metadata + description: Consumer-provided passthrough metadata. + settings: + additionalProperties: true + type: object + title: Settings + description: Provider-specific settings. + mcp: + additionalProperties: + $ref: '#/components/schemas/McpFulfillment' + type: object + title: Mcp + description: server-name -> fulfillment (url/env/secrets) for an Agent-declared + MCP dependency. + name: + type: string + title: Name + description: Unique environment-spec name within the workspace. + type: object + required: + - name + title: CreateEnvironmentSpecRequest + description: Request body for ``POST /v2/workspaces/{workspace}/environment-specs``. CreateSessionRequest: properties: deployment_id: @@ -3490,6 +4477,85 @@ components: projected without the agents plugin depending on that plugin at the entity-schema layer.' + EnvironmentSpecInline: + properties: + description: + type: string + title: Description + description: Human-readable description. + default: '' + env: + additionalProperties: + type: string + type: object + title: Env + description: Plaintext, non-secret env vars. + secrets: + additionalProperties: + type: string + type: object + title: Secrets + description: ENV_VAR_NAME -> Secrets-service/plugin ref. + model_provider_override: + allOf: + - $ref: '#/components/schemas/ModelProviderOverride' + description: Set only to point at a non-IGW external model provider. + provider: + type: string + title: Provider + description: local | docker | opensandbox | k8s. + default: local + workspace_path: + title: Workspace Path + description: Workspace path visible to the harness. + type: string + artifacts_path: + title: Artifacts Path + description: Provider-specific artifact output location. + type: string + control_location: + title: Control Location + description: external_control | in_env_control. + type: string + ownership: + title: Ownership + description: caller_owned | fabric_owned. + type: string + connection: + additionalProperties: true + type: object + title: Connection + description: Provider connection metadata (server url, cred ref, namespace). + metadata: + additionalProperties: true + type: object + title: Metadata + description: Consumer-provided passthrough metadata. + settings: + additionalProperties: true + type: object + title: Settings + description: Provider-specific settings. + mcp: + additionalProperties: + $ref: '#/components/schemas/McpFulfillment' + type: object + title: Mcp + description: server-name -> fulfillment (url/env/secrets) for an Agent-declared + MCP dependency. + type: object + title: EnvironmentSpecInline + description: 'Inline environment spec - the dependencies and configuration an + agent reaches. + + + This is the fulfillment half of a request/fulfill split: the Agent declares + + the dependencies it needs; the EnvironmentSpec provides concrete endpoints + + and secret references. It compiles into the agent.yaml / FabricConfig and + + the injected process environment.' EvaluateAgentSpec: properties: agent: @@ -4227,6 +5293,89 @@ components: title: LogLine description: One line shaped to match ``PlatformJobLog`` so Studio's LogViewer renders it as-is. + McpFulfillment: + properties: + url: + type: string + title: Url + description: Endpoint the environment provides for this MCP server. + env: + additionalProperties: + type: string + type: object + title: Env + description: Non-secret env for the MCP server. + secrets: + additionalProperties: + type: string + type: object + title: Secrets + description: ENV_NAME -> Secrets-service ref, merged into the MCP server + env at compile. + type: object + required: + - url + title: McpFulfillment + description: 'EnvironmentSpec-side fulfillment for one MCP server the Agent + declares. + + + The Agent DECLARES an MCP dependency by name; the EnvironmentSpec PROVIDES + + the url + env + secrets for that same name. Matched by server-name key at + + compile time; ``secrets`` are merged into the server''s ``env``.' + ModelProviderOverride: + properties: + base_url: + type: string + title: Base Url + description: External model-provider endpoint. + api_key: + title: Api Key + description: Secrets-service ref for the provider API key (only needed for + external providers). + type: string + provider: + title: Provider + description: Provider selector (e.g. "openai", "anthropic"). + type: string + type: object + required: + - base_url + title: ModelProviderOverride + description: 'Exceptional external model-provider override. + + + Null in the normal case: model selection is on the Agent and the provider + + URL is the Inference Gateway (auto-injected). Set ONLY to point the agent + at + + a non-IGW external provider endpoint.' + NemoListResponse_AgentComputeSpec_: + properties: + data: + items: + $ref: '#/components/schemas/AgentComputeSpec' + type: array + title: Data + pagination: + allOf: + - $ref: '#/components/schemas/PaginationData' + description: "Pagination metadata \u2014 page, page_size, total_results,\ + \ etc." + sort: + title: Sort + description: Sort field applied to this result set (e.g. '-created_at'). + type: string + filter: + title: Filter + description: Filter criteria echoed back from the request. + type: object + required: + - data + title: NemoListResponse_AgentComputeSpec_ NemoListResponse_AgentDeployment_: properties: data: @@ -4250,6 +5399,52 @@ components: required: - data title: NemoListResponse_AgentDeployment_ + NemoListResponse_AgentEnvironmentSpec_: + properties: + data: + items: + $ref: '#/components/schemas/AgentEnvironmentSpec' + type: array + title: Data + pagination: + allOf: + - $ref: '#/components/schemas/PaginationData' + description: "Pagination metadata \u2014 page, page_size, total_results,\ + \ etc." + sort: + title: Sort + description: Sort field applied to this result set (e.g. '-created_at'). + type: string + filter: + title: Filter + description: Filter criteria echoed back from the request. + type: object + required: + - data + title: NemoListResponse_AgentEnvironmentSpec_ + NemoListResponse_AgentEnvironment_: + properties: + data: + items: + $ref: '#/components/schemas/AgentEnvironment' + type: array + title: Data + pagination: + allOf: + - $ref: '#/components/schemas/PaginationData' + description: "Pagination metadata \u2014 page, page_size, total_results,\ + \ etc." + sort: + title: Sort + description: Sort field applied to this result set (e.g. '-created_at'). + type: string + filter: + title: Filter + description: Filter criteria echoed back from the request. + type: object + required: + - data + title: NemoListResponse_AgentEnvironment_ NemoListResponse_AgentSession_: properties: data: diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/agent_config.py b/plugins/nemo-agents/src/nemo_agents_plugin/agent_config.py index c75a683d8d..f48cd68cf3 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/agent_config.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/agent_config.py @@ -4,10 +4,10 @@ """Platform-owned agent.yaml config models for NeMo Agents. These models back Agent.config when config_format is nemo-agents-spec-v1. -RFC122 proposes first-class environment_spec, sandbox_spec, and harness_spec -fields on Agent; until those shapes are finalized, this config keeps those -inputs together in the versioned Agent.config payload and can be migrated once -the RFC122 contract lands. +First-class environment_spec, sandbox_spec, and harness_spec fields on Agent +are planned; until those shapes are finalized, this config keeps those inputs +together in the versioned Agent.config payload and can be migrated once that +contract lands. """ from __future__ import annotations @@ -50,6 +50,14 @@ class EnvironmentConfig(BaseModel): workspace: str = "./workspace" artifacts: str = "./artifacts" settings: dict[str, Any] = Field(default_factory=dict) + # Fabric environment mirror fields. Additive with backward-compatible + # defaults; populated when an AgentEnvironmentSpec is merged at deploy time and + # forwarded into FabricConfig.environment by the translator. + env: dict[str, str] = Field(default_factory=dict) + control_location: str | None = None + ownership: str | None = None + connection: dict[str, Any] = Field(default_factory=dict) + metadata: dict[str, Any] = Field(default_factory=dict) class TelemetryConfig(BaseModel): diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/_perms.py b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/_perms.py index 387af2e8ec..cf7bda0cfd 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/_perms.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/_perms.py @@ -39,3 +39,24 @@ class SessionPerms(PermissionSet, namespace="agents.sessions"): class GatewayPerms(PermissionSet, namespace="agents.gateway"): INVOKE = perm("Invoke a deployed agent through the gateway proxy") + + +class EnvironmentPerms(PermissionSet, namespace="agents.environments"): + CREATE = perm("Create agent environments") + LIST = perm("List agent environments") + READ = perm("Read an agent environment") + DELETE = perm("Delete an agent environment") + + +class EnvironmentSpecPerms(PermissionSet, namespace="agents.environment-specs"): + CREATE = perm("Create agent environment specs") + LIST = perm("List agent environment specs") + READ = perm("Read an agent environment spec") + DELETE = perm("Delete an agent environment spec") + + +class ComputeSpecPerms(PermissionSet, namespace="agents.compute-specs"): + CREATE = perm("Create agent compute specs") + LIST = perm("List agent compute specs") + READ = perm("Read an agent compute spec") + DELETE = perm("Delete an agent compute spec") diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py index ab82944f13..47062a32a7 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py @@ -28,8 +28,17 @@ from nemo_agents_plugin.entities import ( Agent, AgentDeployment, + AgentEnvironmentInline, + EnvironmentSpecInline, is_container_deployment_mode, ) +from nemo_agents_plugin.environment_resolution import ( + EnvironmentResolutionError, + MergedEnvironment, + ResolvedEnvironment, + merge_environment_spec_into_agent_config, + resolve_environment, +) from nemo_agents_plugin.schema import ( CreateDeploymentRequest, DeploymentFilter, @@ -83,12 +92,25 @@ async def create_deployment( # Platform-owned agent specs stay strict and are translated by the runner. resolved_config = _resolve_deployment_config(agent, workspace=workspace) - # 4. Create the entity with status "pending" + # 4. Resolve and snapshot the referenced AgentEnvironment. The environment + # spec is merged into the resolved config (Agent-config-wins precedence); the + # compute spec and secret-env references are snapshotted for the container + # backend. Once created, a deployment is not kept in sync with the underlying + # environment entities. + resolved_environment = await _resolve_deployment_environment( + body.environment, workspace=workspace, entity_client=entity_client + ) + merged = _merge_environment(resolved_config, resolved_environment.environment_spec) + + # 5. Create the entity with status "pending" deployment = AgentDeployment( name=deployment_name, workspace=workspace, agent=body.agent, - config=resolved_config, + config=merged.config, + environment=body.environment, + compute=resolved_environment.compute_spec, + secrets=merged.secrets, status="pending", deployment_mode=body.deployment_mode, image=body.image, @@ -120,6 +142,28 @@ def _resolve_deployment_config(agent: Agent, *, workspace: str) -> dict[str, Any raise HTTPException(status_code=400, detail=str(exc)) from exc +async def _resolve_deployment_environment( + environment: str | AgentEnvironmentInline | None, + *, + workspace: str, + entity_client: NemoEntitiesClient, +) -> ResolvedEnvironment: + try: + return await resolve_environment(environment, workspace=workspace, entity_client=entity_client) + except EnvironmentResolutionError as exc: + # 422: the request is syntactically valid but references an environment + # (or one of its specs) that cannot be resolved (e.g. a missing entity). + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +def _merge_environment(config: dict[str, Any], env_spec: EnvironmentSpecInline | None) -> MergedEnvironment: + try: + return merge_environment_spec_into_agent_config(config, env_spec) + except EnvironmentResolutionError as exc: + # 422: e.g. a secret env var bound to conflicting references. + raise HTTPException(status_code=422, detail=str(exc)) from exc + + @router.get("/deployments", response_model=DeploymentPage, tags=["Agent Deployments"]) @scope.read @path_rule( diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/environments.py b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/environments.py new file mode 100644 index 0000000000..6dfead11d7 --- /dev/null +++ b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/environments.py @@ -0,0 +1,366 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CRUD routes for AgentEnvironment, AgentEnvironmentSpec, and AgentComputeSpec. + +Mounted under ``/apis/agents/v2/workspaces/{workspace}`` at: +- ``/environments`` (AgentEnvironment) +- ``/environment-specs`` (AgentEnvironmentSpec) +- ``/compute-specs`` (AgentComputeSpec) + +Each collection is a thin CRUD surface over the generic entity client, matching +the Agent/Deployment route conventions (mandatory ``@path_rule`` authz, generic +NemoEntitiesClient, 404/409 mapping). +""" + +from __future__ import annotations + +import logging +from typing import TypeVar + +from fastapi import APIRouter, Depends, HTTPException, Query +from nemo_agents_plugin.api.v2._perms import ComputeSpecPerms, EnvironmentPerms, EnvironmentSpecPerms +from nemo_agents_plugin.api.v2.dependencies import get_entity_client +from nemo_agents_plugin.authz import scope +from nemo_agents_plugin.entities import AgentComputeSpec, AgentEnvironment, AgentEnvironmentSpec +from nemo_agents_plugin.schema import ( + ComputeSpecFilter, + ComputeSpecPage, + CreateComputeSpecRequest, + CreateEnvironmentRequest, + CreateEnvironmentSpecRequest, + EnvironmentFilter, + EnvironmentPage, + EnvironmentSpecFilter, + EnvironmentSpecPage, +) +from nemo_platform_plugin.api.filters import make_filter_obj_dep +from nemo_platform_plugin.authz import CallerKind, path_rule +from nemo_platform_plugin.entity import NemoEntity +from nemo_platform_plugin.entity_client import NemoEntitiesClient, NemoEntityConflictError, NemoEntityNotFoundError +from nemo_platform_plugin.schema import NemoFilter, NemoListResponse, PaginationData + +logger = logging.getLogger(__name__) + +# Generics for the shared CRUD helpers below: one entity type, its list-response +# page type, and its query filter type. +EntityT = TypeVar("EntityT", bound=NemoEntity) +PageT = TypeVar("PageT", bound=NemoListResponse) +FilterT = TypeVar("FilterT", bound=NemoFilter) + +router = APIRouter() + +_environment_filter_dep = make_filter_obj_dep(EnvironmentFilter) +_environment_spec_filter_dep = make_filter_obj_dep(EnvironmentSpecFilter) +_compute_spec_filter_dep = make_filter_obj_dep(ComputeSpecFilter) + + +# --------------------------------------------------------------------------- +# AgentEnvironment +# --------------------------------------------------------------------------- + + +@router.post("/environments", response_model=AgentEnvironment, status_code=201, tags=["Agent Environments"]) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentPerms.CREATE]) +async def create_environment( + workspace: str, + body: CreateEnvironmentRequest, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentEnvironment: + """Create a new AgentEnvironment.""" + environment = AgentEnvironment( + name=body.name, + workspace=workspace, + description=body.description, + environment_spec=body.environment_spec, + compute_spec=body.compute_spec, + ) + return await _create_entity(entity_client, environment, kind="environment", name=body.name, workspace=workspace) + + +@router.get("/environments", response_model=EnvironmentPage, tags=["Agent Environments"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentPerms.LIST]) +async def list_environments( + workspace: str, + page: int = Query(default=1, ge=1), + page_size: int = Query(default=20, ge=1, le=100), + sort: str = Query(default="-created_at"), + filter: EnvironmentFilter = Depends(_environment_filter_dep), + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> EnvironmentPage: + """List AgentEnvironments in the workspace.""" + return await _list_entities( + entity_client, + AgentEnvironment, + EnvironmentPage, + workspace=workspace, + page=page, + page_size=page_size, + sort=sort, + filter=filter, + kind="environments", + ) + + +@router.get("/environments/{name}", response_model=AgentEnvironment, tags=["Agent Environments"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentPerms.READ]) +async def get_environment( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentEnvironment: + """Get an AgentEnvironment by name.""" + return await _get_entity(entity_client, AgentEnvironment, name=name, workspace=workspace, kind="environment") + + +@router.delete("/environments/{name}", status_code=204, tags=["Agent Environments"]) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentPerms.DELETE]) +async def delete_environment( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> None: + """Delete an AgentEnvironment by name.""" + await _delete_entity(entity_client, AgentEnvironment, name=name, workspace=workspace, kind="environment") + + +# --------------------------------------------------------------------------- +# AgentEnvironmentSpec +# --------------------------------------------------------------------------- + + +@router.post( + "/environment-specs", response_model=AgentEnvironmentSpec, status_code=201, tags=["Agent Environment Specs"] +) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentSpecPerms.CREATE]) +async def create_environment_spec( + workspace: str, + body: CreateEnvironmentSpecRequest, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentEnvironmentSpec: + """Create a new AgentEnvironmentSpec.""" + spec = AgentEnvironmentSpec(**body.model_dump(), workspace=workspace) + return await _create_entity(entity_client, spec, kind="environment spec", name=body.name, workspace=workspace) + + +@router.get("/environment-specs", response_model=EnvironmentSpecPage, tags=["Agent Environment Specs"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentSpecPerms.LIST]) +async def list_environment_specs( + workspace: str, + page: int = Query(default=1, ge=1), + page_size: int = Query(default=20, ge=1, le=100), + sort: str = Query(default="-created_at"), + filter: EnvironmentSpecFilter = Depends(_environment_spec_filter_dep), + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> EnvironmentSpecPage: + """List AgentEnvironmentSpecs in the workspace.""" + return await _list_entities( + entity_client, + AgentEnvironmentSpec, + EnvironmentSpecPage, + workspace=workspace, + page=page, + page_size=page_size, + sort=sort, + filter=filter, + kind="environment specs", + ) + + +@router.get("/environment-specs/{name}", response_model=AgentEnvironmentSpec, tags=["Agent Environment Specs"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentSpecPerms.READ]) +async def get_environment_spec( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentEnvironmentSpec: + """Get an AgentEnvironmentSpec by name.""" + return await _get_entity( + entity_client, AgentEnvironmentSpec, name=name, workspace=workspace, kind="environment spec" + ) + + +@router.delete("/environment-specs/{name}", status_code=204, tags=["Agent Environment Specs"]) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[EnvironmentSpecPerms.DELETE]) +async def delete_environment_spec( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> None: + """Delete an AgentEnvironmentSpec by name.""" + await _delete_entity(entity_client, AgentEnvironmentSpec, name=name, workspace=workspace, kind="environment spec") + + +# --------------------------------------------------------------------------- +# AgentComputeSpec +# --------------------------------------------------------------------------- + + +@router.post("/compute-specs", response_model=AgentComputeSpec, status_code=201, tags=["Agent Compute Specs"]) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[ComputeSpecPerms.CREATE]) +async def create_compute_spec( + workspace: str, + body: CreateComputeSpecRequest, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentComputeSpec: + """Create a new AgentComputeSpec.""" + spec = AgentComputeSpec(**body.model_dump(), workspace=workspace) + return await _create_entity(entity_client, spec, kind="compute spec", name=body.name, workspace=workspace) + + +@router.get("/compute-specs", response_model=ComputeSpecPage, tags=["Agent Compute Specs"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[ComputeSpecPerms.LIST]) +async def list_compute_specs( + workspace: str, + page: int = Query(default=1, ge=1), + page_size: int = Query(default=20, ge=1, le=100), + sort: str = Query(default="-created_at"), + filter: ComputeSpecFilter = Depends(_compute_spec_filter_dep), + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> ComputeSpecPage: + """List AgentComputeSpecs in the workspace.""" + return await _list_entities( + entity_client, + AgentComputeSpec, + ComputeSpecPage, + workspace=workspace, + page=page, + page_size=page_size, + sort=sort, + filter=filter, + kind="compute specs", + ) + + +@router.get("/compute-specs/{name}", response_model=AgentComputeSpec, tags=["Agent Compute Specs"]) +@scope.read +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[ComputeSpecPerms.READ]) +async def get_compute_spec( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> AgentComputeSpec: + """Get an AgentComputeSpec by name.""" + return await _get_entity(entity_client, AgentComputeSpec, name=name, workspace=workspace, kind="compute spec") + + +@router.delete("/compute-specs/{name}", status_code=204, tags=["Agent Compute Specs"]) +@scope.write +@path_rule(callers=[CallerKind.PRINCIPAL], permissions=[ComputeSpecPerms.DELETE]) +async def delete_compute_spec( + workspace: str, + name: str, + entity_client: NemoEntitiesClient = Depends(get_entity_client), +) -> None: + """Delete an AgentComputeSpec by name.""" + await _delete_entity(entity_client, AgentComputeSpec, name=name, workspace=workspace, kind="compute spec") + + +# --------------------------------------------------------------------------- +# Shared CRUD helpers +# --------------------------------------------------------------------------- + + +async def _create_entity( + entity_client: NemoEntitiesClient, + entity: EntityT, + *, + kind: str, + name: str, + workspace: str, +) -> EntityT: + try: + return await entity_client.create(entity) + except NemoEntityConflictError as exc: + raise HTTPException( + status_code=409, + detail=f"Agent {kind} '{name}' already exists in workspace '{workspace}'.", + ) from exc + except Exception as exc: + logger.exception("Failed to create agent %s '%s'", kind, name) + raise HTTPException(status_code=500, detail=f"Failed to create agent {kind}.") from exc + + +async def _list_entities( + entity_client: NemoEntitiesClient, + entity_type: type[EntityT], + page_type: type[PageT], + *, + workspace: str, + page: int, + page_size: int, + sort: str, + filter: FilterT, + kind: str, +) -> PageT: + filter_dict = filter if isinstance(filter, dict) else filter.model_dump(exclude_none=True) + try: + result = await entity_client.list( + entity_type, + workspace=workspace, + page=page, + page_size=page_size, + sort=sort, + filter_obj=filter_dict or None, + ) + except Exception as exc: + logger.exception("Failed to list agent %s in workspace '%s'", kind, workspace) + raise HTTPException(status_code=500, detail=f"Failed to list agent {kind}.") from exc + + pagination = PaginationData.model_validate(result.pagination.model_dump()) if result.pagination else None + return page_type(data=result.data, pagination=pagination, sort=sort, filter=filter) + + +async def _get_entity( + entity_client: NemoEntitiesClient, + entity_type: type[EntityT], + *, + name: str, + workspace: str, + kind: str, +) -> EntityT: + try: + return await entity_client.get(entity_type, name=name, workspace=workspace) + except NemoEntityNotFoundError as exc: + raise HTTPException( + status_code=404, + detail=f"Agent {kind} '{name}' not found in workspace '{workspace}'.", + ) from exc + except Exception as exc: + logger.exception("Failed to get agent %s '%s'", kind, name) + raise HTTPException(status_code=500, detail=f"Failed to get agent {kind}.") from exc + + +async def _delete_entity( + entity_client: NemoEntitiesClient, + entity_type: type[EntityT], + *, + name: str, + workspace: str, + kind: str, +) -> None: + try: + await entity_client.delete(entity_type, name=name, workspace=workspace) + except NemoEntityNotFoundError as exc: + raise HTTPException( + status_code=404, + detail=f"Agent {kind} '{name}' not found in workspace '{workspace}'.", + ) from exc + except NemoEntityConflictError as exc: + raise HTTPException( + status_code=409, + detail=f"Agent {kind} '{name}' was modified by another request in workspace '{workspace}'.", + ) from exc + except Exception as exc: + logger.exception("Failed to delete agent %s '%s'", kind, name) + raise HTTPException(status_code=500, detail=f"Failed to delete agent {kind}.") from exc diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/entities.py b/plugins/nemo-agents/src/nemo_agents_plugin/entities.py index 894e5bf5d0..7f3ba89e85 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/entities.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/entities.py @@ -58,6 +58,164 @@ class Endpoint(BaseModel): protocol: Literal["http", "https", "grpc", "tcp"] = "http" +# --------------------------------------------------------------------------- +# AgentEnvironment composition +# --------------------------------------------------------------------------- +# +# An AgentDeployment (and, later, an AgentInvocationJob) runs against an +# AgentEnvironment: a composition of an EnvironmentSpec (the dependencies an +# agent reaches - model endpoints, secrets, env vars, MCP servers) and a +# ComputeSpec (k8s-style resource requests/limits). Each part varies +# independently and is referenced by ``ref | inline | None`` so a spec can be +# authored once and reused across many Environments. +# +# The specs are also first-class entities (``agent_environment_spec``, +# ``agent_compute_spec``, ``agent_environment``) with their own CRUD APIs. The +# inline BaseModels below are the shared shape: an entity embeds the inline +# fields, and an AgentEnvironment field accepts either a ``"workspace/name"`` +# ref string or the inline model. +# +# Environment values compile into two targets at deploy time: the on-disk +# agent.yaml / FabricConfig (env vars, MCP, model provider) and, for container +# modes, the deployments-plugin Container.resources (compute). See +# :mod:`nemo_agents_plugin.environment_resolution` for the merge + snapshot. + + +class ComputeResources(BaseModel): + """Kubernetes-style resource requests/limits. + + Mirrors ``nemo_deployments_plugin.entities.ResourceRequirements`` so the + agents entity schema does not depend on the deployments plugin. Compiled + into the execute container's resources for container deployment modes. + """ + + limits: dict[str, str] = Field( + default_factory=dict, + description="k8s resource limits (e.g. cpu, memory, nvidia.com/gpu).", + ) + requests: dict[str, str] = Field( + default_factory=dict, + description="k8s resource requests.", + ) + + +class ComputeSpecInline(BaseModel): + """Inline compute spec - the resources an invocation runs with.""" + + description: str = Field(default="", description="Human-readable description.") + resources: ComputeResources = Field( + default_factory=ComputeResources, + description="k8s-style resource requests/limits for the execute container.", + ) + + +class ModelProviderOverride(BaseModel): + """Exceptional external model-provider override. + + Null in the normal case: model selection is on the Agent and the provider + URL is the Inference Gateway (auto-injected). Set ONLY to point the agent at + a non-IGW external provider endpoint. + """ + + base_url: str = Field(description="External model-provider endpoint.") + api_key: str | None = Field( + default=None, + description="Secrets-service ref for the provider API key (only needed for external providers).", + ) + provider: str | None = Field( + default=None, + description='Provider selector (e.g. "openai", "anthropic").', + ) + + +class McpFulfillment(BaseModel): + """EnvironmentSpec-side fulfillment for one MCP server the Agent declares. + + The Agent DECLARES an MCP dependency by name; the EnvironmentSpec PROVIDES + the url + env + secrets for that same name. Matched by server-name key at + compile time; ``secrets`` are merged into the server's ``env``. + """ + + url: str = Field(description="Endpoint the environment provides for this MCP server.") + env: dict[str, str] = Field(default_factory=dict, description="Non-secret env for the MCP server.") + secrets: dict[str, str] = Field( + default_factory=dict, + description="ENV_NAME -> Secrets-service ref, merged into the MCP server env at compile.", + ) + + +class EnvironmentSpecInline(BaseModel): + """Inline environment spec - the dependencies and configuration an agent reaches. + + This is the fulfillment half of a request/fulfill split: the Agent declares + the dependencies it needs; the EnvironmentSpec provides concrete endpoints + and secret references. It compiles into the agent.yaml / FabricConfig and + the injected process environment. + """ + + description: str = Field(default="", description="Human-readable description.") + + # Env vars -> injected into the runtime process env (not authored on disk). + env: dict[str, str] = Field(default_factory=dict, description="Plaintext, non-secret env vars.") + + # Secrets -> Secrets-service refs, injected as secret-backed env vars. + secrets: dict[str, str] = Field( + default_factory=dict, + description="ENV_VAR_NAME -> Secrets-service/plugin ref.", + ) + + # External model-provider override (exceptional; null in the normal IGW case). + model_provider_override: ModelProviderOverride | None = Field( + default=None, + description="Set only to point at a non-IGW external model provider.", + ) + + # Fabric environment mirror -> compiles into FabricConfig.environment. + # NOTE: ``workspace_path`` (the harness workspace path) is deliberately named + # to avoid colliding with the NeMo entity ``workspace`` (tenant) field that + # AgentEnvironmentSpec inherits from EntityBase. ``artifacts_path`` carries a + # matching ``_path`` suffix for symmetry. + provider: str = Field(default="local", description="local | docker | opensandbox | k8s.") + workspace_path: str | None = Field(default=None, description="Workspace path visible to the harness.") + artifacts_path: str | None = Field(default=None, description="Provider-specific artifact output location.") + control_location: str | None = Field( + default=None, + description="external_control | in_env_control.", + ) + ownership: str | None = Field(default=None, description="caller_owned | fabric_owned.") + connection: dict[str, Any] = Field( + default_factory=dict, + description="Provider connection metadata (server url, cred ref, namespace).", + ) + metadata: dict[str, Any] = Field(default_factory=dict, description="Consumer-provided passthrough metadata.") + settings: dict[str, Any] = Field(default_factory=dict, description="Provider-specific settings.") + + # MCP fulfillment -> merged into FabricConfig.mcp.servers. by server-name key. + mcp: dict[str, McpFulfillment] = Field( + default_factory=dict, + description="server-name -> fulfillment (url/env/secrets) for an Agent-declared MCP dependency.", + ) + + +class AgentEnvironmentInline(BaseModel): + """Inline AgentEnvironment - a composition of environment + compute specs. + + Each part is a ``ref | inline | None`` union: a ``"workspace/name"`` string + references a stored spec entity, an object provides the spec inline, and + ``None`` omits it. (A ``sandbox_spec`` is out of scope for now and omitted.) + """ + + description: str = Field(default="", description="Human-readable description.") + environment_spec: str | EnvironmentSpecInline | None = Field( + default=None, + description='"workspace/name" ref to an AgentEnvironmentSpec, an inline spec, or None.', + ) + compute_spec: str | ComputeSpecInline | None = Field( + default=None, + description='"workspace/name" ref to an AgentComputeSpec, an inline spec, or None.', + ) + + # --------------------------------------------------------------------------- # Canonical spec storage convention # --------------------------------------------------------------------------- @@ -142,8 +300,33 @@ def agent_config_file_ref(workspace: str, agent_name: str) -> FilesetRef: return FilesetRef(f"{workspace}/{agent_spec_fileset_name(agent_name)}#{AGENT_CONFIG_FILENAME}") -# TODO: RFC-122 will add specs for environment, sandbox, and harness. Add those -# specs to this object once finalized. +class AgentComputeSpec(NemoEntity, ComputeSpecInline, entity_type="agent_compute_spec"): + """A reusable compute spec (k8s-style resource requests/limits). + + Entity type: ``agent_compute_spec`` + Referenced by an AgentEnvironment's ``compute_spec`` (by name or inline). + """ + + +class AgentEnvironmentSpec(NemoEntity, EnvironmentSpecInline, entity_type="agent_environment_spec"): + """A reusable environment spec (the dependencies an agent reaches). + + Entity type: ``agent_environment_spec`` + Referenced by an AgentEnvironment's ``environment_spec`` (by name or inline). + """ + + +class AgentEnvironment(NemoEntity, AgentEnvironmentInline, entity_type="agent_environment"): + """A composition of an environment spec and a compute spec. + + Entity type: ``agent_environment`` + The single thing an AgentDeployment references. Each part is a + ``ref | inline | None`` union so specs can be authored once and reused. + """ + + +# TODO: first-class environment, sandbox, and harness specs are planned for the +# Agent entity. Add those specs to this object once the contract is finalized. class Agent(NemoEntity, entity_type="agent"): """An agent definition — stores agent config and metadata. @@ -184,7 +367,38 @@ class AgentDeployment(NemoEntity, entity_type="agent_deployment"): agent: str = Field(default="", description="Name of the Agent entity this deployment is for.") config: dict[str, Any] = Field( default_factory=dict, - description="Resolved agent config with IGW URL injected, written when the deployment is created.", + description=( + "Resolved agent config with IGW URL injected and any referenced environment spec merged in, " + "written when the deployment is created." + ), + ) + # AgentEnvironment is snapshotted at create time: ``environment`` records the + # raw request input for provenance, environment-spec content is merged into + # ``config``, ``compute`` holds the resolved compute snapshot, and ``secrets`` + # holds the resolved secret-env references — all threaded to the container + # backend. A deployment is not kept in sync with the underlying + # AgentEnvironment/spec entities after creation. + environment: str | AgentEnvironmentInline | None = Field( + default=None, + description=( + '"workspace/name" ref to an AgentEnvironment, an inline environment, or None. ' + "Snapshotted at create time for provenance; the resolved values live in config/compute/secrets." + ), + ) + compute: ComputeSpecInline | None = Field( + default=None, + description=( + "Resolved compute snapshot from the referenced environment. Compiled into the container " + "resources for docker/k8s modes; ignored for subprocess." + ), + ) + secrets: dict[str, str] = Field( + default_factory=dict, + description=( + "Resolved secret env references from the referenced environment, as " + "ENV_VAR_NAME -> 'workspace/secret-name'. Compiled into secret-backed container env " + "vars (never plaintext) for docker/k8s modes; ignored for subprocess." + ), ) status: DeploymentStatus = Field( default="pending", diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/environment_resolution.py b/plugins/nemo-agents/src/nemo_agents_plugin/environment_resolution.py new file mode 100644 index 0000000000..5a9491baf2 --- /dev/null +++ b/plugins/nemo-agents/src/nemo_agents_plugin/environment_resolution.py @@ -0,0 +1,342 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Resolve an AgentEnvironment and merge its EnvironmentSpec into agent config. + +An AgentDeployment references an :class:`AgentEnvironment` (by name or inline). +At create time the deployment route: + +1. resolves the environment - dereferencing any ``"workspace/name"`` refs for + the environment and its two specs into concrete inline specs; +2. merges the resolved EnvironmentSpec into the Platform-owned agent config + (``nemo-agents-spec-v1``) so the Fabric translator sees a single config, and + collects the spec's secret-env references (top-level + per-MCP) for the + container backend to inject as secret-backed env vars; and +3. snapshots the resolved ComputeSpec and the collected secrets onto the + deployment for the container backend. + +Secret references are never written into the config as plaintext. Both the +spec's top-level ``secrets`` and each declared MCP server's ``secrets`` are +returned as an ENV_VAR_NAME -> ref map; the substrate injects the resolved value +into the process env under that name, and MCP servers read their credentials +from the process env by name (env-var-name indirection) rather than the config +embedding a raw reference string. + +Merge precedence is **EnvironmentSpec wins over the Agent config**: the Agent +config supplies defaults, and the EnvironmentSpec overrides them where it sets a +value. Fields the EnvironmentSpec leaves unset fall back to the Agent's +defaults, so an agent authored without an environment behaves identically. + +This ordering is the middle tier of an intended precedence chain +``deployment overrides > EnvironmentSpec > Agent defaults``. A future +deployment-specific override layer (e.g. an inline ``environment_overrides`` on +the deployment) slots in by applying this same merge a second time with the +higher-priority spec last, so each successive layer overrides the previous. +""" + +from __future__ import annotations + +import copy +from dataclasses import dataclass +from typing import Any + +from nemo_agents_plugin.entities import ( + NEMO_AGENTS_SPEC_CONFIG_FORMAT, + AgentComputeSpec, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, + ComputeSpecInline, + EnvironmentSpecInline, +) +from nemo_platform_plugin.entities.base import parse_qualified_name +from nemo_platform_plugin.entity_client import NemoEntitiesClient, NemoEntityNotFoundError + + +class EnvironmentResolutionError(ValueError): + """Raised when an AgentEnvironment or one of its specs cannot be resolved.""" + + +@dataclass(frozen=True) +class ResolvedEnvironment: + """Concrete environment/compute specs resolved from an AgentEnvironment.""" + + environment_spec: EnvironmentSpecInline | None = None + compute_spec: ComputeSpecInline | None = None + + +@dataclass(frozen=True) +class MergedEnvironment: + """Result of merging an EnvironmentSpec into an agent config. + + ``config`` is the merged ``nemo-agents-spec-v1`` config. ``secrets`` maps + ENV_VAR_NAME -> Secrets-service ref, collected from both the EnvironmentSpec's + top-level ``secrets`` and every declared MCP server's ``secrets``. The caller + snapshots ``secrets`` onto the deployment so the substrate injects the + resolved value into the process env (never plaintext); MCP servers read their + credentials from that process env by name (env-var-name indirection), rather + than the config embedding a raw secret reference as a literal value. + """ + + config: dict[str, Any] + secrets: dict[str, str] + + +async def resolve_environment( + environment: str | AgentEnvironmentInline | None, + *, + workspace: str, + entity_client: NemoEntitiesClient, +) -> ResolvedEnvironment: + """Resolve an AgentEnvironment (ref | inline | None) into concrete specs. + + Dereferences the environment and its ``environment_spec`` / ``compute_spec`` + refs. Missing refs raise :class:`EnvironmentResolutionError`. ``None`` yields + an empty :class:`ResolvedEnvironment` (no environment configured). + """ + if environment is None: + return ResolvedEnvironment() + + resolved_env = await _resolve_agent_environment(environment, workspace=workspace, entity_client=entity_client) + environment_spec = await _resolve_environment_spec( + resolved_env.environment_spec, workspace=workspace, entity_client=entity_client + ) + compute_spec = await _resolve_compute_spec( + resolved_env.compute_spec, workspace=workspace, entity_client=entity_client + ) + return ResolvedEnvironment(environment_spec=environment_spec, compute_spec=compute_spec) + + +async def _resolve_agent_environment( + environment: str | AgentEnvironmentInline, + *, + workspace: str, + entity_client: NemoEntitiesClient, +) -> AgentEnvironmentInline: + if isinstance(environment, str): + ref_workspace, name = parse_qualified_name(environment, default_workspace=workspace) + try: + return await entity_client.get(AgentEnvironment, name=name, workspace=ref_workspace) + except NemoEntityNotFoundError as exc: + raise EnvironmentResolutionError( + f"AgentEnvironment '{name}' not found in workspace '{ref_workspace}'." + ) from exc + return environment + + +async def _resolve_environment_spec( + spec: str | EnvironmentSpecInline | None, + *, + workspace: str, + entity_client: NemoEntitiesClient, +) -> EnvironmentSpecInline | None: + if spec is None: + return None + if isinstance(spec, str): + ref_workspace, name = parse_qualified_name(spec, default_workspace=workspace) + try: + return await entity_client.get(AgentEnvironmentSpec, name=name, workspace=ref_workspace) + except NemoEntityNotFoundError as exc: + raise EnvironmentResolutionError( + f"AgentEnvironmentSpec '{name}' not found in workspace '{ref_workspace}'." + ) from exc + return spec + + +async def _resolve_compute_spec( + spec: str | ComputeSpecInline | None, + *, + workspace: str, + entity_client: NemoEntitiesClient, +) -> ComputeSpecInline | None: + if spec is None: + return None + if isinstance(spec, str): + ref_workspace, name = parse_qualified_name(spec, default_workspace=workspace) + try: + return await entity_client.get(AgentComputeSpec, name=name, workspace=ref_workspace) + except NemoEntityNotFoundError as exc: + raise EnvironmentResolutionError( + f"AgentComputeSpec '{name}' not found in workspace '{ref_workspace}'." + ) from exc + return spec + + +def merge_environment_spec_into_agent_config( + config: dict[str, Any], + env_spec: EnvironmentSpecInline | None, +) -> MergedEnvironment: + """Merge a resolved EnvironmentSpec into a ``nemo-agents-spec-v1`` config. + + Returns a :class:`MergedEnvironment` holding a deep-copied merged config and + the collected secret-env references (top-level + per-MCP). Merge precedence is + EnvironmentSpec-wins: the Agent config supplies defaults and the spec overrides + them where it sets a value; fields the spec leaves unset keep the Agent default. + + Only ``nemo-agents-spec-v1`` (Fabric) configs are merged; other formats are + returned unchanged (they have no environment concept to fulfill). + """ + if env_spec is None: + return MergedEnvironment(config=config, secrets={}) + if config.get("config_format") != NEMO_AGENTS_SPEC_CONFIG_FORMAT: + return MergedEnvironment(config=config, secrets=dict(env_spec.secrets)) + + merged = copy.deepcopy(config) + _merge_environment_block(merged, env_spec) + _merge_process_env(merged, env_spec) + _merge_model_provider_override(merged, env_spec) + secrets = _collect_secrets(merged, env_spec) + return MergedEnvironment(config=merged, secrets=secrets) + + +def _merge_environment_block(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Override FabricConfig.environment mirror fields with the EnvironmentSpec's values.""" + environment = config.setdefault("environment", {}) + if not isinstance(environment, dict): + return + + # Scalar mirror fields: the EnvironmentSpec overrides the Agent's value only + # when it EXPLICITLY set the field. ``model_fields_set`` distinguishes an + # explicit value from a schema default - important for ``provider``, whose + # default is "local" (not None); without this guard a spec that never set + # ``provider`` would clobber an Agent's "docker"/"k8s" with "local". The + # spec's ``workspace_path``/``artifacts_path`` map onto the config's + # ``workspace``/``artifacts`` (the harness paths); the entity/tenant + # ``workspace`` is unrelated and never merged here. + scalar_fields = { + "provider": "provider", + "workspace_path": "workspace", + "artifacts_path": "artifacts", + "control_location": "control_location", + "ownership": "ownership", + } + for spec_attr, config_key in scalar_fields.items(): + if spec_attr not in env_spec.model_fields_set: + continue + value = getattr(env_spec, spec_attr) + if value is not None: + environment[config_key] = value + + # Dict mirror fields: Agent supplies the base, EnvironmentSpec keys win on collision. + for field in ("connection", "metadata", "settings"): + spec_value = getattr(env_spec, field) + if spec_value: + environment[field] = {**environment.get(field, {}), **spec_value} + + +def _merge_process_env(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Merge plaintext env vars into environment.env (EnvironmentSpec keys win).""" + if not env_spec.env: + return + environment = config.setdefault("environment", {}) + if not isinstance(environment, dict): + return + existing = environment.get("env") + existing = existing if isinstance(existing, dict) else {} + environment["env"] = {**existing, **env_spec.env} + + +def _merge_model_provider_override(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Apply an external model-provider override to models.default (overrides the Agent).""" + override = env_spec.model_provider_override + if override is None: + return + models = config.setdefault("models", {}) + if not isinstance(models, dict): + return + model = models.setdefault("default", {}) + if not isinstance(model, dict): + return + model["base_url"] = override.base_url + if override.provider is not None: + model["provider"] = override.provider + if override.api_key is not None: + model["api_key_env"] = override.api_key + + +def _collect_secrets(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> dict[str, str]: + """Collect secret-env references and fulfill Agent-declared MCP servers. + + Returns ENV_VAR_NAME -> Secrets-service ref for both the spec's top-level + ``secrets`` and every declared MCP server's ``secrets``. Secret refs are never + written into the config as plaintext: the caller injects the resolved value + into the process env under ENV_VAR_NAME, and MCP servers read that value by + name (env-var-name indirection). Non-secret MCP ``env`` is still merged into + the server config; the fulfillment overrides the Agent's url/env on collision. + + Raises :class:`EnvironmentResolutionError` when the same env var name maps to + two different secret references (an ambiguous, unresolvable collision), or + when a name is bound both as a secret and as a plaintext value (in the merged + ``environment.env`` or a declared MCP server's ``env``) - the effective + credential would otherwise depend on runtime env construction order. + """ + secrets: dict[str, str] = {} + + def _record(env_name: str, ref: str) -> None: + existing = secrets.get(env_name) + if existing is not None and existing != ref: + raise EnvironmentResolutionError( + f"Secret env var {env_name!r} is bound to conflicting references " + f"{existing!r} and {ref!r} in the environment spec." + ) + secrets[env_name] = ref + + for env_name, ref in env_spec.secrets.items(): + _record(env_name, ref) + + if env_spec.mcp: + mcp = config.setdefault("mcp", {}) + servers = mcp.setdefault("servers", {}) if isinstance(mcp, dict) else None + if isinstance(servers, dict): + for name, fulfillment in env_spec.mcp.items(): + server = servers.get(name) + # Only fulfill servers the Agent declared; skip undeclared names. + if not isinstance(server, dict): + continue + # url: the fulfillment's endpoint overrides the Agent's. + server["url"] = fulfillment.url + # Non-secret env merges into the server config (fulfillment wins). + if fulfillment.env: + existing_env = server.get("env") + existing_env = existing_env if isinstance(existing_env, dict) else {} + server["env"] = {**existing_env, **fulfillment.env} + # Secret refs are collected for injection into the process env, + # NOT stored in the server config as a literal reference string. + for env_name, ref in fulfillment.secrets.items(): + _record(env_name, ref) + + _reject_plaintext_secret_collisions(config, secrets) + return secrets + + +def _reject_plaintext_secret_collisions(config: dict[str, Any], secrets: dict[str, str]) -> None: + """Reject any secret-bound env name that is also bound as a plaintext value. + + A name present both in ``secrets`` (injected as a secret-backed env var) and + as a plaintext value in the merged ``environment.env`` or a declared MCP + server's ``env`` would double-bind: the effective value would depend on + runtime env construction order. Reject it up front. + """ + if not secrets: + return + + def _check(env_map: Any, where: str) -> None: + if not isinstance(env_map, dict): + return + clash = sorted(name for name in secrets if name in env_map) + if clash: + raise EnvironmentResolutionError( + f"Env var(s) {', '.join(clash)} are bound both as a secret and as a " + f"plaintext value ({where}). Use one or the other." + ) + + environment = config.get("environment") + if isinstance(environment, dict): + _check(environment.get("env"), "environment.env") + + mcp = config.get("mcp") + servers = mcp.get("servers") if isinstance(mcp, dict) else None + if isinstance(servers, dict): + for name, server in servers.items(): + if isinstance(server, dict): + _check(server.get("env"), f"mcp.servers.{name}.env") diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py b/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py index 2d57b4e8ff..5ca7d68975 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py @@ -52,13 +52,7 @@ def translate_agent_config(config: AgentConfig, harness_name: str | None = None) "default": fabric.ModelConfig(**model_payload), }, instructions=_instructions_config(config), - environment=fabric.EnvironmentConfig( - provider=config.environment.provider, - workspace=config.environment.workspace, - artifacts=config.environment.artifacts, - env=runtime_env, - settings=config.environment.settings, - ), + environment=_environment_config(config, runtime_env), skills=_skills_config(config), mcp=_mcp_config(config), tools=_tools_config(config), @@ -73,6 +67,36 @@ def _platform_runtime_env() -> dict[str, str]: return {name: value for name in PLATFORM_RUNTIME_ENV_VARS if (value := os.environ.get(name))} +def _environment_config(config: AgentConfig, runtime_env: dict[str, str]) -> Any: + """Build FabricConfig.environment, merging spec env + mirror fields. + + ``runtime_env`` carries platform-injected values (base URLs, gateway + credential). The EnvironmentSpec's plaintext ``env`` (merged onto + ``config.environment.env`` at deploy time) is layered underneath so + platform-injected values win on key collision. + """ + environment = config.environment + env = {**environment.env, **runtime_env} + kwargs: dict[str, Any] = { + "provider": environment.provider, + "workspace": environment.workspace, + "artifacts": environment.artifacts, + "env": env, + "settings": environment.settings, + } + # Forward optional Fabric mirror fields only when set, so defaults stay with + # Fabric rather than being pinned by the platform config. + if environment.control_location is not None: + kwargs["control_location"] = environment.control_location + if environment.ownership is not None: + kwargs["ownership"] = environment.ownership + if environment.connection: + kwargs["connection"] = environment.connection + if environment.metadata: + kwargs["metadata"] = environment.metadata + return fabric.EnvironmentConfig(**kwargs) + + def _select_harness(config: AgentConfig, harness_name: str | None) -> tuple[str, HarnessConfig]: selected_harness_name = harness_name or config.default_harness harness = config.harnesses.get(selected_harness_name) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py b/plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py index e8a78e0ddb..17ccc8c646 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/runner/backend.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Any, Literal -from nemo_agents_plugin.entities import DeploymentMode, DeploymentStatus, Endpoint +from nemo_agents_plugin.entities import ComputeResources, DeploymentMode, DeploymentStatus, Endpoint @dataclass(frozen=True) @@ -97,6 +97,8 @@ async def create_deployment( image: str | None = None, deployment_mode: DeploymentMode = "subprocess", created_by: str | None = None, + resources: ComputeResources | None = None, + secrets: dict[str, str] | None = None, ) -> DeploymentInfo: """Start the agent process; returns status="starting". @@ -105,6 +107,16 @@ async def create_deployment( agent's platform calls to this principal (on-behalf-of) so its access is scoped to what the creator can reach rather than the agents service principal's full reach. + + ``resources`` is the snapshotted compute spec's k8s-style + requests/limits. Container backends compile it into the execute + container's resources (k8s passes both; docker consolidates to limits). + Subprocess mode ignores it. + + ``secrets`` maps ENV_VAR_NAME -> "workspace/secret-name" from the + resolved environment. Container backends compile these into secret-backed + container env vars (never plaintext); the deployments-plugin substrate + materializes/mounts them. Subprocess mode ignores it. """ ... diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py b/plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py index ad7cf15969..173a61bfe7 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/runner/controller.py @@ -190,6 +190,8 @@ async def _start_deployment(self, dep: AgentDeployment) -> None: image=dep.image or None, deployment_mode=dep.deployment_mode, created_by=dep.created_by, + resources=dep.compute.resources if dep.compute is not None else None, + secrets=dep.secrets or None, ) except Exception as exc: logger.exception("Failed to start agent for deployment '%s'", dep.name) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py b/plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py index af49b2ac74..ad97cf75e4 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py @@ -27,6 +27,7 @@ AGENT_CONFIG_FILENAME, CONTAINER_DEPLOYMENT_MODES, NEMO_AGENTS_SPEC_CONFIG_FORMAT, + ComputeResources, DeploymentMode, DeploymentStatus, Endpoint, @@ -48,11 +49,14 @@ EnvVar, HTTPGetAction, Probe, + ResourceRequirements, + SecretRef, VolumeMount, ) from nemo_platform_plugin.auth import platform_auth_enabled from nemo_platform_plugin.client.adapter import client_from_platform from nemo_platform_plugin.config import LOOPBACK_ADDRESSES +from nemo_platform_plugin.entities.base import parse_qualified_name from nemo_platform_plugin.entities.client import AsyncEntitiesClient from nemo_platform_plugin.entity_client import NemoEntitiesClient, NemoEntityNotFoundError from nemo_platform_plugin.sdk_provider import get_async_platform_sdk @@ -68,6 +72,25 @@ _FABRIC_SERVER_MODULE = "nemo_agents_plugin.fabric.server" _AUTH_PROXY_IDENTITY = "agents" +# Env var names the backend generates on the agent container. A secret env var +# from the environment must not collide with these: docker would apply the +# secret value over the generated one, while k8s ignores the colliding secret +# (explicit ``env`` entries take precedence over the managed Secret's +# ``envFrom``), so the behavior would be inconsistent and surprising. Reject the +# collision at compile time instead. ``PYTHONPATH`` is only injected in the +# k8s plugin-wheels path, but is reserved unconditionally so the guard does not +# depend on the deployment mode. +_RESERVED_ENV_VAR_NAMES = frozenset( + { + "NMP_WORKSPACE", + "NMP_AGENT_NAME", + "NMP_BASE_URL", + "PYTHONPATH", + _AGENT_CONFIG_PATH_ENV, + _NAT_CONFIG_ENV, + } +) + # On delete, wait up to this long for the deployments controller to tear down the # container and remove the Deployment entity before we drop the DeploymentConfig. @@ -230,6 +253,55 @@ def _is_fabric_agent_config(agent_config: dict[str, Any]) -> bool: return agent_config.get("config_format") == NEMO_AGENTS_SPEC_CONFIG_FORMAT +def build_container_resources(resources: ComputeResources | None, *, mode: DeploymentMode) -> ResourceRequirements: + """Compile a snapshotted compute spec into the container's k8s resources. + + k8s passes both requests and limits through. Docker has no notion of + scheduling requests, so requests are consolidated into limits (limits win on + key collision). Returns an empty ``ResourceRequirements`` when no compute + spec was snapshotted (platform default). + """ + if resources is None: + return ResourceRequirements() + if mode == "docker": + consolidated = {**resources.requests, **resources.limits} + return ResourceRequirements(limits=consolidated, requests={}) + return ResourceRequirements(limits=dict(resources.limits), requests=dict(resources.requests)) + + +class ReservedSecretEnvVarError(ValueError): + """A secret env var name collides with a platform-generated container env var.""" + + +def _secret_env_vars(secrets: dict[str, str] | None, *, workspace: str) -> list[EnvVar]: + """Compile resolved secret references into secret-backed container env vars. + + ``secrets`` maps ENV_VAR_NAME -> "workspace/secret-name" (an unqualified + name resolves against the deployment ``workspace``). Each entry becomes an + ``EnvVar`` carrying a ``secret_ref`` (never a plaintext value); the + deployments-plugin substrate materializes the value at deploy time. + + Raises :class:`ReservedSecretEnvVarError` when a secret name collides with a + platform-generated env var (see ``_RESERVED_ENV_VAR_NAMES``): such a + collision behaves inconsistently across substrates, so it is rejected up + front rather than silently shadowing platform wiring. + """ + if not secrets: + return [] + reserved = sorted(name for name in secrets if name in _RESERVED_ENV_VAR_NAMES) + if reserved: + raise ReservedSecretEnvVarError( + "Environment secret variable name(s) collide with platform-reserved container env vars: " + f"{', '.join(reserved)}. Rename the secret env var(s) to avoid " + f"{', '.join(sorted(_RESERVED_ENV_VAR_NAMES))}." + ) + env_vars: list[EnvVar] = [] + for env_name, ref in secrets.items(): + secret_workspace, secret_name = parse_qualified_name(ref, default_workspace=workspace) + env_vars.append(EnvVar(name=env_name, secretRef=SecretRef(workspace=secret_workspace, name=secret_name))) + return env_vars + + def _fabric_config_mount_path(config_mount_path: str) -> str: parent = str(PurePosixPath(config_mount_path).parent) if parent in ("", "."): @@ -299,6 +371,8 @@ def build_deployment_config( auth_proxy_identity: str | None = None, auth_proxy_on_behalf_of: str | None = None, config_files: list[ConfigFile] | None = None, + resources: ComputeResources | None = None, + secrets: dict[str, str] | None = None, ) -> DeploymentConfig: """Compile an agent into a long-running ``DeploymentConfig`` (Always). @@ -336,6 +410,10 @@ def build_deployment_config( ) else: env.append(EnvVar(name=_NAT_CONFIG_ENV, value=config_mount_path)) + # Secret-backed env vars from the resolved environment: emitted as + # secret_ref (never plaintext). The deployments-plugin substrate resolves + # them (docker) or mounts a managed Secret via envFrom (k8s). + env.extend(_secret_env_vars(secrets, workspace=workspace)) volume_mounts: list[VolumeMount] = [] init_containers: list[Container] = [] @@ -390,6 +468,7 @@ def build_deployment_config( ).model_copy( update={ "volume_mounts": volume_mounts, + "resources": build_container_resources(resources, mode=mode), "readiness_probe": Probe( httpGet=HTTPGetAction(path="/health", port=port), initialDelaySeconds=2, @@ -442,6 +521,8 @@ async def create_deployment( image: str | None = None, deployment_mode: DeploymentMode = "docker", created_by: str | None = None, + resources: ComputeResources | None = None, + secrets: dict[str, str] | None = None, ) -> DeploymentInfo: """Create DeploymentConfig + Deployment entities for the agent container.""" del port # Host port is allocated by the deployments executor, not agents. @@ -531,21 +612,27 @@ async def create_deployment( logger.error("Refusing to deploy Fabric agent %r: %s", name, exc) return DeploymentInfo(name=name, status="failed", error=str(exc)) - deployment_config = build_deployment_config( - name=name, - workspace=workspace, - image=resolved_image, - port=self._config.container_port, - agent_config=config, - platform_base_url=gateway, - config_mount_path=self._config.config_mount_path, - mode=deployment_mode, - plugin_wheels_init_image=self._config.plugin_wheels_init_image, - labels=deployment_labels, - auth_proxy_identity=auth_proxy_identity, - auth_proxy_on_behalf_of=auth_proxy_on_behalf_of, - config_files=staged_config_files, - ) + try: + deployment_config = build_deployment_config( + name=name, + workspace=workspace, + image=resolved_image, + port=self._config.container_port, + agent_config=config, + platform_base_url=gateway, + config_mount_path=self._config.config_mount_path, + mode=deployment_mode, + plugin_wheels_init_image=self._config.plugin_wheels_init_image, + labels=deployment_labels, + auth_proxy_identity=auth_proxy_identity, + auth_proxy_on_behalf_of=auth_proxy_on_behalf_of, + config_files=staged_config_files, + resources=resources, + secrets=secrets, + ) + except ReservedSecretEnvVarError as exc: + logger.error("Refusing to deploy agent %r: %s", name, exc) + return DeploymentInfo(name=name, status="failed", error=str(exc)) await entities.create(deployment_config) try: deployment = Deployment( diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py b/plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py index 6e8966727e..7906bed330 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/runner/in_memory.py @@ -37,7 +37,12 @@ import httpx import yaml from nemo_agents_plugin.config import AgentsConfig, ControllerConfig -from nemo_agents_plugin.entities import AGENT_CONFIG_FILENAME, NEMO_AGENTS_SPEC_CONFIG_FORMAT, DeploymentMode +from nemo_agents_plugin.entities import ( + AGENT_CONFIG_FILENAME, + NEMO_AGENTS_SPEC_CONFIG_FORMAT, + ComputeResources, + DeploymentMode, +) from nemo_agents_plugin.fabric.gateway_credentials import platform_gateway_credential_env from nemo_agents_plugin.runner.backend import DeploymentInfo, LocalLog, LogLocation, NotYetAvailable, RunnerBackend from nemo_agents_plugin.runner.fabric_artifact_staging import stage_fabric_spec_dir @@ -225,12 +230,17 @@ async def create_deployment( image: str | None = None, deployment_mode: DeploymentMode = "subprocess", created_by: str | None = None, + resources: ComputeResources | None = None, + secrets: dict[str, str] | None = None, ) -> DeploymentInfo: """Start a local deployment for NAT workflows or Platform-owned agent specs.""" # created_by drives on-behalf-of delegation only for container modes (via # the auth-proxy sidecar). Subprocess deployments run in-process on the # platform host and do not use the sidecar, so it does not apply here. - del image, deployment_mode, created_by + # resources (compute spec) and secrets (secret-env refs) only apply to + # container modes; subprocess runs in-process on the platform host with no + # resource isolation and injects no managed secrets. + del image, deployment_mode, created_by, resources, secrets if config.get("config_format") == NEMO_AGENTS_SPEC_CONFIG_FORMAT: return await self._create_fabric_deployment(workspace, name, config, port, agent=agent) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/schema.py b/plugins/nemo-agents/src/nemo_agents_plugin/schema.py index bb701c70a3..2de705c4e1 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/schema.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/schema.py @@ -25,10 +25,16 @@ from nemo_agents_plugin.entities import ( NAT_WORKFLOW_CONFIG_FORMAT, Agent, + AgentComputeSpec, AgentDeployment, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, AgentSession, + ComputeSpecInline, DeploymentMode, DeploymentStatus, + EnvironmentSpecInline, ) from nemo_platform_plugin.schema import NemoFilter, NemoListResponse from pydantic import BaseModel, Field @@ -63,6 +69,13 @@ class CreateDeploymentRequest(BaseModel): default="", description="Container image for docker/k8s modes. Ignored for subprocess.", ) + environment: str | AgentEnvironmentInline | None = Field( + default=None, + description=( + 'Optional AgentEnvironment: a "workspace/name" ref, an inline environment, or None. ' + "Resolved and snapshotted onto the deployment at create time." + ), + ) class CreateSessionRequest(BaseModel): @@ -111,6 +124,36 @@ class SessionFilter(NemoFilter): ) +class CreateEnvironmentRequest(AgentEnvironmentInline): + """Request body for ``POST /v2/workspaces/{workspace}/environments``.""" + + name: str = Field(description="Unique environment name within the workspace.") + + +class CreateEnvironmentSpecRequest(EnvironmentSpecInline): + """Request body for ``POST /v2/workspaces/{workspace}/environment-specs``.""" + + name: str = Field(description="Unique environment-spec name within the workspace.") + + +class CreateComputeSpecRequest(ComputeSpecInline): + """Request body for ``POST /v2/workspaces/{workspace}/compute-specs``.""" + + name: str = Field(description="Unique compute-spec name within the workspace.") + + +class EnvironmentFilter(NemoFilter): + """Query filter for ``GET /v2/workspaces/{workspace}/environments``.""" + + +class EnvironmentSpecFilter(NemoFilter): + """Query filter for ``GET /v2/workspaces/{workspace}/environment-specs``.""" + + +class ComputeSpecFilter(NemoFilter): + """Query filter for ``GET /v2/workspaces/{workspace}/compute-specs``.""" + + # --------------------------------------------------------------------------- # List response type aliases # --------------------------------------------------------------------------- @@ -118,3 +161,6 @@ class SessionFilter(NemoFilter): AgentPage = NemoListResponse[Agent] DeploymentPage = NemoListResponse[AgentDeployment] SessionPage = NemoListResponse[AgentSession] +EnvironmentPage = NemoListResponse[AgentEnvironment] +EnvironmentSpecPage = NemoListResponse[AgentEnvironmentSpec] +ComputeSpecPage = NemoListResponse[AgentComputeSpec] diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/service.py b/plugins/nemo-agents/src/nemo_agents_plugin/service.py index 3d9d9f71fe..fb4c1de9c8 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/service.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/service.py @@ -102,6 +102,7 @@ def get_routers(self) -> list[RouterSpec]: agents, deployment_logs, deployments, + environments, gateway, sessions, ) @@ -111,6 +112,12 @@ def get_routers(self) -> list[RouterSpec]: RouterSpec(agents.router, tag="Agents", description="Agent CRUD", prefix=_prefix), RouterSpec(deployments.router, tag="Agent Deployments", description="Deployment lifecycle", prefix=_prefix), RouterSpec(sessions.router, tag="Agent Sessions", description="Session lifecycle", prefix=_prefix), + RouterSpec( + environments.router, + tag="Agent Environments", + description="AgentEnvironment, EnvironmentSpec, and ComputeSpec CRUD", + prefix=_prefix, + ), RouterSpec( deployment_logs.router, tag="Agent Deployments", diff --git a/plugins/nemo-agents/tests/unit/test_deployments_api.py b/plugins/nemo-agents/tests/unit/test_deployments_api.py index 5d78119226..eb51617857 100644 --- a/plugins/nemo-agents/tests/unit/test_deployments_api.py +++ b/plugins/nemo-agents/tests/unit/test_deployments_api.py @@ -13,7 +13,15 @@ from fastapi.testclient import TestClient from nemo_agents_plugin.api.v2 import deployments as deployments_router_module from nemo_agents_plugin.api.v2.dependencies import get_entity_client -from nemo_agents_plugin.entities import NEMO_AGENTS_SPEC_CONFIG_FORMAT, Agent, AgentDeployment, DeploymentStatus +from nemo_agents_plugin.entities import ( + NEMO_AGENTS_SPEC_CONFIG_FORMAT, + Agent, + AgentComputeSpec, + AgentDeployment, + AgentEnvironment, + AgentEnvironmentSpec, + DeploymentStatus, +) from nemo_platform_plugin.entity_client import NemoEntityConflictError, NemoEntityNotFoundError NOW = datetime.now(timezone.utc) @@ -108,6 +116,100 @@ async def _save_deployment(deployment: AgentDeployment) -> AgentDeployment: assert "functions" not in created_deployment.config assert "workflow" not in created_deployment.config + def test_create_with_environment_ref_snapshots_config_and_compute(self) -> None: + agent = _make_agent() + environment = AgentEnvironment( + name="env1", + workspace="default", + environment_spec="default/espec", + compute_spec="default/cspec", + ) + espec = AgentEnvironmentSpec( + name="espec", + workspace="default", + env={"CUSTOM": "from-spec"}, + secrets={"APP_TOKEN": "default/app-token"}, + ) + cspec = AgentComputeSpec(name="cspec", workspace="default", resources={"limits": {"cpu": "2"}}) + + mock_entity_client = AsyncMock() + # get order: agent (route), then AgentEnvironment, env spec, compute spec (resolver). + mock_entity_client.get = AsyncMock(side_effect=[agent, environment, espec, cspec]) + + async def _save_deployment(deployment: AgentDeployment) -> AgentDeployment: + deployment._id = f"deployment-{deployment.name}-id" + deployment._created_at = NOW + return deployment + + mock_entity_client.create = AsyncMock(side_effect=_save_deployment) + client = _test_client(mock_entity_client) + + resp = client.post( + "/apis/agents/v2/workspaces/default/deployments", + json={"agent": "fabric-agent", "name": "fabric-dep", "environment": "default/env1"}, + ) + + assert resp.status_code == 201 + created: AgentDeployment = mock_entity_client.create.call_args[0][0] + # Raw environment ref is snapshotted for provenance. + assert created.environment == "default/env1" + # Environment spec env merged into the resolved config. + assert created.config["environment"]["env"]["CUSTOM"] == "from-spec" + # Compute spec snapshotted onto the deployment. + assert created.compute is not None + assert created.compute.resources.limits == {"cpu": "2"} + # Secret env references snapshotted (never merged into config as plaintext). + assert created.secrets == {"APP_TOKEN": "default/app-token"} + assert "APP_TOKEN" not in created.config.get("environment", {}).get("env", {}) + + def test_create_with_inline_environment(self) -> None: + agent = _make_agent() + mock_entity_client = AsyncMock() + mock_entity_client.get = AsyncMock(return_value=agent) + + async def _save_deployment(deployment: AgentDeployment) -> AgentDeployment: + deployment._id = f"deployment-{deployment.name}-id" + deployment._created_at = NOW + return deployment + + mock_entity_client.create = AsyncMock(side_effect=_save_deployment) + client = _test_client(mock_entity_client) + + resp = client.post( + "/apis/agents/v2/workspaces/default/deployments", + json={ + "agent": "fabric-agent", + "name": "fabric-dep", + "environment": { + "environment_spec": {"env": {"INLINE": "yes"}}, + "compute_spec": {"resources": {"requests": {"cpu": "1"}}}, + }, + }, + ) + + assert resp.status_code == 201 + created: AgentDeployment = mock_entity_client.create.call_args[0][0] + assert created.config["environment"]["env"]["INLINE"] == "yes" + assert created.compute is not None + assert created.compute.resources.requests == {"cpu": "1"} + # Only the agent lookup hit the entity store; inline specs need no deref. + assert mock_entity_client.get.await_count == 1 + + def test_create_rejects_missing_environment_ref(self) -> None: + agent = _make_agent() + mock_entity_client = AsyncMock() + mock_entity_client.get = AsyncMock(side_effect=[agent, NemoEntityNotFoundError("gone")]) + client = _test_client(mock_entity_client) + + resp = client.post( + "/apis/agents/v2/workspaces/default/deployments", + json={"agent": "fabric-agent", "name": "fabric-dep", "environment": "default/missing"}, + ) + + assert resp.status_code == 422 + assert "AgentEnvironment 'missing' not found" in resp.json()["detail"] + mock_entity_client.create.assert_not_called() + def test_create_rejects_invalid_platform_agent_config(self) -> None: config = _fabric_agent_config() config["default_harness"] = "missing" diff --git a/plugins/nemo-agents/tests/unit/test_entities.py b/plugins/nemo-agents/tests/unit/test_entities.py index 17777d19df..a433a4f156 100644 --- a/plugins/nemo-agents/tests/unit/test_entities.py +++ b/plugins/nemo-agents/tests/unit/test_entities.py @@ -20,8 +20,14 @@ from nemo_agents_plugin.entities import ( NAT_WORKFLOW_CONFIG_FORMAT, Agent, + AgentComputeSpec, AgentDeployment, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, AgentSession, + ComputeSpecInline, + EnvironmentSpecInline, SessionStatus, agent_config_file_ref, agent_spec_file_ref, @@ -30,7 +36,10 @@ ) from nemo_agents_plugin.schema import ( CreateAgentRequest, + CreateComputeSpecRequest, CreateDeploymentRequest, + CreateEnvironmentRequest, + CreateEnvironmentSpecRequest, ) from pydantic import ValidationError @@ -298,3 +307,111 @@ def test_required_agent(self) -> None: def test_optional_name(self) -> None: req = CreateDeploymentRequest(agent="calc", name="calc-abc1") assert req.name == "calc-abc1" + + def test_environment_defaults_none(self) -> None: + req = CreateDeploymentRequest(agent="calc") + assert req.environment is None + + def test_environment_ref_string(self) -> None: + req = CreateDeploymentRequest(agent="calc", environment="default/env1") + assert req.environment == "default/env1" + + def test_environment_inline(self) -> None: + req = CreateDeploymentRequest( + agent="calc", + environment={"compute_spec": {"resources": {"limits": {"cpu": "2"}}}}, + ) + assert isinstance(req.environment, AgentEnvironmentInline) + assert isinstance(req.environment.compute_spec, ComputeSpecInline) + + +# --------------------------------------------------------------------------- +# Entities: AgentEnvironment / AgentEnvironmentSpec / AgentComputeSpec +# --------------------------------------------------------------------------- + + +class TestEnvironmentEntities: + def test_entity_types(self) -> None: + assert AgentEnvironment.__entity_type__ == "agent_environment" + assert AgentEnvironmentSpec.__entity_type__ == "agent_environment_spec" + assert AgentComputeSpec.__entity_type__ == "agent_compute_spec" + + def test_compute_spec_resources(self) -> None: + cs = AgentComputeSpec( + name="c1", + workspace="default", + resources={"limits": {"cpu": "2", "nvidia.com/gpu": "1"}, "requests": {"cpu": "1"}}, + ) + assert cs.resources.limits == {"cpu": "2", "nvidia.com/gpu": "1"} + assert cs.resources.requests == {"cpu": "1"} + + def test_environment_spec_fields(self) -> None: + es = AgentEnvironmentSpec( + name="e1", + workspace="default", + env={"FOO": "bar"}, + secrets={"TOKEN": "default/token"}, + mcp={"search": {"url": "http://x", "secrets": {"KEY": "default/key"}}}, + ) + assert es.env == {"FOO": "bar"} + assert es.secrets == {"TOKEN": "default/token"} + assert es.mcp["search"].url == "http://x" + assert es.mcp["search"].secrets == {"KEY": "default/key"} + assert es.provider == "local" + + def test_environment_ref_and_inline_unions(self) -> None: + by_ref = AgentEnvironment(name="env1", workspace="default", environment_spec="default/e1") + assert by_ref.environment_spec == "default/e1" + assert by_ref.compute_spec is None + + inline = AgentEnvironment( + name="env2", + workspace="default", + environment_spec={"env": {"A": "1"}}, + compute_spec={"resources": {"limits": {"cpu": "1"}}}, + ) + assert isinstance(inline.environment_spec, EnvironmentSpecInline) + assert isinstance(inline.compute_spec, ComputeSpecInline) + + def test_data_fields_include_domain_fields(self) -> None: + es = AgentEnvironmentSpec(name="e1", workspace="default", env={"FOO": "bar"}) + data = es._get_data_fields() + assert "env" in data + assert "provider" in data + assert "name" not in data + + +class TestAgentDeploymentEnvironmentSnapshot: + def test_environment_and_compute_default_none(self) -> None: + d = AgentDeployment(name="dep", workspace="default") + assert d.environment is None + assert d.compute is None + + def test_environment_ref_and_compute_snapshot(self) -> None: + d = AgentDeployment( + name="dep", + workspace="default", + agent="calc", + environment="default/env1", + compute=ComputeSpecInline(resources={"limits": {"cpu": "2"}}), + ) + assert d.environment == "default/env1" + assert isinstance(d.compute, ComputeSpecInline) + assert d.compute.resources.limits == {"cpu": "2"} + + +class TestCreateEnvironmentRequests: + def test_create_environment_request(self) -> None: + req = CreateEnvironmentRequest(name="env1", environment_spec="default/e1") + assert req.name == "env1" + assert req.environment_spec == "default/e1" + + def test_create_environment_spec_request(self) -> None: + req = CreateEnvironmentSpecRequest(name="e1", env={"FOO": "bar"}) + assert req.name == "e1" + assert req.env == {"FOO": "bar"} + + def test_create_compute_spec_request(self) -> None: + req = CreateComputeSpecRequest(name="c1", resources={"limits": {"cpu": "2"}}) + assert req.name == "c1" + assert req.resources.limits == {"cpu": "2"} diff --git a/plugins/nemo-agents/tests/unit/test_environment_resolution.py b/plugins/nemo-agents/tests/unit/test_environment_resolution.py new file mode 100644 index 0000000000..93a1b194bf --- /dev/null +++ b/plugins/nemo-agents/tests/unit/test_environment_resolution.py @@ -0,0 +1,290 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for AgentEnvironment resolution and config merge.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock + +import pytest +from nemo_agents_plugin.entities import ( + NEMO_AGENTS_SPEC_CONFIG_FORMAT, + AgentComputeSpec, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, + ComputeSpecInline, + EnvironmentSpecInline, + McpFulfillment, + ModelProviderOverride, +) +from nemo_agents_plugin.environment_resolution import ( + EnvironmentResolutionError, + merge_environment_spec_into_agent_config, + resolve_environment, +) +from nemo_platform_plugin.entity_client import NemoEntityNotFoundError + + +def _agent_config(**overrides: Any) -> dict[str, Any]: + config: dict[str, Any] = { + "config_format": NEMO_AGENTS_SPEC_CONFIG_FORMAT, + "name": "fabric-agent", + "default_harness": "hermes", + "harnesses": {"hermes": {"kind": "hermes"}}, + "models": {"default": {"provider": "openai", "model": "openai/gpt-5.4"}}, + "environment": {"provider": "local"}, + } + config.update(overrides) + return config + + +# --------------------------------------------------------------------------- +# resolve_environment +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_resolve_none_returns_empty() -> None: + resolved = await resolve_environment(None, workspace="default", entity_client=AsyncMock()) + assert resolved.environment_spec is None + assert resolved.compute_spec is None + + +@pytest.mark.asyncio +async def test_resolve_inline_environment_with_inline_specs() -> None: + environment = AgentEnvironmentInline( + environment_spec=EnvironmentSpecInline(env={"FOO": "bar"}), + compute_spec=ComputeSpecInline(resources={"limits": {"cpu": "2"}}), + ) + resolved = await resolve_environment(environment, workspace="default", entity_client=AsyncMock()) + assert resolved.environment_spec is not None + assert resolved.environment_spec.env == {"FOO": "bar"} + assert resolved.compute_spec is not None + assert resolved.compute_spec.resources.limits == {"cpu": "2"} + + +@pytest.mark.asyncio +async def test_resolve_environment_ref_dereferences_all_entities() -> None: + env_entity = AgentEnvironment( + name="env1", + workspace="default", + environment_spec="default/espec", + compute_spec="default/cspec", + ) + espec = AgentEnvironmentSpec(name="espec", workspace="default", env={"A": "1"}) + cspec = AgentComputeSpec(name="cspec", workspace="default", resources={"requests": {"cpu": "1"}}) + + entity_client = AsyncMock() + entity_client.get = AsyncMock(side_effect=[env_entity, espec, cspec]) + + resolved = await resolve_environment("default/env1", workspace="default", entity_client=entity_client) + + assert resolved.environment_spec is not None + assert resolved.environment_spec.env == {"A": "1"} + assert resolved.compute_spec is not None + assert resolved.compute_spec.resources.requests == {"cpu": "1"} + # AgentEnvironment, then its two specs. + assert entity_client.get.await_count == 3 + + +@pytest.mark.asyncio +async def test_resolve_missing_environment_ref_raises() -> None: + entity_client = AsyncMock() + entity_client.get = AsyncMock(side_effect=NemoEntityNotFoundError("gone")) + with pytest.raises(EnvironmentResolutionError, match="AgentEnvironment 'env1' not found"): + await resolve_environment("default/env1", workspace="default", entity_client=entity_client) + + +@pytest.mark.asyncio +async def test_resolve_missing_spec_ref_raises() -> None: + env_entity = AgentEnvironment(name="env1", workspace="default", environment_spec="default/missing") + entity_client = AsyncMock() + entity_client.get = AsyncMock(side_effect=[env_entity, NemoEntityNotFoundError("gone")]) + with pytest.raises(EnvironmentResolutionError, match="AgentEnvironmentSpec 'missing' not found"): + await resolve_environment("default/env1", workspace="default", entity_client=entity_client) + + +# --------------------------------------------------------------------------- +# merge_environment_spec_into_agent_config +# --------------------------------------------------------------------------- + + +def test_merge_none_spec_returns_config_unchanged() -> None: + config = _agent_config() + result = merge_environment_spec_into_agent_config(config, None) + assert result.config is config + assert result.secrets == {} + + +def test_merge_ignores_non_fabric_config() -> None: + config = {"config_format": "nat-workflow-v1", "functions": {}} + spec = EnvironmentSpecInline(env={"FOO": "bar"}, secrets={"TOK": "default/tok"}) + result = merge_environment_spec_into_agent_config(config, spec) + # Non-fabric config is left unchanged, but top-level secrets are still collected. + assert result.config is config + assert result.secrets == {"TOK": "default/tok"} + + +def test_merge_env_spec_wins_on_collision() -> None: + config = _agent_config(environment={"provider": "local", "env": {"SHARED": "agent"}}) + spec = EnvironmentSpecInline(env={"SHARED": "spec", "ONLY_SPEC": "spec"}) + result = merge_environment_spec_into_agent_config(config, spec) + # EnvironmentSpec overrides the Agent's value on collision; unique keys merge. + assert result.config["environment"]["env"] == {"SHARED": "spec", "ONLY_SPEC": "spec"} + # Original is not mutated (deep copy). + assert config["environment"]["env"] == {"SHARED": "agent"} + + +def test_merge_environment_mirror_fields_override_agent() -> None: + config = _agent_config(environment={"provider": "docker", "connection": {"agentkey": "a"}}) + spec = EnvironmentSpecInline( + provider="k8s", + workspace_path="/ws", + artifacts_path="/artifacts", + control_location="in_env_control", + ownership="fabric_owned", + connection={"url": "http://x"}, + ) + env = merge_environment_spec_into_agent_config(config, spec).config["environment"] + # EnvironmentSpec overrides the Agent's explicitly-set provider. + assert env["provider"] == "k8s" + # workspace_path/artifacts_path map onto workspace/artifacts. + assert env["workspace"] == "/ws" + assert env["artifacts"] == "/artifacts" + assert env["control_location"] == "in_env_control" + assert env["ownership"] == "fabric_owned" + # Dict fields: Agent keys survive, spec keys win on collision. + assert env["connection"] == {"agentkey": "a", "url": "http://x"} + + +def test_merge_environment_mirror_field_unset_spec_keeps_agent_default() -> None: + # A spec that leaves a scalar unset must not clobber the Agent's value. + config = _agent_config(environment={"workspace": "/agent-ws"}) + spec = EnvironmentSpecInline(workspace_path="/ws") # provider left unset + env = merge_environment_spec_into_agent_config(config, spec).config["environment"] + assert env["workspace"] == "/ws" + + +def test_merge_omitted_provider_does_not_clobber_agent() -> None: + # Regression: provider defaults to "local" (non-None). A spec that never set + # provider must not overwrite the Agent's explicit "docker". + config = _agent_config(environment={"provider": "docker"}) + spec = EnvironmentSpecInline(env={"FOO": "bar"}) # provider not set + env = merge_environment_spec_into_agent_config(config, spec).config["environment"] + assert env["provider"] == "docker" + + +def test_merge_explicit_provider_overrides_agent() -> None: + config = _agent_config(environment={"provider": "docker"}) + spec = EnvironmentSpecInline(provider="k8s") # explicitly set + env = merge_environment_spec_into_agent_config(config, spec).config["environment"] + assert env["provider"] == "k8s" + + +def test_merge_model_provider_override_overrides_agent() -> None: + config = _agent_config() + spec = EnvironmentSpecInline( + model_provider_override=ModelProviderOverride( + base_url="https://api.example.com", + provider="anthropic", + api_key="MY_SECRET", + ) + ) + model = merge_environment_spec_into_agent_config(config, spec).config["models"]["default"] + assert model["base_url"] == "https://api.example.com" + # EnvironmentSpec's provider overrides the Agent's. + assert model["provider"] == "anthropic" + assert model["api_key_env"] == "MY_SECRET" + + +def test_merge_top_level_secrets_collected_not_in_config() -> None: + config = _agent_config() + spec = EnvironmentSpecInline(secrets={"APP_TOKEN": "default/app-token"}) + result = merge_environment_spec_into_agent_config(config, spec) + assert result.secrets == {"APP_TOKEN": "default/app-token"} + # Secret refs never land in the config as plaintext env values. + assert "APP_TOKEN" not in result.config.get("environment", {}).get("env", {}) + + +def test_merge_mcp_fulfills_declared_server_env_wins_and_secrets_collected() -> None: + config = _agent_config(mcp={"servers": {"search": {"transport": "streamable-http", "url": "http://agent-url"}}}) + spec = EnvironmentSpecInline( + mcp={ + "search": McpFulfillment( + url="http://env-url", + env={"E": "1"}, + secrets={"SEARCH_TOKEN": "default/search-token"}, + ), + # Fulfillment for a server the Agent did not declare — must be ignored. + "undeclared": McpFulfillment(url="http://new-url"), + } + ) + result = merge_environment_spec_into_agent_config(config, spec) + servers = result.config["mcp"]["servers"] + # Fulfillment url overrides the Agent's; non-secret env merges in. + assert servers["search"]["url"] == "http://env-url" + assert servers["search"]["env"] == {"E": "1"} + # The secret ref is collected for process-env injection, NOT written into the + # server config as a literal reference string (env-var-name indirection). + assert "SEARCH_TOKEN" not in servers["search"]["env"] + assert result.secrets == {"SEARCH_TOKEN": "default/search-token"} + # An environment cannot add MCP servers the Agent never declared. + assert "undeclared" not in servers + + +def test_merge_conflicting_secret_reference_raises() -> None: + # The same env var name bound to two different secret refs is ambiguous. + config = _agent_config(mcp={"servers": {"search": {"transport": "streamable-http", "url": "http://a"}}}) + spec = EnvironmentSpecInline( + secrets={"TOKEN": "default/one"}, + mcp={"search": McpFulfillment(url="http://a", secrets={"TOKEN": "default/two"})}, + ) + with pytest.raises(EnvironmentResolutionError, match="conflicting references"): + merge_environment_spec_into_agent_config(config, spec) + + +def test_merge_same_secret_reference_twice_is_ok() -> None: + # Identical ref for the same name is not a conflict. + config = _agent_config(mcp={"servers": {"search": {"transport": "streamable-http", "url": "http://a"}}}) + spec = EnvironmentSpecInline( + secrets={"TOKEN": "default/same"}, + mcp={"search": McpFulfillment(url="http://a", secrets={"TOKEN": "default/same"})}, + ) + result = merge_environment_spec_into_agent_config(config, spec) + assert result.secrets == {"TOKEN": "default/same"} + + +def test_merge_rejects_top_level_plaintext_and_secret_same_name() -> None: + # Same name bound both as plaintext env and as a secret is ambiguous. + config = _agent_config() + spec = EnvironmentSpecInline(env={"TOKEN": "plain"}, secrets={"TOKEN": "default/token"}) + with pytest.raises(EnvironmentResolutionError, match="both as a secret and as a plaintext"): + merge_environment_spec_into_agent_config(config, spec) + + +def test_merge_rejects_agent_plaintext_and_spec_secret_same_name() -> None: + # The plaintext binding can come from the Agent config, not just the spec. + config = _agent_config(environment={"env": {"TOKEN": "agent-plain"}}) + spec = EnvironmentSpecInline(secrets={"TOKEN": "default/token"}) + with pytest.raises(EnvironmentResolutionError, match="both as a secret and as a plaintext"): + merge_environment_spec_into_agent_config(config, spec) + + +def test_merge_rejects_mcp_plaintext_and_secret_same_name() -> None: + config = _agent_config(mcp={"servers": {"search": {"transport": "streamable-http", "url": "http://a"}}}) + spec = EnvironmentSpecInline( + mcp={"search": McpFulfillment(url="http://a", env={"TOKEN": "plain"}, secrets={"TOKEN": "default/token"})}, + ) + with pytest.raises(EnvironmentResolutionError, match="both as a secret and as a plaintext"): + merge_environment_spec_into_agent_config(config, spec) + + +def test_merge_no_environment_reference_is_identical_to_today() -> None: + # Baseline agent config with no environment merged behaves unchanged. + config = _agent_config() + result = merge_environment_spec_into_agent_config(config, None) + assert result.config == config + assert result.secrets == {} diff --git a/plugins/nemo-agents/tests/unit/test_environments_api.py b/plugins/nemo-agents/tests/unit/test_environments_api.py new file mode 100644 index 0000000000..ec3a649fdf --- /dev/null +++ b/plugins/nemo-agents/tests/unit/test_environments_api.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for AgentEnvironment / EnvironmentSpec / ComputeSpec CRUD routes.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TypeVar +from unittest.mock import AsyncMock + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from nemo_agents_plugin.api.v2 import environments as environments_router_module +from nemo_agents_plugin.api.v2.dependencies import get_entity_client +from nemo_agents_plugin.entities import ( + AgentComputeSpec, + AgentEnvironment, + AgentEnvironmentSpec, + EnvironmentSpecInline, +) +from nemo_platform_plugin.entity import NemoEntity +from nemo_platform_plugin.entity_client import NemoEntityConflictError, NemoEntityNotFoundError + +NOW = datetime.now(timezone.utc) + +EntityT = TypeVar("EntityT", bound=NemoEntity) + + +def _stamp(entity: EntityT) -> EntityT: + entity._id = f"{entity.__entity_type__}-{entity.name}-id" + entity._created_at = NOW + return entity + + +def _test_client(mock_entity_client: AsyncMock) -> TestClient: + app = FastAPI() + app.include_router( + environments_router_module.router, + prefix="/apis/agents/v2/workspaces/{workspace}", + ) + app.dependency_overrides[get_entity_client] = lambda: mock_entity_client + return TestClient(app, raise_server_exceptions=False) + + +class TestComputeSpecRoutes: + def test_create(self) -> None: + client_mock = AsyncMock() + client_mock.create = AsyncMock(side_effect=lambda e: _stamp(e)) + client = _test_client(client_mock) + + resp = client.post( + "/apis/agents/v2/workspaces/default/compute-specs", + json={"name": "c1", "resources": {"limits": {"cpu": "2"}}}, + ) + + assert resp.status_code == 201 + created: AgentComputeSpec = client_mock.create.call_args[0][0] + assert created.name == "c1" + assert created.resources.limits == {"cpu": "2"} + + def test_create_conflict(self) -> None: + client_mock = AsyncMock() + client_mock.create = AsyncMock(side_effect=NemoEntityConflictError("exists")) + client = _test_client(client_mock) + + resp = client.post( + "/apis/agents/v2/workspaces/default/compute-specs", + json={"name": "c1", "resources": {}}, + ) + assert resp.status_code == 409 + + def test_get_not_found(self) -> None: + client_mock = AsyncMock() + client_mock.get = AsyncMock(side_effect=NemoEntityNotFoundError("gone")) + client = _test_client(client_mock) + + resp = client.get("/apis/agents/v2/workspaces/default/compute-specs/c1") + assert resp.status_code == 404 + + +class TestEnvironmentSpecRoutes: + def test_create(self) -> None: + client_mock = AsyncMock() + client_mock.create = AsyncMock(side_effect=lambda e: _stamp(e)) + client = _test_client(client_mock) + + resp = client.post( + "/apis/agents/v2/workspaces/default/environment-specs", + json={"name": "e1", "env": {"FOO": "bar"}, "mcp": {"search": {"url": "http://x"}}}, + ) + + assert resp.status_code == 201 + created: AgentEnvironmentSpec = client_mock.create.call_args[0][0] + assert created.name == "e1" + assert created.env == {"FOO": "bar"} + assert created.mcp["search"].url == "http://x" + + def test_delete(self) -> None: + client_mock = AsyncMock() + client_mock.delete = AsyncMock(return_value=None) + client = _test_client(client_mock) + + resp = client.delete("/apis/agents/v2/workspaces/default/environment-specs/e1") + assert resp.status_code == 204 + + +class TestEnvironmentRoutes: + def test_create_with_refs(self) -> None: + client_mock = AsyncMock() + client_mock.create = AsyncMock(side_effect=lambda e: _stamp(e)) + client = _test_client(client_mock) + + resp = client.post( + "/apis/agents/v2/workspaces/default/environments", + json={"name": "env1", "environment_spec": "default/e1", "compute_spec": "default/c1"}, + ) + + assert resp.status_code == 201 + created: AgentEnvironment = client_mock.create.call_args[0][0] + assert created.name == "env1" + assert created.environment_spec == "default/e1" + assert created.compute_spec == "default/c1" + + def test_create_with_inline(self) -> None: + client_mock = AsyncMock() + client_mock.create = AsyncMock(side_effect=lambda e: _stamp(e)) + client = _test_client(client_mock) + + resp = client.post( + "/apis/agents/v2/workspaces/default/environments", + json={"name": "env2", "environment_spec": {"env": {"A": "1"}}}, + ) + + assert resp.status_code == 201 + created: AgentEnvironment = client_mock.create.call_args[0][0] + assert isinstance(created.environment_spec, EnvironmentSpecInline) + assert created.environment_spec.env == {"A": "1"} + + def test_get(self) -> None: + env = _stamp(AgentEnvironment(name="env1", workspace="default", environment_spec="default/e1")) + client_mock = AsyncMock() + client_mock.get = AsyncMock(return_value=env) + client = _test_client(client_mock) + + resp = client.get("/apis/agents/v2/workspaces/default/environments/env1") + assert resp.status_code == 200 + assert resp.json()["environment_spec"] == "default/e1" + + def test_list(self) -> None: + env = _stamp(AgentEnvironment(name="env1", workspace="default")) + result = AsyncMock() + result.data = [env] + result.pagination = None + client_mock = AsyncMock() + client_mock.list = AsyncMock(return_value=result) + client = _test_client(client_mock) + + resp = client.get("/apis/agents/v2/workspaces/default/environments") + assert resp.status_code == 200 + assert resp.json()["data"][0]["name"] == "env1" diff --git a/plugins/nemo-agents/tests/unit/test_fabric_translator.py b/plugins/nemo-agents/tests/unit/test_fabric_translator.py index 92ae791463..ea85b4a863 100644 --- a/plugins/nemo-agents/tests/unit/test_fabric_translator.py +++ b/plugins/nemo-agents/tests/unit/test_fabric_translator.py @@ -224,6 +224,41 @@ def test_top_level_prompts_rejected_until_shared_prompt_contract_exists(self) -> with pytest.raises(FabricTranslationError, match="Top-level prompts are not translated yet"): translate_agent_config(config) + def test_environment_spec_env_forwarded_platform_values_win(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Simulates a config after an EnvironmentSpec merge: environment.env holds + # the spec's plaintext vars. Platform-injected runtime values win on key + # collision. + monkeypatch.setenv("NMP_WORKSPACE", "runtime-ws") + monkeypatch.delenv("NEMO_BASE_URL", raising=False) + monkeypatch.delenv("NMP_BASE_URL", raising=False) + payload = copy.deepcopy(_example_yaml_config()) + payload["environment"]["env"] = {"CUSTOM": "from-spec", "NMP_WORKSPACE": "spec-should-lose"} + config = AgentConfig.model_validate(payload) + + fabric_config = translate_agent_config(config, harness_name="codex") + + assert fabric_config.environment.env["CUSTOM"] == "from-spec" + assert fabric_config.environment.env["NMP_WORKSPACE"] == "runtime-ws" + + def test_environment_mirror_fields_forwarded(self) -> None: + payload = copy.deepcopy(_example_yaml_config()) + payload["environment"].update( + { + "control_location": "in_env_control", + "ownership": "fabric_owned", + "connection": {"url": "http://sandbox"}, + "metadata": {"team": "platform"}, + } + ) + config = AgentConfig.model_validate(payload) + + fabric_config = translate_agent_config(config, harness_name="codex") + + assert fabric_config.environment.control_location == "in_env_control" + assert fabric_config.environment.ownership == "fabric_owned" + assert fabric_config.environment.connection == {"url": "http://sandbox"} + assert fabric_config.environment.metadata == {"team": "platform"} + @pytest.mark.parametrize( ("kind", "adapter_id"), [ diff --git a/plugins/nemo-agents/tests/unit/test_runner_deployments.py b/plugins/nemo-agents/tests/unit/test_runner_deployments.py index 045e40bd42..040debb6dc 100644 --- a/plugins/nemo-agents/tests/unit/test_runner_deployments.py +++ b/plugins/nemo-agents/tests/unit/test_runner_deployments.py @@ -11,11 +11,13 @@ import pytest import yaml from nemo_agents_plugin.config import AgentsConfig, DeploymentsRunnerConfig -from nemo_agents_plugin.entities import Endpoint +from nemo_agents_plugin.entities import ComputeResources, Endpoint from nemo_agents_plugin.fabric.gateway_credentials import PLATFORM_IGW_API_KEY_ENV, PLATFORM_IGW_API_KEY_PLACEHOLDER from nemo_agents_plugin.runner.deployments_backend import ( DeploymentsRunnerBackend, + ReservedSecretEnvVarError, UnreachableGatewayURLError, + build_container_resources, build_deployment_config, executor_for_mode, map_status, @@ -246,6 +248,138 @@ def test_build_deployment_config_always_single_container() -> None: assert loaded["llms"]["nim"]["_type"] == "nim" +def test_build_container_resources_none_is_empty() -> None: + resources = build_container_resources(None, mode="k8s") + assert resources.limits == {} + assert resources.requests == {} + + +def test_build_container_resources_k8s_passes_both() -> None: + compute = ComputeResources(limits={"cpu": "2", "nvidia.com/gpu": "1"}, requests={"cpu": "1"}) + resources = build_container_resources(compute, mode="k8s") + assert resources.limits == {"cpu": "2", "nvidia.com/gpu": "1"} + assert resources.requests == {"cpu": "1"} + + +def test_build_container_resources_docker_consolidates_to_limits() -> None: + compute = ComputeResources(limits={"cpu": "2"}, requests={"cpu": "1", "memory": "1Gi"}) + resources = build_container_resources(compute, mode="docker") + # Docker has no scheduling requests: requests fold into limits, limits win on collision. + assert resources.limits == {"cpu": "2", "memory": "1Gi"} + assert resources.requests == {} + + +def test_build_deployment_config_applies_k8s_resources() -> None: + cfg = build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://nmp-api:8080", + config_mount_path="/workspace/config.yaml", + mode="k8s", + resources=ComputeResources(limits={"cpu": "2"}, requests={"cpu": "1"}), + ) + container = cfg.containers[0] + assert container.resources.limits == {"cpu": "2"} + assert container.resources.requests == {"cpu": "1"} + + +def test_build_deployment_config_docker_resources_limits_only() -> None: + cfg = build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://host.docker.internal:8080", + config_mount_path="/tmp/nemo/config.yaml", + mode="docker", + resources=ComputeResources(limits={"cpu": "2"}, requests={"memory": "1Gi"}), + ) + container = cfg.containers[0] + assert container.resources.limits == {"cpu": "2", "memory": "1Gi"} + assert container.resources.requests == {} + + +def test_build_deployment_config_no_resources_is_empty() -> None: + cfg = build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://nmp-api:8080", + config_mount_path="/workspace/config.yaml", + mode="k8s", + ) + assert cfg.containers[0].resources.limits == {} + assert cfg.containers[0].resources.requests == {} + + +def test_build_deployment_config_emits_secret_ref_env_vars() -> None: + cfg = build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://nmp-api:8080", + config_mount_path="/workspace/config.yaml", + mode="k8s", + secrets={"APP_TOKEN": "default/app-token", "OTHER": "prod/other-secret"}, + ) + by_name = {e.name: e for e in cfg.containers[0].env} + # Secret-backed env vars carry a secret_ref, never a plaintext value. + assert by_name["APP_TOKEN"].value is None + assert by_name["APP_TOKEN"].secret_ref is not None + assert by_name["APP_TOKEN"].secret_ref.workspace == "default" + assert by_name["APP_TOKEN"].secret_ref.name == "app-token" + # A workspace-qualified reference keeps its explicit workspace. + assert by_name["OTHER"].secret_ref is not None + assert by_name["OTHER"].secret_ref.workspace == "prod" + assert by_name["OTHER"].secret_ref.name == "other-secret" + # The plaintext secret value never appears in the rendered config. + assert "app-token" not in yaml.safe_dump([e.model_dump() for e in cfg.containers[0].env if e.value]) + + +def test_build_deployment_config_no_secrets_adds_no_secret_env() -> None: + cfg = build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://nmp-api:8080", + config_mount_path="/workspace/config.yaml", + mode="k8s", + ) + assert all(e.secret_ref is None for e in cfg.containers[0].env) + + +@pytest.mark.parametrize("mode", ["docker", "k8s"]) +@pytest.mark.parametrize( + "reserved_name", + ["NMP_WORKSPACE", "NMP_AGENT_NAME", "NMP_BASE_URL", "PYTHONPATH", "AGENT_CONFIG_PATH", "NAT_CONFIG_PATH"], +) +def test_build_deployment_config_rejects_secret_name_colliding_with_reserved(reserved_name: str, mode: str) -> None: + # A secret env var whose name collides with a platform-generated container + # env var is rejected up front (behavior would otherwise differ by substrate). + with pytest.raises(ReservedSecretEnvVarError, match=reserved_name): + build_deployment_config( + name="hello-dep", + workspace="default", + image="nat-runtime:latest", + port=8000, + agent_config={}, + platform_base_url="http://nmp-api:8080", + config_mount_path="/workspace/config.yaml", + mode=mode, + secrets={reserved_name: "default/some-secret"}, + ) + + def test_build_deployment_config_k8s_uses_nat_entrypoint() -> None: cfg = build_deployment_config( name="hello-dep",