diff --git a/.github/skills/README.md b/.github/skills/README.md index a5c4d7d2..e5cce9a8 100644 --- a/.github/skills/README.md +++ b/.github/skills/README.md @@ -78,6 +78,9 @@ Run these directly in Copilot Chat (CLI or VS Code): **Notes:** * If prompted for a build mode, choose remote or local in chat. -* If referenced agents declare discoveryExtensions.knowledgeBases, provide the requested knowledgeBaseId values in /bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}/versions/{version} format. Starter-kit deployment does not create knowledge bases; it patches user-provided IDs into each deployed agent. +* Both deployers default new configurations to Discovery GA `2026-06-01`; existing local configs and saved runs retain explicit API versions. +* Both deployers set agent `humanInTheLoop` and all bound tool `confirmation` values to `Disabled` in deployed agents, overriding source settings for all future use. They do not bypass nodepool capacity confirmation or unexpected platform approvals. +* Agent-deployer validation names its conversation after the investigation so Studio can select its history and associated outputs. Starter-kit deployment does not create validation conversations. +* If referenced agents declare discoveryExtensions.knowledgeBases, provide the requested knowledgeBaseId values in `/bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}` format for GA. Explicit preview configs also accept `/versions/{version}`; GA rejects that suffix. Starter-kit deployment does not create knowledge bases; it patches user-provided IDs into each deployed agent. * If prompted for Supercomputer nodepool confirmation, choose Proceed / Stop in chat before tool builds continue. * Starter-kit deployment deploys each referenced agent individually and does not create a validation investigation; the summary lists deployed agents, deployed tools, and sample prompts you can use to test the deployment. diff --git a/.github/skills/discovery-services-agent-deployer/SKILL.md b/.github/skills/discovery-services-agent-deployer/SKILL.md index 0caf7ea8..c7f67a1a 100644 --- a/.github/skills/discovery-services-agent-deployer/SKILL.md +++ b/.github/skills/discovery-services-agent-deployer/SKILL.md @@ -10,6 +10,8 @@ allowed-tools: shell Runs on Windows, macOS, and Linux through PowerShell 7+ (`pwsh`). Requires `git`, `python` with `pip`, and Azure CLI (`az`). Docker is optional because the runner can use Azure Container Registry Tasks when local Docker is unavailable. +New configurations default to Discovery GA `2026-06-01`. Explicit preview versions in local configs and saved runs remain unchanged. See [configuration](references/configuration.md) for version-specific validation routes and knowledge-base binding formats. + ## What this skill does Deploy one or more Discovery catalog agents from this repository with a single deterministic runner: @@ -20,6 +22,8 @@ pwsh -NoProfile -ExecutionPolicy Bypass -File .github/skills/discovery-services- The single PowerShell script discovers the agent folder, selects its tool folder when present, builds and pushes the image, creates or updates the `Microsoft.Discovery/tools` resource, patches a run-local copy of `agent.yaml`, deploys the agent, validates it with a temporary investigation, and prints a final summary. If the agent has no tool, it skips `build` and `deploy-tool` and continues with agent deployment. It persists stage status to `stage-todos.json` in the run directory so the invoking assistant can keep native Copilot task tracking in sync without duplicating progress logs. +Agent deployment sets `humanInTheLoop` and every bound tool's `confirmation` to `Disabled`, even if the source YAML enables them. This persists for subsequent uses of the deployed agent, not just validation. The source YAML is unchanged. This does not bypass Supercomputer nodepool confirmation, platform-required actions, or approval requests in agent instructions; unexpected validation approvals still require user input. + ## When to use Use this skill for requests like: @@ -171,4 +175,3 @@ If the runner fails, report the failed stage, the action required from the scrip - `references/troubleshooting.md` - common failures and recovery steps Keep `SKILL.md` lean. Prefer the single public runner and references over ad-hoc scripts or long manual instructions. The deployer implementation is intentionally consolidated into `scripts/deploy-discovery-agent.ps1` so there is only one executable script to maintain. - diff --git a/.github/skills/discovery-services-agent-deployer/config.template.json b/.github/skills/discovery-services-agent-deployer/config.template.json index 9ee5e031..66982512 100644 --- a/.github/skills/discovery-services-agent-deployer/config.template.json +++ b/.github/skills/discovery-services-agent-deployer/config.template.json @@ -4,7 +4,7 @@ "acrName": "", "acrResourceGroup": "", "location": "uksouth", - "apiVersion": "2026-02-01-preview", + "apiVersion": "2026-06-01", "workspaceEndpoint": "https://.workspace.discovery.azure.com", "project": "", "tenantId": "", diff --git a/.github/skills/discovery-services-agent-deployer/references/configuration.md b/.github/skills/discovery-services-agent-deployer/references/configuration.md index 4366296a..df7a69b8 100644 --- a/.github/skills/discovery-services-agent-deployer/references/configuration.md +++ b/.github/skills/discovery-services-agent-deployer/references/configuration.md @@ -17,7 +17,7 @@ Copy-Item .github/skills/discovery-services-agent-deployer/config.template.json | `acrName` | Yes | ACR name without `.azurecr.io`. | | `acrResourceGroup` | No | ACR resource group when the registry is not in `resourceGroup`; defaults to `resourceGroup`. | | `location` | Yes | Tool resource region. Ask the user for this value when it is missing; do not infer `uksouth` from the example. | -| `apiVersion` | No | Defaults to `2026-02-01-preview` from `config.template.json`. | +| `apiVersion` | No | Defaults to GA `2026-06-01` from `config.template.json`. Explicit preview overrides are preserved. | | `workspaceEndpoint` | Yes | Discovery workspace endpoint. | | `project` | Yes | Discovery project name. | | `tenantId` | Yes | Entra tenant for Discovery auth. | @@ -30,6 +30,10 @@ Copy-Item .github/skills/discovery-services-agent-deployer/config.template.json ## Copilot-style input behavior +The configured API version applies to ARM tools and workspace agent, investigation, and conversation requests. Validation uses the GA `/conversations/{name}/openai/v1/responses` route without an `api-version` query and sends `agent_reference`. An explicit `2026-02-01-preview` override instead uses `/conversations/{name}/openai/responses?api-version=2026-02-01-preview` and the preview `agent` field, including when polling or resuming validation. + +Existing ignored config files and saved run configs are not automatically migrated. To opt an existing environment into GA, set `apiVersion` to `2026-06-01` before starting a new run. For GA agent bindings, use knowledge-base IDs in `/bookshelves/{bookshelf}/knowledgeBases/{knowledgebase}` format, without `/versions/{version}`. Preview also accepts versioned IDs. Do not silently strip a version from a user-provided ID. + The runner never assumes `config.json` is present and never uses terminal `Read-Host` prompts for required config. Stage 1 verifies these required values are present: `subscriptionId`, `resourceGroup`, `acrName`, `location`, `workspaceEndpoint`, `project`, `tenantId`, `chatModel`, and `forceToolImageRebuild`. If `config.json` is absent or any required value is missing, Stage 1 prints a structured block: ```text @@ -49,4 +53,3 @@ After Stage 1 succeeds, the runner writes the resolved values into the run-local `buildMode` and `confirmSupercomputerNodepools` are intentionally not config fields. If Docker is available, the runner asks for build mode as a per-run Copilot choice and should be rerun with `-BuildMode remote` or `-BuildMode local`. Before building, the runner prints the tool image and recommended SKU choices; ask the customer to Proceed or Stop based on Supercomputer nodepool capacity. If they choose Proceed, rerun build with `-ConfirmSupercomputerNodepools`. Do not infer these choices and do not write them to `config.json`. If Docker is unavailable, the runner uses remote ACR build. Validation prompts are generated at runtime by default. If `testPrompt` is present and non-empty in `config.json`, it acts as a customer-provided override and validation uses it directly. Otherwise, for tool-backed agents, GitHub Copilot should generate a short, concrete validation prompt from the deployed agent/tool context at validation time and pass it with `-ValidationPrompt`. - diff --git a/.github/skills/discovery-services-agent-deployer/references/operations.md b/.github/skills/discovery-services-agent-deployer/references/operations.md index 9dc1a45a..10c1b727 100644 --- a/.github/skills/discovery-services-agent-deployer/references/operations.md +++ b/.github/skills/discovery-services-agent-deployer/references/operations.md @@ -22,6 +22,12 @@ The runner is a single self-contained PowerShell script and owns: Tool deployment resolves `{name}.azurecr.io/` and `{name}/` image registry placeholders without changing Handlebars action inputs such as `{{name}}`. +The run-local agent YAML overrides `discoveryExtensions.humanInTheLoop` and every +bound tool's `confirmation` to `Disabled`, including explicit `Enabled` source +settings. The upsert persists these settings for all subsequent uses of the +deployed agent; the source YAML is not changed. This does not disable unrelated +platform approvals or prevent instructions from asking for confirmation. + Only `scripts/deploy-discovery-agent.ps1` is used for deployment logic. Keep new stage behavior in that script rather than adding separate internal stage scripts, so this skill stays aligned with the single-script `discovery-services-starter-kit-deployer` layout. ## Stage-at-a-time execution @@ -62,6 +68,12 @@ records the conversation, response, investigation, original prompt, verdict, and diagnostic reason. Failed and timed-out investigations are retained even when successful-test cleanup is enabled. +New validation conversations use the investigation's name as their display +name, matching Studio's primary-conversation selection for session history and +output assets. This applies to initial and retry investigations. Existing +conversations are not renamed or merged; already-created sessions may still +show an empty primary conversation. + For tool-backed agents, success requires a correlated call and successful output from the deployed tool, not just an assistant message. Code environments must additionally print this receipt in tool stdout after executing assertions diff --git a/.github/skills/discovery-services-agent-deployer/scripts/deploy-discovery-agent.ps1 b/.github/skills/discovery-services-agent-deployer/scripts/deploy-discovery-agent.ps1 index cc9688af..9fc089fa 100644 --- a/.github/skills/discovery-services-agent-deployer/scripts/deploy-discovery-agent.ps1 +++ b/.github/skills/discovery-services-agent-deployer/scripts/deploy-discovery-agent.ps1 @@ -323,7 +323,7 @@ function Load-BuilderConfig { Write-Host "[config] Location '$Location' is outside the preferred list (eastus, uksouth, swedencentral). Continuing with custom value." } } - $ApiVersion = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'apiVersion' -Default '2026-02-01-preview' -PromptLabel 'apiVersion' + $ApiVersion = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'apiVersion' -Default '2026-06-01' -PromptLabel 'apiVersion' $WorkspaceEndpoint = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'workspaceEndpoint' -Required:($Required.ContainsKey('workspaceEndpoint')) -PromptLabel 'workspaceEndpoint (https://.workspace.discovery.azure.com)' $Project = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'project' -Required:($Required.ContainsKey('project')) -PromptLabel 'project' $TenantId = Resolve-ConfigField -ConfigObj $WorkingConfig -Field 'tenantId' -Required:($Required.ContainsKey('tenantId')) -PromptLabel 'tenantId' @@ -1317,19 +1317,24 @@ if isinstance(model, dict): model['id'] = chat_model doc['model'] = model -if tool_id: - disc = doc.get('discoveryExtensions') or {} - tools = disc.get('tools') or [] - if not isinstance(tools, list): - tools = [] +disc = doc.get('discoveryExtensions') or {} +if not isinstance(disc, dict): + raise SystemExit('Invalid discoveryExtensions: expected a mapping') +disc['humanInTheLoop'] = 'Disabled' +tools = disc.get('tools') or [] +if not isinstance(tools, list): + raise SystemExit('Invalid discoveryExtensions.tools: expected a list') +for item in tools: + if not isinstance(item, dict): + raise SystemExit('Invalid discoveryExtensions.tools entry: expected a mapping') + item['confirmation'] = 'Disabled' +if tool_id: tool_leaf = tool_id.rsplit('/', 1)[-1].lower() replaced_any = False deduped = [] seen_leaves = set() for item in tools: - if not isinstance(item, dict): - continue current = str(item.get('toolId', '')).strip() current_leaf = current.rsplit('/', 1)[-1].lower() if current else '' is_placeholder = current.startswith('{{') and current.endswith('}}') @@ -1339,8 +1344,6 @@ if tool_id: if tool_leaf in seen_leaves: continue item['toolId'] = tool_id - if 'confirmation' not in item: - item['confirmation'] = 'Disabled' deduped.append(item) seen_leaves.add(tool_leaf) replaced_any = True @@ -1355,7 +1358,7 @@ if tool_id: deduped.append({'toolId': tool_id, 'confirmation': 'Disabled'}) disc['tools'] = deduped - doc['discoveryExtensions'] = disc +doc['discoveryExtensions'] = disc with open(path, 'w', encoding='utf-8') as f: yaml.safe_dump(doc, f, sort_keys=False, allow_unicode=False) @@ -1368,6 +1371,7 @@ $PatchArgs = @($PatchPy, $TempAgentYaml, $Config.chatModel) if ($HasTool) { $PatchArgs += $State.deploy.toolResourceId } python @PatchArgs if ($LASTEXITCODE -ne 0) { throw 'Failed to patch temp agent.yaml' } +Write-Host '[stage-04] Agent human-in-the-loop and tool confirmations are disabled for the deployed agent.' $CurrentTenant = (az account show --query tenantId -o tsv --only-show-errors).Trim() if ($CurrentTenant -ne $Config.tenantId) { @@ -1861,6 +1865,15 @@ def call_with_retries(method, url, **kwargs): base = cfg["workspaceEndpoint"].rstrip("/") api = cfg["apiVersion"] project = cfg["project"] +is_ga_api = api >= "2026-06-01" + +def response_url(conversation_name, response_id=None): + route = "openai/v1/responses" if is_ga_api else "openai/responses" + url = f"{base}/conversations/{conversation_name}/{route}" + if response_id: + url += f"/{response_id}" + return url if is_ga_api else f"{url}?api-version={api}" + safe = re.sub(r"[^a-z0-9-]", "-", agent_name.lower())[:8].strip("-") or "agent" # Use deterministic investigation naming (timestamp-based, not random) @@ -1883,7 +1896,7 @@ else: create = call_with_retries("PUT", inv_url, headers=headers(t), json={"description": "Post-deploy validation", "displayName": f"Validation Test {agent_name}"}) create.raise_for_status() conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={ - "displayName": f"validation-{agent_name}", + "displayName": inv, "investigationName": f"/projects/{project}/investigations/{inv}", "projectName": project, }) @@ -1978,11 +1991,11 @@ def assess_response(body): def wait_for_completion(conversation_name, text_prompt, wait_seconds): t_local = token() if resume_response: - resp = call_with_retries("GET", f"{base}/conversations/{conversation_name}/openai/v1/responses/{resume_response}", headers=headers(t_local)) + resp = call_with_retries("GET", response_url(conversation_name, resume_response), headers=headers(t_local)) else: - resp = call_with_retries("POST", f"{base}/conversations/{conversation_name}/openai/v1/responses", headers=headers(t_local), json={ + resp = call_with_retries("POST", response_url(conversation_name), headers=headers(t_local), json={ "input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": text_prompt}]}], - "agent": {"type": "agent_reference", "name": agent_name}, + ("agent_reference" if is_ga_api else "agent"): {"type": "agent_reference", "name": agent_name}, }) resp.raise_for_status() body_local = resp.json() @@ -2009,7 +2022,7 @@ def wait_for_completion(conversation_name, text_prompt, wait_seconds): elapsed = int(time.time() - start_time) print(f"[validation] Polling attempt {poll_count}, elapsed {elapsed}s, status={status_local}...", flush=True) t_local = token() - poll = call_with_retries("GET", f"{base}/conversations/{conversation_name}/openai/v1/responses/{rid_local}", headers=headers(t_local)) + poll = call_with_retries("GET", response_url(conversation_name, rid_local), headers=headers(t_local)) poll.raise_for_status() body_local = poll.json() status_local = body_local.get("status", "") @@ -2092,7 +2105,7 @@ for attempt in range(1, max_retries + 1): _created_inv_urls.append(inv_url) # Create new conversation with new investigation conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={ - "displayName": f"validation-{agent_name}", + "displayName": inv, "investigationName": f"/projects/{project}/investigations/{inv}", "projectName": project, }) @@ -2126,7 +2139,7 @@ for attempt in range(1, max_retries + 1): _created_inv_urls.append(inv_url) # Create new conversation with new investigation conv = call_with_retries("POST", f"{base}/conversations?api-version={api}", headers=headers(t), json={ - "displayName": f"validation-{agent_name}", + "displayName": inv, "investigationName": f"/projects/{project}/investigations/{inv}", "projectName": project, }) diff --git a/.github/skills/discovery-services-starter-kit-deployer/SKILL.md b/.github/skills/discovery-services-starter-kit-deployer/SKILL.md index 809b83c9..abdf70fa 100644 --- a/.github/skills/discovery-services-starter-kit-deployer/SKILL.md +++ b/.github/skills/discovery-services-starter-kit-deployer/SKILL.md @@ -10,6 +10,8 @@ allowed-tools: shell Runs on Windows, macOS, and Linux through PowerShell 7+ (`pwsh`). Requires `git`, `python` with `pip`, and Azure CLI (`az`). Docker is optional because the runner can use Azure Container Registry Tasks when local Docker is unavailable. +New configurations default to Discovery GA `2026-06-01`. Explicit preview versions in local configs and saved runs remain unchanged; knowledge-base bindings are validated against the selected API version. + ## What this skill does Deploy a starter-kit from `starter-kits\` as individual Discovery prompt agents: @@ -20,6 +22,8 @@ pwsh -NoProfile -ExecutionPolicy Bypass -File .github\skills\discovery-services- The runner reads `starter-kits\\kit.json`, validates every top-level `agentRefs[].ref` resolves to an existing `agents\\` folder, builds every tool from those agents, deploys the tool resources in parallel, patches each referenced source agent with its own deployed tool resource ids, deploys all referenced agents in parallel, and prints a summary of deployed agents, deployed tools, and all top-level `samplePrompts` the customer can run manually. +For each deployed agent, the runner sets `humanInTheLoop` and every bound tool's `confirmation` to `Disabled`, even if the source YAML enables them. This persists for subsequent uses of the deployed agents; source YAML files remain unchanged. It does not bypass Supercomputer nodepool confirmation or approval requests in agent instructions. Starter-kit deployment does not run validation or create conversations. + ## When to use Use this skill for requests like: @@ -70,7 +74,7 @@ For `location`, use a multiple-choice prompt with `eastus`, `swedencentral`, and 3. Parse `agentRefs[].ref` and ensure every referenced `agents\\agent.yaml` exists before creating deployment TODOs. 4. Create the native Copilot TODO list only after starter-kit preflight succeeds. 5. Run stages from the repo root. Keep all starter-kit-level run artifacts under `starter-kits\tmp\\\`. For every new starter-kit run, choose `remote` or `local` build mode during `init`; do not let `auto` proceed silently. -6. During `init`, the runner scans every referenced `agent.yaml` for `discoveryExtensions.knowledgeBases`. Knowledge bases are not deployed by this skill. If any are declared, the runner emits `KNOWLEDGE_BASE_INPUT_REQUIRED=true` plus one `KNOWLEDGE_BASE_REQUIRED agent= count=` line per affected agent and a suggested JSON shape. Ask the user to provide the actual `knowledgeBaseId` values in `/bookshelves/{bookshelf_name}/knowledgeBases/{knowledgebase_name}/versions/{version}` format, save them to a temporary JSON file or pass inline JSON, and rerun `init` with `-KnowledgeBasesJson `. The JSON must be keyed by agent name and each value must be an array of objects with `knowledgeBaseId`; the runner stores these values in `run-state.json` and patches each agent before deployment. +6. During `init`, the runner scans every referenced `agent.yaml` for `discoveryExtensions.knowledgeBases`. Knowledge bases are not deployed by this skill. If any are declared, the runner emits `KNOWLEDGE_BASE_INPUT_REQUIRED=true` plus one `KNOWLEDGE_BASE_REQUIRED agent= count=` line per affected agent and a suggested JSON shape. Ask the user to provide the actual `knowledgeBaseId` values in `/bookshelves/{bookshelf_name}/knowledgeBases/{knowledgebase_name}` format for GA `2026-06-01` (the default). Explicit preview configs also accept a `/versions/{version}` suffix; GA rejects it. Follow the runner's `KNOWLEDGE_BASE_ID_FORMAT`; do not silently strip versions. Save the IDs to a temporary JSON file or pass inline JSON, and rerun `init` with `-KnowledgeBasesJson `. The JSON must be keyed by agent name and each value must be an array of objects with `knowledgeBaseId`; the runner stores these values in `run-state.json` and patches each agent before deployment. 7. Before building tools, run the runner's `build-tools` stage without `-ConfirmSupercomputerNodepools` so it prints the authoritative `TOOL_BUILD_PLAN` lines, writes `\build-plan.json`, and pauses with `TASK_STATUS=build-tools:input_required`. Do not use `rg`, `grep`, or ad-hoc file scanning to inspect generated `tool.yaml` files; those tools may not be installed in the customer's shell, and the runner already parses `recommended_sku` choices consistently. Show the customer every tool image and the `recommendedSkus` choices from the `TOOL_BUILD_PLAN` output or `BUILD_PLAN_JSON` path. Explain that the SKUs listed for a tool are alternative nodepool choices, not cumulative requirements; the customer needs capacity for at least one listed SKU per tool. Then ask whether to proceed or stop based on their Supercomputer nodepool capacity. Use `ask_user` with choices `Proceed - I have Supercomputer nodepool capacity for at least one listed SKU per tool` and `Stop - I do not have the required Supercomputer nodepool capacity`. Ask this at most once per run. If they choose Proceed after `build-tools` requested confirmation, rerun `build-tools` with `-ConfirmSupercomputerNodepools`; do not persist that confirmation in `config.json` because it must be reconfirmed for every new starter-kit run. If they choose Stop, do not run `build-tools`; immediately run `-Stage stop -RunDir ` so the runner emits `TASK_STATUS=:stopped` for every full-deployment task (`init`, `build-tools`, `deploy-tools`, `deploy-agent`, `summary`), even if `init` had already completed. Existing ACR tags are reused by default; `forceToolImageRebuild: true` overrides reuse and queues a rebuild for the same tag. In remote mode, submit all required ACR builds first, then track every run independently; if any build fails, stop the deployment and report the failed tool list. Agents without `tools\*\tool.yaml` are recorded as skipped. 8. Deploy every built tool in the `deploy-tools` stage and capture each `Microsoft.Discovery/tools/` resource id. The runner deploys tool resources in parallel. 9. Deploy every referenced source agent individually in the `deploy-agent` stage. The runner patches each agent's `discoveryExtensions.tools` to point to the tool resources deployed for that agent, patches `discoveryExtensions.knowledgeBases` with user-provided knowledge base ids when required, writes run-local patched YAML files under `\agents\`, and upserts the agents in parallel. It does not synthesize a combined starter-kit agent. @@ -121,5 +125,3 @@ When the runner completes, summarize: If the runner fails, report the failed stage and required action from the output. Include the exact stage command with `-RunDir ` for recoverable failures, except when the customer chose Stop for Supercomputer nodepool capacity; in that case, do not provide a direct PowerShell rerun command and say exactly: "When you have Supercomputer nodepool capacity for at least one of the listed SKUs per tool, rerun the skill." - - diff --git a/.github/skills/discovery-services-starter-kit-deployer/config.template.json b/.github/skills/discovery-services-starter-kit-deployer/config.template.json index b8f35d58..ca4ff4bc 100644 --- a/.github/skills/discovery-services-starter-kit-deployer/config.template.json +++ b/.github/skills/discovery-services-starter-kit-deployer/config.template.json @@ -4,11 +4,10 @@ "acrName": "", "acrResourceGroup": "", "location": "uksouth", - "apiVersion": "2026-02-01-preview", + "apiVersion": "2026-06-01", "workspaceEndpoint": "https://.workspace.discovery.azure.com", "project": "", "tenantId": "", "chatModel": "", "forceToolImageRebuild": false } - diff --git a/.github/skills/discovery-services-starter-kit-deployer/scripts/deploy-discovery-starter-kit.ps1 b/.github/skills/discovery-services-starter-kit-deployer/scripts/deploy-discovery-starter-kit.ps1 index af72c0b7..88220ce3 100644 --- a/.github/skills/discovery-services-starter-kit-deployer/scripts/deploy-discovery-starter-kit.ps1 +++ b/.github/skills/discovery-services-starter-kit-deployer/scripts/deploy-discovery-starter-kit.ps1 @@ -215,18 +215,26 @@ print(json.dumps(rows)) } function Resolve-KnowledgeBaseInput { - param([object[]]$Requirements, [string]$InputJson) + param([object[]]$Requirements, [string]$InputJson, [Parameter(Mandatory)][string]$ApiVersion) if (@($Requirements).Count -eq 0) { return @{} } + $idFormat = '/bookshelves/{bookshelf_name}/knowledgeBases/{knowledgebase_name}' + $suggestedId = '/bookshelves//knowledgeBases/' + $idPattern = '^/bookshelves/[^/]+/knowledgeBases/[^/]+$' + if ($ApiVersion -lt '2026-06-01') { + $idFormat += ' (optionally followed by /versions/{version})' + $suggestedId += '/versions/' + $idPattern = '^/bookshelves/[^/]+/knowledgeBases/[^/]+(?:/versions/[^/]+)?$' + } if ([string]::IsNullOrWhiteSpace($InputJson)) { $suggested = [ordered]@{} foreach ($row in @($Requirements)) { $suggested[[string]$row.agent] = @(@($row.knowledgeBases) | ForEach-Object { - [ordered]@{ knowledgeBaseId = '/bookshelves//knowledgeBases//versions/' } + [ordered]@{ knowledgeBaseId = $suggestedId } }) } Write-Host 'KNOWLEDGE_BASE_INPUT_REQUIRED=true' Write-Host 'KNOWLEDGE_BASE_INPUT_FORMAT=json-file-or-inline-json' - Write-Host 'KNOWLEDGE_BASE_ID_FORMAT=/bookshelves/{bookshelf_name}/knowledgeBases/{knowledgebase_name}/versions/{version}' + Write-Host ("KNOWLEDGE_BASE_ID_FORMAT={0}" -f $idFormat) foreach ($row in @($Requirements)) { Write-Host ("KNOWLEDGE_BASE_REQUIRED agent={0} count={1}" -f $row.agent, @($row.knowledgeBases).Count) } @@ -244,8 +252,8 @@ function Resolve-KnowledgeBaseInput { if ($items.Count -eq 0) { throw "KnowledgeBasesJson agent '$agentName' must include at least one knowledgeBaseId." } foreach ($item in $items) { $kbId = if ($item -is [System.Collections.IDictionary]) { [string]$item['knowledgeBaseId'] } else { [string]$item.knowledgeBaseId } - if ($kbId -notmatch '^/bookshelves/[^/]+/knowledgeBases/[^/]+/versions/[^/]+$') { - throw "Invalid knowledgeBaseId for agent '$agentName': '$kbId'. Expected /bookshelves/{bookshelf_name}/knowledgeBases/{knowledgebase_name}/versions/{version}." + if ($kbId -match '[{}<>]' -or $kbId -notmatch $idPattern) { + throw "Invalid knowledgeBaseId for agent '$agentName': '$kbId'. Expected $idFormat for API version '$ApiVersion'." } } } @@ -846,10 +854,13 @@ def patch_agent_doc(agent_item): provided_kbs = (plan.get("knowledgeBases") or {}).get(agent_item["name"]) if provided_kbs is not None: ext["knowledgeBases"] = provided_kbs - elif ext.get("knowledgeBases"): - unresolved = [kb for kb in ext.get("knowledgeBases") or [] if "{{" in str(kb.get("knowledgeBaseId", "")) or not re.match(r"^/bookshelves/[^/]+/knowledgeBases/[^/]+/versions/[^/]+$", str(kb.get("knowledgeBaseId", "")))] - if unresolved: - raise RuntimeError(f"{agent_item['name']} has unresolved knowledgeBases but no values were provided in KnowledgeBasesJson.") + kb_pattern = r"/bookshelves/[^/]+/knowledgeBases/[^/]+" + if cfg["apiVersion"] < "2026-06-01": + kb_pattern += r"(?:/versions/[^/]+)?" + for kb in ext.get("knowledgeBases") or []: + kb_id = str(kb.get("knowledgeBaseId", "")) + if re.search(r"[{}<>]", kb_id) or not re.fullmatch(kb_pattern, kb_id): + raise RuntimeError(f"{agent_item['name']} has invalid or unresolved knowledgeBases for API version {cfg['apiVersion']}: '{kb_id}'. Provide matching IDs in KnowledgeBasesJson.") doc["discoveryExtensions"] = ext return doc @@ -1046,7 +1057,7 @@ switch ($Stage) { } $kbRequirements = @(Get-AgentKnowledgeBaseRequirements -Agents @($pluginData.agents)) try { - $knowledgeBases = Resolve-KnowledgeBaseInput -Requirements $kbRequirements -InputJson $KnowledgeBasesJson + $knowledgeBases = Resolve-KnowledgeBaseInput -Requirements $kbRequirements -InputJson $KnowledgeBasesJson -ApiVersion $config.apiVersion } catch { if ([string]$_.Exception.Message -like 'KNOWLEDGE_BASE_INPUT_REQUIRED:*') { Write-Host 'TASK_STATUS=init:input_required' @@ -1081,4 +1092,3 @@ switch ($Stage) { } } - diff --git a/.github/tests/test_agent_deployer_validation.py b/.github/tests/test_agent_deployer_validation.py index ba795ea2..e9e42435 100644 --- a/.github/tests/test_agent_deployer_validation.py +++ b/.github/tests/test_agent_deployer_validation.py @@ -92,7 +92,10 @@ def setUp(self): self.tool.write_text("code_environments:\n - language: r\n", encoding="utf-8") self.result = self.root / "validation-result.json" - def run_validator(self, bodies, *, tool=True, continuation=False, retries=1): + def run_validator(self, bodies, *, tool=True, continuation=False, retries=1, api_version="2026-06-01", wait_seconds=1): + config = json.loads(self.config.read_text(encoding="utf-8")) + config["apiVersion"] = api_version + self.config.write_text(json.dumps(config), encoding="utf-8") bodies = iter(bodies) calls = [] current = None @@ -101,12 +104,11 @@ def run_validator(self, bodies, *, tool=True, continuation=False, retries=1): def request(method, url, **kwargs): nonlocal current calls.append((method, url, kwargs)) - if method == "POST" and url.endswith("/responses"): + if method == "POST" and "/responses" in url: current = next(bodies) data = current elif method == "GET": - if current is None: - current = next(bodies) + current = next(bodies, current) data = current elif method == "POST" and "/conversations?" in url: data = {"name": f"conversation-{next(conversations)}"} @@ -125,7 +127,7 @@ def request(method, url, **kwargs): log = io.StringIO() error = None with patch.object(sys, "argv", args), patch.dict(os.environ, { - "VALIDATION_WAIT_SECONDS": "1", "VALIDATION_MAX_RETRIES": str(retries), + "VALIDATION_WAIT_SECONDS": str(wait_seconds), "VALIDATION_MAX_RETRIES": str(retries), }), patch("requests.request", side_effect=request), patch("subprocess.run", return_value=Mock(stdout="test-token")), \ patch("time.sleep"), patch("time.time", side_effect=lambda: next(clock)), \ patch("atexit.register"), patch("signal.signal"), redirect_stdout(log): @@ -138,6 +140,79 @@ def request(method, url, **kwargs): def test_embedded_script_compiles(self): ast.parse(SOURCE) + def assert_conversations_match_investigations(self, calls, expected_count): + creates = [kw["json"] for method, url, kw in calls + if method == "POST" and "/conversations?" in url] + self.assertEqual(len(creates), expected_count) + for body in creates: + self.assertEqual(body["displayName"], body["investigationName"].rsplit("/", 1)[-1]) + self.assertEqual(body["projectName"], "test") + + def test_initial_conversation_is_studio_primary(self): + _, calls, _, error = self.run_validator([response(execution())]) + self.assertIsNone(error) + self.assert_conversations_match_investigations(calls, 1) + + def test_retry_conversations_are_studio_primary(self): + for first in (response([], "failed"), response([], "in_progress")): + with self.subTest(status=first["status"]): + self.result.unlink(missing_ok=True) + result, calls, _, error = self.run_validator( + [first, response(execution())], retries=2, + ) + self.assertIsNone(error) + self.assertEqual(result["status"], "passed") + self.assert_conversations_match_investigations(calls, 2) + + def test_ga_response_request_contract(self): + result, calls, _, error = self.run_validator([response(execution())]) + self.assertIsNone(error) + self.assertEqual(result["status"], "passed") + request = next(c for c in calls if c[0] == "POST" and "/responses" in c[1]) + self.assertEqual(request[1], "https://workspace.example/conversations/conversation-1/openai/v1/responses") + self.assertEqual(request[2]["json"]["agent_reference"], {"type": "agent_reference", "name": "RStatisticsAgent"}) + self.assertNotIn("agent", request[2]["json"]) + for method, url, _ in calls: + if "/responses" not in url: + self.assertIn("api-version=2026-06-01", url) + + def test_explicit_preview_response_request_contract(self): + result, calls, _, error = self.run_validator( + [response(execution())], api_version="2026-02-01-preview", + ) + self.assertIsNone(error) + self.assertEqual(result["status"], "passed") + request = next(c for c in calls if c[0] == "POST" and "/responses" in c[1]) + self.assertEqual(request[1], "https://workspace.example/conversations/conversation-1/openai/responses?api-version=2026-02-01-preview") + self.assertEqual(request[2]["json"]["agent"], {"type": "agent_reference", "name": "RStatisticsAgent"}) + self.assertNotIn("agent_reference", request[2]["json"]) + + def test_polling_and_resuming_use_selected_response_api(self): + for version, suffix in ( + ("2026-06-01", "/openai/v1/responses/response-1"), + ("2026-02-01-preview", "/openai/responses/response-1?api-version=2026-02-01-preview"), + ): + with self.subTest(version=version): + self.result.unlink(missing_ok=True) + result, calls, _, error = self.run_validator( + [response([], "in_progress"), response(execution())], + api_version=version, wait_seconds=900, + ) + self.assertIsNone(error) + self.assertEqual(result["status"], "passed") + gets = [url for method, url, _ in calls if method == "GET"] + self.assertEqual(gets, ["https://workspace.example/conversations/conversation-1" + suffix]) + pending = response([], "requires_action") + pending["required_action"] = {"type": "submit_tool_outputs"} + self.run_validator([pending], api_version=version) + result, calls, _, error = self.run_validator([response(execution())], api_version=version) + self.assertIsNone(error) + self.assertEqual(result["status"], "passed") + self.assertFalse(any(method == "POST" for method, _, _ in calls)) + self.assertEqual([(method, url) for method, url, _ in calls if method == "GET"], [ + ("GET", "https://workspace.example/conversations/conversation-1" + suffix), + ]) + def test_real_execution_and_receipt_pass(self): result, _, _, error = self.run_validator([response(execution())]) self.assertIsNone(error) diff --git a/.github/tests/test_deployer_api_contracts.py b/.github/tests/test_deployer_api_contracts.py new file mode 100644 index 00000000..51c5ca3f --- /dev/null +++ b/.github/tests/test_deployer_api_contracts.py @@ -0,0 +1,268 @@ +"""Offline API-version and knowledge-base binding regressions for both deployers.""" + +import ast +import copy +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest +from unittest.mock import Mock + +import yaml + + +SKILLS = Path(__file__).resolve().parents[1] / "skills" +AGENT = SKILLS / "discovery-services-agent-deployer" +STARTER = SKILLS / "discovery-services-starter-kit-deployer" +AGENT_SOURCE = (AGENT / "scripts" / "deploy-discovery-agent.ps1").read_text(encoding="utf-8") +STARTER_SOURCE = (STARTER / "scripts" / "deploy-discovery-starter-kit.ps1").read_text(encoding="utf-8") +GA_ID = "/bookshelves/library/knowledgeBases/reference" +PREVIEW_ID = GA_ID + "/versions/1" + + +class ApiDefaultsTests(unittest.TestCase): + def test_both_templates_default_to_ga(self): + for skill in (AGENT, STARTER): + with self.subTest(skill=skill.name): + template = json.loads((skill / "config.template.json").read_text(encoding="utf-8")) + self.assertEqual(template["apiVersion"], "2026-06-01") + + @unittest.skipUnless(shutil.which("pwsh"), "PowerShell 7 is required") + def test_agent_default_and_explicit_preview_override(self): + assignment = next(line for line in AGENT_SOURCE.splitlines() if "$ApiVersion = Resolve-ConfigField" in line) + script = """ +function Resolve-ConfigField { + param($ConfigObj, $Field, $Default, $PromptLabel) + if ($ConfigObj.ContainsKey($Field)) { return $ConfigObj[$Field] } + return $Default +} +$WorkingConfig = @{} +""" + assignment + """ +if ($ApiVersion -ne '2026-06-01') { throw "Unexpected default: $ApiVersion" } +$WorkingConfig = @{ apiVersion = '2026-02-01-preview' } +""" + assignment + """ +if ($ApiVersion -ne '2026-02-01-preview') { throw "Override was replaced: $ApiVersion" } +""" + result = subprocess.run(["pwsh", "-NoProfile", "-Command", script], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + + @unittest.skipUnless(shutil.which("pwsh"), "PowerShell 7 is required") + def test_starter_default_and_explicit_preview_override(self): + functions = "function Test-ConfigValuePresent" + STARTER_SOURCE.split( + "function Test-ConfigValuePresent", 1, + )[1].split("function Write-BuildModeInputRequest", 1)[0] + with tempfile.TemporaryDirectory() as directory: + local = Path(directory) / "config.json" + fallback = Path(directory) / "agent-config.json" + for agent_override, starter_override, expected in ( + ({}, {}, "2026-06-01"), + ({"apiVersion": "2026-02-01-preview"}, {}, "2026-02-01-preview"), + ({"apiVersion": "2026-06-01"}, {"apiVersion": "2026-02-01-preview"}, "2026-02-01-preview"), + ): + with self.subTest(agent=agent_override, starter=starter_override): + local.write_text(json.dumps(starter_override), encoding="utf-8") + fallback.write_text(json.dumps(agent_override), encoding="utf-8") + script = """ +$ErrorActionPreference = 'Stop' +function Get-ConfigPath { $env:TEST_LOCAL } +function Get-AgentDeployerConfigPath { $env:TEST_FALLBACK } +function Get-TemplateConfigPath { $env:TEST_TEMPLATE } +""" + functions + """ +$config = Load-StarterConfig -RequiredFields @() +if ($config.apiVersion -ne $env:TEST_EXPECTED) { throw "Unexpected version: $($config.apiVersion)" } +""" + result = subprocess.run( + ["pwsh", "-NoProfile", "-Command", script], + env={**os.environ, "TEST_LOCAL": str(local), "TEST_FALLBACK": str(fallback), + "TEST_TEMPLATE": str(STARTER / "config.template.json"), "TEST_EXPECTED": expected}, + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + +class StarterKnowledgeBaseTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("pwsh"), "PowerShell 7 is required") + def test_input_validation_matches_selected_api(self): + function = "function Resolve-KnowledgeBaseInput" + STARTER_SOURCE.split( + "function Resolve-KnowledgeBaseInput", 1, + )[1].split("function New-StarterRun", 1)[0] + for version, kb_id, valid in ( + ("2026-06-01", GA_ID, True), + ("2026-06-01", PREVIEW_ID, False), + ("2026-02-01-preview", GA_ID, True), + ("2026-02-01-preview", PREVIEW_ID, True), + ("2026-06-01", GA_ID + "/unexpected", False), + ("2026-06-01", "/bookshelves/{{library}}/knowledgeBases/reference", False), + ("2026-02-01-preview", "/bookshelves//knowledgeBases/reference", False), + ): + with self.subTest(version=version, kb_id=kb_id): + script = "$ErrorActionPreference = 'Stop'\n" + function + """ +$requirements = @(@{ agent = 'TestAgent'; knowledgeBases = @(@{}) }) +Resolve-KnowledgeBaseInput -Requirements $requirements -InputJson $env:TEST_KBS -ApiVersion $env:TEST_API | Out-Null +""" + result = subprocess.run( + ["pwsh", "-NoProfile", "-Command", script], + env={**os.environ, "TEST_KBS": json.dumps({"TestAgent": [{"knowledgeBaseId": kb_id}]}), "TEST_API": version}, + capture_output=True, text=True, + ) + self.assertEqual(result.returncode == 0, valid, result.stderr) + + def test_deployment_binding_matches_selected_api(self): + script = STARTER_SOURCE.split("def tool_leaf(", 1)[1].split("def write_agent_yaml(", 1)[0] + script = "def tool_leaf(" + script + ast.parse(script) + for version, kb_id, valid in ( + ("2026-06-01", GA_ID, True), + ("2026-06-01", PREVIEW_ID, False), + ("2026-02-01-preview", GA_ID, True), + ("2026-02-01-preview", PREVIEW_ID, True), + ("2026-06-01", "/bookshelves/{{library}}/knowledgeBases/reference", False), + ("2026-02-01-preview", "/bookshelves//knowledgeBases/reference", False), + ): + for provided in (False, True): + with self.subTest(version=version, kb_id=kb_id, provided=provided), tempfile.TemporaryDirectory() as directory: + agent = Path(directory) / "agent.yaml" + kbs = [{"knowledgeBaseId": kb_id}] + agent.write_text(yaml.safe_dump({ + "name": "TestAgent", + "discoveryExtensions": {"knowledgeBases": kbs}, + }), encoding="utf-8") + context = { + "copy": copy, "yaml": yaml, "re": re, + "cfg": {"apiVersion": version}, + "plan": {"knowledgeBases": {"TestAgent": kbs} if provided else {}}, + } + exec(compile(script, str(STARTER), "exec"), context) + if valid: + doc = context["patch_agent_doc"]({"name": "TestAgent", "agentYaml": str(agent)}) + self.assertEqual(doc["discoveryExtensions"]["knowledgeBases"], kbs) + else: + with self.assertRaisesRegex(RuntimeError, "knowledgeBases"): + context["patch_agent_doc"]({"name": "TestAgent", "agentYaml": str(agent)}) + + +class ConfirmationSettingsTests(unittest.TestCase): + def test_agent_without_existing_extensions_disables_plan_confirmation(self): + patch_script = AGENT_SOURCE.split("$PatchPy = Join-Path", 1)[1].split("@'\n", 1)[1].split("\n'@ | Set-Content $PatchPy", 1)[0] + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "agent.yaml" + path.write_text("name: NoToolAgent\nmodel:\n id: '{{CHAT-MODEL}}'\n", encoding="utf-8") + result = subprocess.run( + [shutil.which("python") or "python", "-c", patch_script, str(path), "model"], + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + doc = yaml.safe_load(path.read_text(encoding="utf-8")) + self.assertEqual(doc["discoveryExtensions"], {"humanInTheLoop": "Disabled"}) + + def test_agent_upsert_payload_retains_disabled_settings(self): + script = "def build_prompt_definition(" + AGENT_SOURCE.split( + "def build_prompt_definition(", 1, + )[1].split("def upsert_agent(", 1)[0] + context = {} + exec(compile(script, str(AGENT), "exec"), context) + payload = context["build_payload"]({ + "name": "Example", "model": {"id": "model"}, + "discoveryExtensions": { + "humanInTheLoop": "Disabled", + "tools": [{"toolId": "tool-id", "confirmation": "Disabled"}], + }, + }) + self.assertEqual(payload["humanInTheLoop"], "Disabled") + self.assertEqual(payload["tools"], [{"toolId": "tool-id", "confirmation": "Disabled"}]) + + def test_agent_patch_disables_all_confirmations_and_preserves_bindings(self): + patch_script = AGENT_SOURCE.split("$PatchPy = Join-Path", 1)[1].split("@'\n", 1)[1].split("\n'@ | Set-Content $PatchPy", 1)[0] + deployed_id = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Discovery/tools/test" + other_id = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Discovery/tools/other" + for tool_id in (deployed_id, ""): + with self.subTest(tool_id=tool_id), tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "agent.yaml" + source = { + "name": "Example", "model": {"id": "{{CHAT-MODEL}}"}, + "discoveryExtensions": { + "humanInTheLoop": "Enabled", + "tools": [ + {"toolId": deployed_id, "confirmation": "Enabled"}, + {"toolId": other_id, "confirmation": "Enabled"}, + ], + "knowledgeBases": [{"knowledgeBaseId": GA_ID}], + }, + } + path.write_text(yaml.safe_dump(source), encoding="utf-8") + result = subprocess.run( + [shutil.which("python") or "python", "-c", patch_script, str(path), "model", tool_id], + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + doc = yaml.safe_load(path.read_text(encoding="utf-8")) + ext = doc["discoveryExtensions"] + self.assertEqual(ext["humanInTheLoop"], "Disabled") + self.assertEqual([x["toolId"] for x in ext["tools"]], [deployed_id, other_id]) + self.assertTrue(all(x["confirmation"] == "Disabled" for x in ext["tools"])) + self.assertEqual(ext["knowledgeBases"], source["discoveryExtensions"]["knowledgeBases"]) + + def test_starter_patch_disables_confirmations_even_for_existing_tools(self): + script = "def tool_leaf(" + STARTER_SOURCE.split("def tool_leaf(", 1)[1].split("def write_agent_yaml(", 1)[0] + deployed_id = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Discovery/tools/test" + other_id = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Discovery/tools/other" + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "agent.yaml" + path.write_text(yaml.safe_dump({ + "name": "Example", + "discoveryExtensions": { + "humanInTheLoop": "Enabled", + "tools": [ + {"toolId": deployed_id, "confirmation": "Enabled"}, + {"toolId": other_id, "confirmation": "Enabled"}, + ], + }, + }), encoding="utf-8") + context = { + "copy": copy, "yaml": yaml, "re": re, "cfg": {"apiVersion": "2026-06-01"}, + "plan": {"tools": [{"agent": "Example", "toolResourceId": deployed_id}]}, + } + exec(compile(script, str(STARTER), "exec"), context) + ext = context["patch_agent_doc"]({"name": "Example", "agentYaml": str(path)})["discoveryExtensions"] + self.assertEqual(ext["humanInTheLoop"], "Disabled") + self.assertEqual([x["toolId"] for x in ext["tools"]], [deployed_id, other_id]) + self.assertTrue(all(x["confirmation"] == "Disabled" for x in ext["tools"])) + + def test_starter_upsert_payload_retains_disabled_settings(self): + functions = "def tool_leaf(" + STARTER_SOURCE.split( + "def tool_leaf(", 1, + )[1].split("rows = []", 1)[0] + tool_id = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Discovery/tools/test" + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "agent.yaml" + path.write_text(yaml.safe_dump({ + "name": "Example", + "discoveryExtensions": { + "humanInTheLoop": "Enabled", + "tools": [{"toolId": tool_id, "confirmation": "Enabled"}], + }, + }), encoding="utf-8") + response = Mock(status_code=200, headers={"content-type": "application/json"}) + response.json.return_value = {} + requests = Mock() + requests.post.return_value = response + context = { + "copy": copy, "yaml": yaml, "re": re, "os": os, "json": json, + "requests": requests, "token": lambda: "token", + "prompt_definition": lambda doc: {"kind": "prompt", "model": "model"}, + "cfg": {"apiVersion": "2026-06-01", "workspaceEndpoint": "https://example.test", "project": "test"}, + "plan": {"outputDir": directory, "tools": [{"agent": "Example", "toolResourceId": tool_id}]}, + } + exec(compile(functions, str(STARTER), "exec"), context) + context["deploy_agent"]({"name": "Example", "agentYaml": str(path)}) + payload = requests.post.call_args.kwargs["json"] + self.assertEqual(payload["humanInTheLoop"], "Disabled") + self.assertEqual(payload["tools"], [{"toolId": tool_id, "confirmation": "Disabled"}]) + + +if __name__ == "__main__": + unittest.main() diff --git a/agents/cp2k/README.md b/agents/cp2k/README.md index 953c838a..60080969 100644 --- a/agents/cp2k/README.md +++ b/agents/cp2k/README.md @@ -24,6 +24,8 @@ CP2K is an open-source atomistic simulation package for DFT (GPW/GAPW), ab initi - **Compressed size**: ~803 MB - **Base**: `mambaorg/micromamba:1.5-jammy` (2-stage build) - **CP2K**: Source-built with DFT-D4 support (`cp2k.psmp` MPI+OMP, `cp2k.ssmp` OMP-only fallback) +- **Build tools**: The builder installs `binutils` to provide the `ar` and `ranlib` utilities required by CMake for DBCSR and CP2K static libraries. +- **LibXSMM**: Pinned to `1.17`, which supplies the `libxsmmext` and `libxsmmnoblas` components required by CP2K 2024.2. Its pkg-config files are discovered directly under the conda environment's `lib/` directory. - **Hybrid DFT**: still blocked in `cp2k_utils.py` pending stable libint/HFX validation - **Python**: 3.11 with numpy, scipy, matplotlib, pandas, ASE, pymatgen, cp2k-input-tools, MDAnalysis diff --git a/agents/cp2k/tools/cp2k/Dockerfile b/agents/cp2k/tools/cp2k/Dockerfile index 08b5cfd6..1e9bcba8 100644 --- a/agents/cp2k/tools/cp2k/Dockerfile +++ b/agents/cp2k/tools/cp2k/Dockerfile @@ -34,9 +34,10 @@ ENV DEBIAN_FRONTEND=noninteractive ENV MAMBA_ROOT_PREFIX=/opt/conda ENV PATH="${MAMBA_ROOT_PREFIX}/bin:${PATH}" -# System tools only (NOT compilers — those come from conda for ABI compatibility) +# System tools only (NOT compilers — those come from conda for ABI compatibility). +# binutils provides ar/ranlib for CMake when using MPI compiler wrappers. RUN apt-get update && apt-get install -y --no-install-recommends \ - wget git ca-certificates openssh-client \ + wget git ca-certificates openssh-client binutils \ && rm -rf /var/lib/apt/lists/* # Create conda env with compilers + build tools + all CP2K dependencies. @@ -48,7 +49,7 @@ RUN micromamba create -n cp2kenv -c conda-forge --quiet -y \ cmake ninja pkg-config \ compilers \ openmpi \ - "libxc>=6,<7" fftw openblas scalapack libxsmm \ + "libxc>=6,<7" fftw openblas scalapack "libxsmm=1.17" \ "dftd4>=3.0,<4" \ "libint>=2.6" \ numpy scipy matplotlib-base pandas ase pymatgen \ @@ -65,8 +66,8 @@ RUN micromamba create -n cp2kenv -c conda-forge --quiet -y \ ENV CE=/opt/conda/envs/cp2kenv ENV PATH="${CE}/bin:${PATH}" ENV LD_LIBRARY_PATH="${CE}/lib" -# CRITICAL: pkg-config must find .pc files from conda packages (dftd4, scalapack, libxc, etc.) -ENV PKG_CONFIG_PATH="${CE}/lib/pkgconfig:${CE}/share/pkgconfig" +# LibXSMM 1.17 provides CP2K 2024.2's required extensions and ships .pc files in lib/. +ENV PKG_CONFIG_PATH="${CE}/lib/pkgconfig:${CE}/share/pkgconfig:${CE}/lib" # --------------------------------------------------------------------------- # libint2 pkg-config shim: conda-forge "libint" package provides the library