diff --git a/plugins/nemo-agents/openapi/openapi.yaml b/plugins/nemo-agents/openapi/openapi.yaml index ee018f54fc..82f5f25b59 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/optimize: - 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_optimize_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,229 +1672,37 @@ paths: schema: type: string title: Workspace - requestBody: + - name: job + in: path required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OptimizeJobRequest' + 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/OptimizeJob' + $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_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_skills__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/optimize-skills/{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_optimize_skills__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 @@ -1776,12 +1738,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize-skills/{name}: + /apis/agents/v2/workspaces/{workspace}/jobs/evaluate/{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_evaluate__name__get parameters: - name: workspace in: path @@ -1801,7 +1763,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJob' + $ref: '#/components/schemas/EvaluateJob' '422': description: Validation Error content: @@ -1812,7 +1774,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_evaluate__name__delete parameters: - name: workspace in: path @@ -1835,12 +1797,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/evaluate/{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_evaluate__name__cancel_post parameters: - name: workspace in: path @@ -1860,19 +1822,19 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeSkillsJob' + $ref: '#/components/schemas/EvaluateJob' '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/evaluate/{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_evaluate__name__logs_get parameters: - name: workspace in: path @@ -1911,12 +1873,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/evaluate/{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_evaluate__name__results_get parameters: - name: workspace in: path @@ -1943,12 +1905,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/evaluate/{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_evaluate__name__status_get parameters: - name: workspace in: path @@ -1975,12 +1937,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - /apis/agents/v2/workspaces/{workspace}/jobs/optimize/{job}/results/{name}: - get: + /apis/agents/v2/workspaces/{workspace}/jobs/optimize: + post: tags: - Agents - summary: Get Job Result - operationId: get_job_result_apis_agents_v2_workspaces__workspace__jobs_optimize__job__results__name__get + summary: Create Job + operationId: create_job_apis_agents_v2_workspaces__workspace__jobs_optimize_post parameters: - name: workspace in: path @@ -1988,37 +1950,229 @@ 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/OptimizeJobRequest' responses: - '200': + '201': description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/PlatformJobResultResponse' + $ref: '#/components/schemas/OptimizeJob' '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: + - 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_skills__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/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 @@ -2054,12 +2208,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 @@ -2079,7 +2233,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OptimizeJob' + $ref: '#/components/schemas/OptimizeSkillsJob' '422': description: Validation Error content: @@ -2090,7 +2244,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 @@ -2113,12 +2267,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 @@ -2138,19 +2292,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 @@ -2189,12 +2343,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 @@ -2221,12 +2375,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 @@ -2253,41 +2407,400 @@ paths: 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/{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 + 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: + - 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/optimize/{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 + 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/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/optimize/{name}: + get: + tags: + - Agents + summary: Get Job + operationId: get_job_apis_agents_v2_workspaces__workspace__jobs_optimize__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/OptimizeJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - Agents + summary: Delete Job + operationId: delete_job_apis_agents_v2_workspaces__workspace__jobs_optimize__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}/jobs/optimize/{name}/cancel: + post: + tags: + - Agents + summary: Cancel Job + operationId: cancel_job_apis_agents_v2_workspaces__workspace__jobs_optimize__name__cancel_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/OptimizeJob' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /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 + 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/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 + 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/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 + 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/PlatformJobStatusResponse' + '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 of the agent. + description: Human-readable description. default: '' - config: - additionalProperties: true - type: object - title: Config - description: Agent config dict interpreted according to config_format. - config_format: + 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 - 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 @@ -2338,12 +2851,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: @@ -2369,8 +2883,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: @@ -2395,43 +2934,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 @@ -2482,11 +3290,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).' AnalyzeBatchConfig: properties: batch: @@ -2659,6 +3469,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: @@ -2686,6 +3534,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: @@ -2711,11 +3579,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``. DatetimeFilter: additionalProperties: false properties: @@ -2786,6 +3773,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: @@ -3343,6 +4409,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: @@ -3366,6 +4515,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_Agent_: 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 413ce91274..046a4516be 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 @@ -31,3 +31,24 @@ class DeploymentPerms(PermissionSet, namespace="agents.deployments"): 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..247af1cf64 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,15 @@ from nemo_agents_plugin.entities import ( Agent, AgentDeployment, + AgentEnvironmentInline, is_container_deployment_mode, ) +from nemo_agents_plugin.environment_resolution import ( + EnvironmentResolutionError, + ResolvedEnvironment, + merge_environment_spec_into_agent_config, + resolve_environment, +) from nemo_agents_plugin.schema import ( CreateDeploymentRequest, DeploymentFilter, @@ -83,12 +90,27 @@ 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 + ) + resolved_config = merge_environment_spec_into_agent_config(resolved_config, resolved_environment.environment_spec) + env_spec = resolved_environment.environment_spec + resolved_secrets = dict(env_spec.secrets) if env_spec is not None else {} + + # 5. Create the entity with status "pending" deployment = AgentDeployment( name=deployment_name, workspace=workspace, agent=body.agent, config=resolved_config, + environment=body.environment, + compute=resolved_environment.compute_spec, + secrets=resolved_secrets, status="pending", deployment_mode=body.deployment_mode, image=body.image, @@ -120,6 +142,20 @@ 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 + + @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 d3eab6ccfe..4798652965 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/entities.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/entities.py @@ -48,6 +48,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 # --------------------------------------------------------------------------- @@ -132,8 +290,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. @@ -174,7 +357,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..ad018acc81 --- /dev/null +++ b/plugins/nemo-agents/src/nemo_agents_plugin/environment_resolution.py @@ -0,0 +1,250 @@ +# 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 +3. snapshots the resolved ComputeSpec onto the deployment for the container + backend. + +Merge precedence is **Agent config wins over EnvironmentSpec**: the +EnvironmentSpec is the fulfillment/base layer, and any value the Agent set +explicitly is preserved. This keeps existing agent configs (authored without an +environment) behaving identically. +""" + +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 + + +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, +) -> dict[str, Any]: + """Merge a resolved EnvironmentSpec into a ``nemo-agents-spec-v1`` config. + + Returns a deep-copied config with the spec merged in. Merge precedence is + Agent-config-wins: the Agent's explicitly-set values are preserved and the + EnvironmentSpec only fills gaps or contributes additive keys. + + 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 config + if config.get("config_format") != NEMO_AGENTS_SPEC_CONFIG_FORMAT: + return config + + merged = copy.deepcopy(config) + _merge_environment_block(merged, env_spec) + _merge_process_env(merged, env_spec) + _merge_model_provider_override(merged, env_spec) + _merge_mcp(merged, env_spec) + return merged + + +def _merge_environment_block(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Fill FabricConfig.environment mirror fields where the Agent left them unset.""" + environment = config.setdefault("environment", {}) + if not isinstance(environment, dict): + return + + # Scalar mirror fields: only fill when the Agent did not set them. 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(): + value = getattr(env_spec, spec_attr) + if value is not None and config_key not in environment: + environment[config_key] = value + + # Dict mirror fields: EnvironmentSpec is the base, Agent keys win on collision. + for field in ("connection", "metadata", "settings"): + spec_value = getattr(env_spec, field) + if spec_value: + environment[field] = {**spec_value, **environment.get(field, {})} + + +def _merge_process_env(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Merge plaintext env vars into environment.env (Agent 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"] = {**env_spec.env, **existing} + + +def _merge_model_provider_override(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Apply an external model-provider override to models.default where unset.""" + 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 + if "base_url" not in model: + model["base_url"] = override.base_url + if override.provider is not None and "provider" not in model: + model["provider"] = override.provider + if override.api_key is not None and "api_key_env" not in model: + model["api_key_env"] = override.api_key + + +def _merge_mcp(config: dict[str, Any], env_spec: EnvironmentSpecInline) -> None: + """Fulfill Agent-declared MCP servers by name (Agent keys win). + + McpFulfillment is a request/fulfill contract: the Agent DECLARES an MCP + server by name and the EnvironmentSpec PROVIDES its url/env/secrets. A + fulfillment whose name the Agent did not declare is ignored - an environment + must not add MCP servers the Agent never requested. + """ + if not env_spec.mcp: + return + mcp = config.setdefault("mcp", {}) + if not isinstance(mcp, dict): + return + servers = mcp.setdefault("servers", {}) + if not isinstance(servers, dict): + return + + 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: fill only when the Agent did not provide one. + if "url" not in server: + server["url"] = fulfillment.url + # env + secrets merge into the server env; Agent-authored env wins. + merged_env = {**fulfillment.env, **fulfillment.secrets} + if merged_env: + existing_env = server.get("env") + existing_env = existing_env if isinstance(existing_env, dict) else {} + server["env"] = {**merged_env, **existing_env} + servers[name] = server 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 e510ad6bc5..743d1d9104 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..af5eaaafe8 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,22 @@ _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. +_RESERVED_ENV_VAR_NAMES = frozenset( + { + "NMP_WORKSPACE", + "NMP_AGENT_NAME", + "NMP_BASE_URL", + _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 +250,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 +368,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 +407,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 +465,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 +518,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 +609,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 cb14f429de..acd2d69e8d 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 @@ -223,12 +228,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 agent, 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 agent, 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) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/schema.py b/plugins/nemo-agents/src/nemo_agents_plugin/schema.py index 6955181c15..8e636fd4d3 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/schema.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/schema.py @@ -25,9 +25,15 @@ from nemo_agents_plugin.entities import ( NAT_WORKFLOW_CONFIG_FORMAT, Agent, + AgentComputeSpec, AgentDeployment, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, + ComputeSpecInline, DeploymentMode, DeploymentStatus, + EnvironmentSpecInline, ) from nemo_platform_plugin.schema import NemoFilter, NemoListResponse from pydantic import BaseModel, Field @@ -62,6 +68,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." + ), + ) # --------------------------------------------------------------------------- @@ -91,9 +104,42 @@ class DeploymentFilter(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 # --------------------------------------------------------------------------- AgentPage = NemoListResponse[Agent] DeploymentPage = NemoListResponse[AgentDeployment] +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 4b0a82cf95..868ca1be77 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/service.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/service.py @@ -94,6 +94,7 @@ def get_routers(self) -> list[RouterSpec]: agents, deployment_logs, deployments, + environments, gateway, ) @@ -101,6 +102,12 @@ def get_routers(self) -> list[RouterSpec]: specs: list[RouterSpec] = [ RouterSpec(agents.router, tag="Agents", description="Agent CRUD", prefix=_prefix), RouterSpec(deployments.router, tag="Agent Deployments", description="Deployment 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 7eef5bf9e2..ff18da8ed2 100644 --- a/plugins/nemo-agents/tests/unit/test_entities.py +++ b/plugins/nemo-agents/tests/unit/test_entities.py @@ -19,7 +19,13 @@ from nemo_agents_plugin.entities import ( NAT_WORKFLOW_CONFIG_FORMAT, Agent, + AgentComputeSpec, AgentDeployment, + AgentEnvironment, + AgentEnvironmentInline, + AgentEnvironmentSpec, + ComputeSpecInline, + EnvironmentSpecInline, agent_config_file_ref, agent_spec_file_ref, agent_spec_fileset_name, @@ -27,7 +33,10 @@ ) from nemo_agents_plugin.schema import ( CreateAgentRequest, + CreateComputeSpecRequest, CreateDeploymentRequest, + CreateEnvironmentRequest, + CreateEnvironmentSpecRequest, ) from pydantic import ValidationError @@ -220,3 +229,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..7601c47ae8 --- /dev/null +++ b/plugins/nemo-agents/tests/unit/test_environment_resolution.py @@ -0,0 +1,196 @@ +# 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() + assert merge_environment_spec_into_agent_config(config, None) is config + + +def test_merge_ignores_non_fabric_config() -> None: + config = {"config_format": "nat-workflow-v1", "functions": {}} + spec = EnvironmentSpecInline(env={"FOO": "bar"}) + assert merge_environment_spec_into_agent_config(config, spec) is config + + +def test_merge_env_agent_wins_on_collision() -> None: + config = _agent_config(environment={"provider": "local", "env": {"SHARED": "agent"}}) + spec = EnvironmentSpecInline(env={"SHARED": "spec", "ONLY_SPEC": "spec"}) + merged = merge_environment_spec_into_agent_config(config, spec) + assert merged["environment"]["env"] == {"SHARED": "agent", "ONLY_SPEC": "spec"} + # Original is not mutated (deep copy). + assert config["environment"]["env"] == {"SHARED": "agent"} + + +def test_merge_environment_mirror_fields_fill_only_when_unset() -> None: + config = _agent_config(environment={"provider": "docker"}) + spec = EnvironmentSpecInline( + provider="k8s", + workspace_path="/ws", + artifacts_path="/artifacts", + control_location="in_env_control", + ownership="fabric_owned", + connection={"url": "http://x"}, + ) + merged = merge_environment_spec_into_agent_config(config, spec) + env = merged["environment"] + # Agent explicitly set provider -> preserved. + assert env["provider"] == "docker" + # Agent left these unset -> filled from spec (workspace_path/artifacts_path + # -> workspace/artifacts). + assert env["workspace"] == "/ws" + assert env["artifacts"] == "/artifacts" + assert env["control_location"] == "in_env_control" + assert env["ownership"] == "fabric_owned" + assert env["connection"] == {"url": "http://x"} + + +def test_merge_model_provider_override_applies_when_unset() -> None: + config = _agent_config() + spec = EnvironmentSpecInline( + model_provider_override=ModelProviderOverride( + base_url="https://api.example.com", + provider="anthropic", + api_key="MY_SECRET", + ) + ) + merged = merge_environment_spec_into_agent_config(config, spec) + model = merged["models"]["default"] + assert model["base_url"] == "https://api.example.com" + assert model["provider"] == "openai" # Agent's explicit provider wins. + assert model["api_key_env"] == "MY_SECRET" + + +def test_merge_mcp_fulfills_by_name_agent_url_wins() -> 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={"TOKEN": "secret-ref"}), + # Fulfillment for a server the Agent did not declare — must be ignored. + "undeclared": McpFulfillment(url="http://new-url"), + } + ) + merged = merge_environment_spec_into_agent_config(config, spec) + servers = merged["mcp"]["servers"] + # Agent-provided url wins; env + secrets merged in. + assert servers["search"]["url"] == "http://agent-url" + assert servers["search"]["env"] == {"E": "1", "TOKEN": "secret-ref"} + # An environment cannot add MCP servers the Agent never declared. + assert "undeclared" not in servers + + +def test_merge_no_environment_reference_is_identical_to_today() -> None: + # Baseline agent config with no environment merged behaves unchanged. + config = _agent_config() + merged = merge_environment_spec_into_agent_config(config, None) + assert merged == config 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 fe0e3f0ee0..20ec26c3ec 100644 --- a/plugins/nemo-agents/tests/unit/test_fabric_translator.py +++ b/plugins/nemo-agents/tests/unit/test_fabric_translator.py @@ -196,6 +196,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..da96271ce9 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", "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",