<?xml version="1.0"?>
<rss version="2.0"><channel><title>Articles</title><link>https://community.spotfire.com/articles/</link><description/><language>en</language><item><title>Setup and Deployment Guide for Agent Registry Container (1)</title><link>https://community.spotfire.com/articles/spotfire/setup-and-deployment-guide-for-agent-registry-container1/</link><description><![CDATA[<h1><strong>Spotfire Copilot<span class="ipsEmoji">™</span> — Agent Registry Installation Guide</strong></h1><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Version:</strong> <code>1.1.0</code>  |  <strong>Last updated:</strong> 19 June 2026  |  <strong>Applies to:</strong> Agent Registry Container</p><p>The <strong>Agent Registry</strong> is a Docker container that hosts one or more A2A (Agent-to-Agent) agents on a single port and exposes them for registration with the Spotfire Copilot<span class="ipsEmoji">™</span> orchestrator. In development mode it also enables optional tools (MCP server, WebSocket reverse tunnel, type-stub generator) for building custom agents locally.</p><p>This guide covers two audiences:</p><ul><li><p><strong>Spotfire administrators</strong> deploying the Agent Registry to a cloud platform (Azure, GCP, AWS, or Kubernetes) or on-premise so that agents are reachable from a deployed orchestrator.</p></li><li><p><strong>Spotfire developers</strong> running the Agent Registry locally on their workstation to build, test, and tunnel custom agents into a deployed orchestrator without opening any inbound ports.</p></li></ul><p>The Agent Registry depends on a deployed Spotfire Copilot<span class="ipsEmoji">™</span> orchestrator. If you have not yet installed the orchestrator, see the <a rel="" href="https://community.spotfire.com/articles/spotfire/installation-guide-spotfire-copilot/">Spotfire Copilot<span class="ipsEmoji">™</span> Installation Guide (Backend)</a> first.</p></div></blockquote><hr><h2><strong>What's New in 1.1</strong></h2><ul><li><p><strong>MCP development server now requires VS Code 1.109 or later.</strong> The MCP Apps UI surface (design form, welcome screens, interactive components) was updated for the stable MCP Apps protocol shipped in VS Code 1.109. Earlier VS Code builds will not render the rich UI; upgrade VS Code before running <code>MCP_ENABLED=true</code>. See section 7, <em>Enabling the MCP Development Server</em>.</p></li><li><p><strong>Toolkit marking fixes.</strong> Reworked the <code>mark_rows_*</code> operations and helpers for reliable round-tripping with Spotfire:</p><ul><li><p><code>mark_rows_ops_from_tuples</code> is now the single supported public API for batched marking and emits one op per batch with a fixed batch size of 100 rows.</p></li><li><p>The lower-level helper is now private; a new <code>MarkingBatchError</code> surfaces partial failures instead of silently dropping rows.</p></li><li><p>Updated scaffolding templates and the <code>viz-data-shaping</code> skill to recommend the new API.</p></li></ul></li><li><p><strong>Agent stability fixes.</strong></p><ul><li><p>The registry no longer silently demotes a failing code agent to a no-code stub — failures are reported and the agent is taken out of rotation.</p></li><li><p>Remove → re-add cycles on the in-process file watcher no longer leave stale Starlette mounts or executors behind.</p></li><li><p>A new in-process file watcher hot-reloads custom workflows without dropping the MCP connection.</p></li></ul></li><li><p><strong>Tracing and monitoring improvements.</strong></p><ul><li><p>Per-conversation log files now include an executor diagnostic block on every turn, making it easier to correlate orchestrator requests with stage execution.</p></li><li><p>The dry-run harness (<code>dry_run_agent</code>) prepends an unmissable PASS / PASS-PARTIAL / FAIL banner above its JSON report.</p></li></ul></li><li><p><strong>Spotfire marking topology guidance</strong> added to the <code>viz-data-shaping</code> and <code>marked-data</code> skills, covering semantic column mapping and viz property paths.</p></li></ul><hr><h2><strong>1. Introduction</strong></h2><h3><strong>1.1 What You Are Deploying</strong></h3><p>The Agent Registry is a <strong>single Docker container</strong> that serves one or more Spotfire Copilot<span class="ipsEmoji">™</span> agents over the <a rel="external nofollow" href="https://github.com/a2aproject/a2a-samples">A2A protocol</a>. A single image plays two roles:</p><ul><li><p><strong>In production</strong>, it hosts a fixed set of bundled agents plus any custom agents mounted at <code>CUSTOM_WORKFLOWS_DIR</code>, behind OAuth2 bearer-token authentication. A deployed orchestrator calls it directly over HTTP after an administrator registers each agent in the orchestrator's admin console.</p></li><li><p><strong>In development</strong>, the same image runs on a developer's laptop with development features enabled. Hot-reload, type-stub generation, the MCP development server, and a WebSocket reverse tunnel into a deployed orchestrator allow agent code to be written, validated, and tested end-to-end against the orchestrator without opening any inbound ports.</p></li></ul><p>The container provides:</p><ul><li><p><strong>Multi-agent hosting</strong> — each agent is served at <code>/agents/&lt;slug&gt;/</code> and auto-publishes its agent card at <code>&lt;endpoint&gt;/.well-known/agent-card.json</code></p></li><li><p><strong>OAuth2 client-credentials authentication</strong> — a <code>/token</code> endpoint issues short-lived JWTs that protect every agent endpoint</p></li><li><p><strong>Auto-discovery</strong> — bundled agents and any custom agents mounted at <code>CUSTOM_WORKFLOWS_DIR</code> are discovered at startup and on file change</p></li><li><p><strong>Orchestrator relay</strong> — outbound LLM calls and RAG retrieval are proxied to the orchestrator using a single configured OAuth2 client, so agents do not hold their own LLM credentials</p></li><li><p><strong>WebSocket reverse tunnel</strong> <em>(development only)</em> — registers locally-running agents with a deployed orchestrator without inbound ports</p></li><li><p><strong>MCP development server</strong> <em>(development only)</em> — exposes scaffolding, dry-run, and read-skill tools to AI coding assistants over the Model Context Protocol</p></li><li><p><strong>Shared visualization capabilities</strong> — well-log, standard, and advanced visualization helpers are available to every agent</p></li></ul><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>The MCP server and tunnel are development-only features.</strong> Both are disabled unless you explicitly opt in, and must remain disabled in production deployments. See section 11, <em>Security Best Practices</em>.</p></div></blockquote><h3><strong>1.2 Architecture Overview</strong></h3><p>The Agent Registry runs as <strong>one container</strong> from a single Docker image. There is no database to provision and no separate admin console — registration state lives in the orchestrator.</p><div class="ipsRichText__table-wrapper"><table style="min-width: 100px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Container</p></th><th colspan="1" rowspan="1"><p>Default Port</p></th><th colspan="1" rowspan="1"><p>Image (<code>1.1.0</code>)</p></th><th colspan="1" rowspan="1"><p>Required?</p></th><th colspan="1" rowspan="1"><p>Role</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Agent Registry</strong></p></td><td colspan="1" rowspan="1"><p><code>8050</code></p></td><td colspan="1" rowspan="1"><p><code>copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0</code></p></td><td colspan="1" rowspan="1"><p><strong>Yes</strong></p></td><td colspan="1" rowspan="1"><p>Hosts agents, issues bearer tokens, relays LLM calls to the orchestrator</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Orchestrator</strong></p></td><td colspan="1" rowspan="1"><p><code>8080</code></p></td><td colspan="1" rowspan="1"><p><code>copilotoci.azurecr.io/spotfirecopilot/llm-orchestrator:2.3.4</code></p></td><td colspan="1" rowspan="1"><p><strong>Yes</strong> <em>(separate install)</em></p></td><td colspan="1" rowspan="1"><p>LLM orchestration, thread management, agent registration. Compatible with orchestrator <code>2.3.1</code> and later; <code>2.3.4</code> is the latest.</p></td></tr></tbody></table></div><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>One image, two deployment shapes.</strong> The same image binary serves production and development. Production deployments run the image with its default startup command and minimal environment. Local development sets <code>MCP_ENABLED=true</code> and <code>TUNNEL_ENABLED=true</code> and bind-mounts a custom-agents directory; nothing about the image itself changes.</p></div></blockquote><p><img class="ipsImage ipsRichText__align--block" data-fileid="30389" src="//media.invisioncic.com/h329577/monthly_2026_06/image.png.68e4afa75efa9353e6f41aff2fcc45a9.png" alt="image.png.68e4afa75efa9353e6f41aff2fcc45a9.png" title="image.png.68e4afa75efa9353e6f41aff2fcc45a9.png" width="888" height="301" loading="lazy"></p><p><img class="ipsImage ipsImage_thumbnailed ipsRichText__align--block" data-fileid="30390" src="//media.invisioncic.com/h329577/monthly_2026_06/image.thumb.png.e20d55b7c318934bc8476f0a786ac2b0.png" alt="image.thumb.png.e20d55b7c318934bc8476f0a786ac2b0.png" title="image.thumb.png.e20d55b7c318934bc8476f0a786ac2b0.png" width="1000" height="200" data-full-image="//media.invisioncic.com/h329577/monthly_2026_06/image.png.3277ed53738f5d07a77a1f7adaba66ef.png" loading="lazy"></p><p><strong>Production (HTTP Direct):</strong> The orchestrator makes direct HTTP calls to the Agent Registry. Requires inbound firewall rules to allow the orchestrator to reach the registry's <code>BASE_URL</code>.</p><p><strong>Development (WebSocket Tunnel):</strong> A local Agent Registry opens an outbound WebSocket to the orchestrator's <code>/tunnel/connect</code> endpoint and registers its agents. The orchestrator proxies requests back over the same tunnel. No inbound ports or firewall changes required — the connection is always initiated by the registry. Tunneled agents are scoped to the developer's Spotfire user ID and appear only in that user's Copilot session.</p><p>Both modes use the <strong>same OAuth2 client credentials</strong> to authenticate the Agent Registry to the orchestrator. The tunnel requires that client to hold the <code>agents:write</code> scope (granted automatically by the <code>agent_developer</code> scope profile — see <em>Orchestrator OAuth2 Client</em> in section 3).</p><h3><strong>Health Check Endpoints</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong><span class="ipsEmoji" title="">⚠️</span> The Agent Registry exposes two distinct health endpoints.</strong> Using the wrong one in your platform's probe configuration will cause restart loops.</p></div></blockquote><div class="ipsRichText__table-wrapper"><table style="min-width: 120px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Container</p></th><th colspan="1" rowspan="1"><p>Liveness Path</p></th><th colspan="1" rowspan="1"><p>Readiness Path</p></th><th colspan="1" rowspan="1"><p>Port</p></th><th colspan="1" rowspan="1"><p>Method</p></th><th colspan="1" rowspan="1"><p>Expected Response</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Agent Registry</strong></p></td><td colspan="1" rowspan="1"><p><code>/healthz</code></p></td><td colspan="1" rowspan="1"><p><code>/readyz</code></p></td><td colspan="1" rowspan="1"><p>8050</p></td><td colspan="1" rowspan="1"><p><code>GET</code></p></td><td colspan="1" rowspan="1"><p><code>200 OK</code> with <code>{"status":"ok"}</code></p></td></tr></tbody></table></div><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Liveness — is the process up?
curl -f http://localhost:8050/healthz

# Readiness — are agents discovered and ready?
curl -f http://localhost:8050/readyz
</code></pre><p><strong>Platform-specific configuration:</strong></p><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Platform</p></th><th colspan="1" rowspan="1"><p>Liveness</p></th><th colspan="1" rowspan="1"><p>Readiness</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Docker Compose</strong></p></td><td colspan="1" rowspan="1"><p><code>curl -f http://localhost:8050/healthz</code></p></td><td colspan="1" rowspan="1"><p><code>curl -f http://localhost:8050/readyz</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Kubernetes</strong></p></td><td colspan="1" rowspan="1"><p><code>httpGet</code> → path: <code>/healthz</code>, port: <code>8050</code></p></td><td colspan="1" rowspan="1"><p><code>httpGet</code> → path: <code>/readyz</code>, port: <code>8050</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>AWS ECS</strong></p></td><td colspan="1" rowspan="1"><p><code>CMD-SHELL curl -f http://localhost:8050/healthz || exit 1</code></p></td><td colspan="1" rowspan="1"><p>n/a (use the same)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Azure Container Apps</strong></p></td><td colspan="1" rowspan="1"><p>Liveness probe → path: <code>/healthz</code>, port: <code>8050</code></p></td><td colspan="1" rowspan="1"><p>Startup probe → path: <code>/readyz</code>, port: <code>8050</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>GCP Cloud Run</strong></p></td><td colspan="1" rowspan="1"><p>Startup probe → path: <code>/readyz</code>, port: <code>8050</code></p></td><td colspan="1" rowspan="1"><p>Liveness probe → path: <code>/healthz</code>, port: <code>8050</code></p></td></tr></tbody></table></div><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Do not point health probes at </strong><code>/</code><strong> or at any </strong><code>/agents/...</code><strong> path.</strong> The root path returns 404; the agent endpoints require a bearer token and will return 401. Either will be treated as a failure and the container will be restarted in a loop.</p></div></blockquote><h3><strong>Pulling the Docker Image</strong></h3><p>The Agent Registry is distributed exclusively via the <strong>Azure Container Registry</strong> at <code>copilotoci.azurecr.io</code>. There is no public ECR mirror — credentials are required for every environment that pulls the image.</p><div class="ipsRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Distribution</p></th><th colspan="1" rowspan="1"><p>Registry</p></th><th colspan="1" rowspan="1"><p>Authentication</p></th><th colspan="1" rowspan="1"><p>Reference</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>All versions</strong></p></td><td colspan="1" rowspan="1"><p><code>copilotoci.azurecr.io/spotfirecopilot/</code> (Azure Container Registry)</p></td><td colspan="1" rowspan="1"><p><strong>Required</strong> — credentials issued by Spotfire Support</p></td><td colspan="1" rowspan="1"><p><a rel="" href="https://community.spotfire.com/articles/spotfire/oci-registry-access-guide/">OCI Registry Access Guide</a></p></td></tr></tbody></table></div><h4><strong>Obtain registry credentials and pull</strong></h4><ol><li><p><strong>Request access.</strong> Open a case via the <a rel="external nofollow" href="https://support.tibco.com/">Spotfire Support Portal</a> selecting <strong>Spotfire Enterprise</strong> as the Product and request access to the Spotfire Copilot OCI registry. Support will issue a registry username and password/token. The same credentials grant access to the orchestrator image, the Agent Registry image, and any data-loader images.</p></li><li><p><strong>Log in to the registry</strong> on every host that will pull the image (developer workstations, CI runners, and production hosts):</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>docker login copilotoci.azurecr.io
# Helm-based deployments:
helm registry login copilotoci.azurecr.io
</code></pre></li><li><p><strong>Pull the image:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>docker pull copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0
</code></pre></li></ol><p>For login command details, version policy, troubleshooting registry auth errors, and the complete artifact catalog, consult the <a rel="" href="https://community.spotfire.com/articles/spotfire/oci-registry-access-guide/">OCI Registry Access Guide</a>.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Cloud and Kubernetes deployments:</strong> create an image-pull secret using your registry credentials and reference it from your deployment manifest. For example, on Kubernetes:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>kubectl create secret docker-registry copilotoci-pull \
  --docker-server=copilotoci.azurecr.io \
  --docker-username='&lt;registry-username&gt;' \
  --docker-password='&lt;registry-password-or-token&gt;'</code></pre><p>Then add <code>imagePullSecrets: [{ name: copilotoci-pull }]</code> to each deployment that pulls from <code>copilotoci.azurecr.io</code>. AWS ECS, Azure Container Apps, and GCP Cloud Run have equivalent registry-credential mechanisms — see the platform-specific subsections in section 5.</p></div></blockquote><h4><strong>Air-gapped / private registry mirroring</strong></h4><p>If your deployment environment cannot reach the upstream registry, pull on an internet-connected machine, retag, and push to your internal registry:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>docker login copilotoci.azurecr.io
docker pull copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0
docker tag  copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0 \
            your-registry.example.com/agent-container:1.1.0
docker push your-registry.example.com/agent-container:1.1.0
</code></pre><p>Update all deployment manifests and compose files to reference your internal registry URL.</p><h3><strong>1.3 Prerequisites</strong></h3><p>Before you begin, ensure you have:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 40px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Requirement</p></th><th colspan="1" rowspan="1"><p>Details</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Spotfire Copilot<span class="ipsEmoji">™</span> orchestrator</strong></p></td><td colspan="1" rowspan="1"><p>A reachable orchestrator at <code>http://&lt;host&gt;:8080</code> (or your TLS endpoint). The Agent Registry cannot operate without one.</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>OCI registry credentials</strong></p></td><td colspan="1" rowspan="1"><p>Issued by Spotfire Support — see <em>Pulling the Docker Image</em> in section 1.</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Cloud platform account</strong> <em>(production)</em></p></td><td colspan="1" rowspan="1"><p>An active account on Azure, GCP, or AWS, plus the corresponding CLI (<code>az</code>, <code>gcloud</code>, <code>aws</code>).</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Docker</strong> <em>(local development and on-premise)</em></p></td><td colspan="1" rowspan="1"><p>Docker Engine 20.10+ with Docker Compose V2. Docker Desktop on Windows/macOS is fine.</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Network reachability</strong></p></td><td colspan="1" rowspan="1"><p>The orchestrator must be able to reach the Agent Registry on <code>BASE_URL</code> (HTTP Direct mode), <strong>or</strong> the Agent Registry must be able to open an outbound WebSocket to the orchestrator (tunnel mode).</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Python 3.11+</strong></p></td><td colspan="1" rowspan="1"><p>Needed <strong>only</strong> on the machine where you generate local container credentials (Step 1).</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>VS Code 1.109+ with GitHub Copilot</strong> <em>(developers, optional)</em></p></td><td colspan="1" rowspan="1"><p>Required to use the MCP development server. The MCP Apps UI (design form, welcome screens, interactive components) targets the stable MCP Apps protocol shipped in VS Code 1.109; older builds will not render the rich UI.</p></td></tr></tbody></table></div><h3><strong>1.4 Recommended Personnel</strong></h3><p>For <strong>production deployments</strong>, an <strong>IT / Analytics Engineer</strong> familiar with:</p><ul><li><p>Deploying containerized applications to at least one cloud platform (Azure Container Apps, GCP Cloud Run, AWS ECS/Fargate, or Kubernetes)</p></li><li><p>Managing environment variables, secrets, and image-pull credentials via the platform's native secrets store</p></li><li><p>Configuring health probes, ingress, and TLS termination</p></li><li><p>Working with OAuth2 client credentials and bearer-token authentication</p></li></ul><p>For <strong>local agent development</strong>, a <strong>Spotfire developer</strong> familiar with:</p><ul><li><p>Docker Desktop and Docker Compose</p></li><li><p>Python 3.11+ and <code>uv</code> (for the type-stubs virtual environment)</p></li><li><p>VS Code and GitHub Copilot (for the MCP development server)</p></li><li><p>The conceptual structure of an A2A agent — see <a rel="">Spotfire A2A Agents Guide</a></p></li></ul><hr><h2><strong>2. Choose Your Deployment Path</strong></h2><p>The Agent Registry supports the same set of deployment targets as the orchestrator, plus a first-class <strong>local development</strong> path. Identify your scenario first:</p><h3><strong>Where will you run the Agent Registry?</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p></p></th><th colspan="1" rowspan="1"><p>Cloud Managed Service <em>(production)</em></p></th><th colspan="1" rowspan="1"><p>On-Premise <em>(production)</em></p></th><th colspan="1" rowspan="1"><p>Local Workstation <em>(development)</em></p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>What</strong></p></td><td colspan="1" rowspan="1"><p>Azure Container Apps, GCP Cloud Run, AWS ECS/Fargate, or Kubernetes (AKS / GKE / EKS)</p></td><td colspan="1" rowspan="1"><p>Docker Compose on your own server</p></td><td colspan="1" rowspan="1"><p>Docker Desktop on a developer laptop</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Connection mode</strong></p></td><td colspan="1" rowspan="1"><p>HTTP Direct (orchestrator → registry)</p></td><td colspan="1" rowspan="1"><p>HTTP Direct (orchestrator → registry)</p></td><td colspan="1" rowspan="1"><p>WebSocket tunnel (registry → orchestrator)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Inbound ports needed</strong></p></td><td colspan="1" rowspan="1"><p>Yes — orchestrator must reach <code>BASE_URL</code></p></td><td colspan="1" rowspan="1"><p>Yes — orchestrator must reach <code>BASE_URL</code></p></td><td colspan="1" rowspan="1"><p><strong>No</strong> — outbound only</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Hot-reload</strong></p></td><td colspan="1" rowspan="1"><p>No</p></td><td colspan="1" rowspan="1"><p>No</p></td><td colspan="1" rowspan="1"><p>Yes (custom-agents directory bind-mounted)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>MCP server</strong></p></td><td colspan="1" rowspan="1"><p><strong>Disabled</strong> (security)</p></td><td colspan="1" rowspan="1"><p><strong>Disabled</strong> (security)</p></td><td colspan="1" rowspan="1"><p>Optional, controlled by <code>MCP_ENABLED=true</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Custom agents</strong></p></td><td colspan="1" rowspan="1"><p>Mounted via cloud volume at <code>CUSTOM_WORKFLOWS_DIR</code></p></td><td colspan="1" rowspan="1"><p>Mounted from a host directory</p></td><td colspan="1" rowspan="1"><p>Mounted from a host directory (typically your editor workspace)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Audience</strong></p></td><td colspan="1" rowspan="1"><p>Spotfire administrators</p></td><td colspan="1" rowspan="1"><p>Spotfire administrators</p></td><td colspan="1" rowspan="1"><p>Spotfire developers</p></td></tr></tbody></table></div><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>All paths share Steps 1–2.</strong> Credentials and orchestrator wiring work the same way. The differences begin in Step 3, where the deployment mechanism and runtime profile diverge.</p></div></blockquote><hr><h2><strong>3. Step 1 — Generate Credentials</strong></h2><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Applies to:</strong> All deployments.</p></div></blockquote><p>The Agent Registry uses two independent sets of OAuth2 client credentials:</p><ol><li><p><strong>Local container credentials</strong> (<code>AUTH_*</code>) — used by Spotfire clients and the orchestrator to authenticate against the Agent Registry's own <code>/token</code> endpoint when calling its agents.</p></li><li><p><strong>Orchestrator OAuth2 client</strong> (<code>ORCHESTRATOR_*</code>) — used by the Agent Registry to authenticate <strong>outbound</strong> calls to the orchestrator (LLM relay, RAG, agent registration, and the tunnel).</p></li></ol><p>Generate both before deploying.</p><h3><strong>Local Container Credentials</strong></h3><p>The Agent Registry itself does not ship a credential generator — the same <code>generate_credentials.py</code> helper that produces orchestrator credentials, distributed with <strong>Spotfire Copilot<span class="ipsEmoji">™</span></strong>, also produces all three values needed here. Run it once and copy the outputs into your Agent Registry environment:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Run the helper from your Spotfire Copilot install
python generate_credentials.py
</code></pre><p>This outputs three values for the Agent Registry:</p><ol><li><p><code>AUTH_CLIENT_ID</code> — a short, opaque client identifier</p></li><li><p><code>AUTH_CLIENT_SECRET</code> — a randomly generated secret (shown only once)</p></li><li><p><code>AUTH_SIGNING_KEY</code> — a URL-safe random key used to sign issued JWTs</p></li></ol><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Save the plaintext secret immediately</strong> — <code>AUTH_CLIENT_SECRET</code> is shown only once. For on-premise and local development, paste it into your <code>.env</code> file. For cloud deployments, store it in your platform's secrets manager (Azure Key Vault, GCP Secret Manager, AWS Secrets Manager, or Kubernetes Secrets) and reference it from your deployment manifest.</p></div></blockquote><p>If you do not have access to the helper, generate the three values manually with any Python 3.11+ interpreter:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># AUTH_CLIENT_ID — any short identifierecho "agent-registry-$(date +%s)"

# AUTH_CLIENT_SECRET — a 32-byte URL-safe random string
python -c "import secrets; print(secrets.token_urlsafe(32))"

# AUTH_SIGNING_KEY — a separate 32-byte URL-safe random string
python -c "import secrets; print(secrets.token_urlsafe(32))"</code></pre><h3><strong>Orchestrator OAuth2 Client</strong></h3><p>Register an OAuth2 client in the orchestrator using the <code>agent_developer</code> scope profile. This profile grants exactly the scopes the Agent Registry needs and nothing more:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 40px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Scope</p></th><th colspan="1" rowspan="1"><p>Purpose</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>api_access</code></p></td><td colspan="1" rowspan="1"><p>Base API access (required for all operations)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>rag</code></p></td><td colspan="1" rowspan="1"><p>Retrieval-augmented generation / vector search</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>agents</code></p></td><td colspan="1" rowspan="1"><p>Read agent metadata and cards</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>agents:write</code></p></td><td colspan="1" rowspan="1"><p>Register and update agents (<strong>required for the tunnel</strong>)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>store</code></p></td><td colspan="1" rowspan="1"><p>Document store operations</p></td></tr></tbody></table></div><h4><strong>Option A — Admin Console (recommended)</strong></h4><ol><li><p>Open the orchestrator's admin console (e.g., <code>http://localhost:8080/admin</code>).</p></li><li><p>Navigate to the <strong>OAuth Clients</strong> tab.</p></li><li><p>Click <strong>Create Client</strong> and fill in:</p><ul><li><p><strong>Client Name:</strong> a descriptive label such as <code>agent-registry-prod</code> or <code>agent-registry-dev-&lt;your-name&gt;</code></p></li><li><p><strong>Scope Profile:</strong> <code>agent_developer</code></p></li></ul></li><li><p>Click <strong>Create</strong>. The console displays the generated <code>client_id</code> and <code>client_secret</code>.</p></li></ol><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>The </strong><code>client_secret</code><strong> is shown only once.</strong> Copy both values immediately. Use them as <code>ORCHESTRATOR_CLIENT_ID</code> and <code>ORCHESTRATOR_CLIENT_SECRET</code>.</p></div></blockquote><h4><strong>Option B — REST API</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>curl -X POST https://your-orchestrator.example.com/register_client \
  -H "Authorization: Bearer &lt;admin_token&gt;" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_name=Agent Registry&amp;scope_profile=agent_developer"</code></pre><p>Response:</p><pre spellcheck="" class="ipsCode language-json" data-language="JSON"><code>{"client_id": "abc123...","client_secret": "s3cr3t..."}</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p>If you must pass scopes explicitly (no profile), use: <code>scopes=api_access rag agents agents:write store</code> (space-separated).</p></div></blockquote><hr><h2><strong>4. Step 2 — Configure the Orchestrator Connection</strong></h2><p>Pick one of two connection modes based on where the Agent Registry will run.</p><h3><strong>HTTP Direct (Production)</strong></h3><p>The orchestrator opens HTTP connections to the Agent Registry on its configured <code>BASE_URL</code>. This is the mode used by every cloud and on-premise production deployment.</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># .env  (or your platform's secret store)
ORCHESTRATOR_URL=https://orchestrator.example.com
ORCHESTRATOR_CLIENT_ID=&lt;from Step 1 — Orchestrator OAuth2 Client&gt;
ORCHESTRATOR_CLIENT_SECRET=&lt;from Step 1&gt;

BASE_URL=https://agents.example.com   # how the orchestrator reaches *this* container
TUNNEL_ENABLED=false                  # explicitly disabled in production</code></pre><p>Once the container is running, an administrator registers each agent in the orchestrator admin console at the URL <code>${BASE_URL}/agents/&lt;slug&gt;/</code> — see section 6, <em>Step 4 — Verify and Register Agents</em>.</p><h3><strong>WebSocket Tunnel (Local Development)</strong></h3><p>The Agent Registry opens an outbound WebSocket to the orchestrator's <code>/tunnel/connect</code> endpoint, authenticates using the same <code>ORCHESTRATOR_*</code> credentials, and registers each discovered agent as a tunneled agent scoped to a specific Spotfire user. Inbound A2A requests are proxied back over the tunnel.</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># .env  (developer workstation)
ORCHESTRATOR_URL=https://orchestrator.example.com
ORCHESTRATOR_CLIENT_ID=&lt;from Step 1 — Orchestrator OAuth2 Client&gt;
ORCHESTRATOR_CLIENT_SECRET=&lt;from Step 1&gt;

TUNNEL_ENABLED=true
TUNNEL_USER_ID=alice.smith@company.com   # MUST exactly match your Spotfire login
MCP_ENABLED=true                         # enable the MCP development server</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong><span class="ipsEmoji" title="">⚠️</span> </strong><code>TUNNEL_USER_ID</code><strong> must exactly match the username of your active Spotfire session.</strong> The orchestrator routes inbound A2A requests to a tunnel based on this value: if Spotfire is logged in as <code>alice.smith@company.com</code> and the tunnel announces itself as <code>Alice.Smith@COMPANY.COM</code>, the tunneled agents will not appear in that Spotfire session. The match is case-sensitive and includes any domain/realm portion. If your tunneled agents are not visible in Spotfire Copilot, this is almost always the cause.</p></div></blockquote><p>Requirements:</p><ul><li><p>The orchestrator must have the <code>/tunnel/connect</code> endpoint enabled (<code>TUNNEL_ENABLED=true</code> on the orchestrator side — see the <a rel="" href="https://community.spotfire.com/articles/spotfire/installation-guide-spotfire-copilot/">Spotfire Copilot<span class="ipsEmoji">™</span> Installation Guide</a> for the corresponding orchestrator-side variables).</p></li><li><p>The <code>ORCHESTRATOR_CLIENT_*</code> credentials must hold the <code>agents:write</code> scope. The <code>agent_developer</code> profile grants this automatically.</p></li><li><p>Tunneled agents are visible <strong>only</strong> to the Spotfire user whose login matches <code>TUNNEL_USER_ID</code>. Other users on the same orchestrator will not see them, even when the tunnel is live.</p></li></ul><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Production deployments must set </strong><code>TUNNEL_ENABLED=false</code> (or omit the variable). The cloud and on-premise deployment examples in §5 do not set it, so it is off by default in production.</p></div></blockquote><hr><h2><strong>5. Step 3 — Deploy</strong></h2><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Image references below pin version </strong><code>1.1.0</code><strong>.</strong> All snippets pull from <code>copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0</code>. You must already have logged in to that registry with credentials issued by Spotfire Support — see <em>Pulling the Docker Image</em> in section 1 and the <a rel="" href="https://community.spotfire.com/articles/spotfire/oci-registry-access-guide/">OCI Registry Access Guide</a>.</p></div></blockquote><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Key pattern for all platforms:</strong> You deploy the <strong>same Docker image</strong> in every environment. The differences are the environment variables (<code>MCP_ENABLED</code>/<code>TUNNEL_ENABLED</code> for development), the volumes (mounting your custom-agents directory at <code>CUSTOM_WORKFLOWS_DIR</code>), and how you supply secrets.</p></div></blockquote><h3><strong>Managing Environment Variables in the Cloud</strong></h3><p>In cloud deployments, you <strong>do not use </strong><code>.env</code><strong> files</strong>. Each cloud platform has its own mechanism for injecting environment variables and secrets into containers:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Platform</p></th><th colspan="1" rowspan="1"><p>Environment Variables</p></th><th colspan="1" rowspan="1"><p>Secrets (auth keys, OAuth2 client secrets)</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Azure Container Apps</strong></p></td><td colspan="1" rowspan="1"><p>Container App → <strong>Settings → Environment variables</strong> (portal), or <code>--set-env-vars</code> (CLI)</p></td><td colspan="1" rowspan="1"><p>Azure Key Vault references: <code>secretref:&lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>GCP Cloud Run</strong></p></td><td colspan="1" rowspan="1"><p><code>--set-env-vars</code> flag, or Cloud Run → <strong>Variables &amp; Secrets</strong> tab (console)</p></td><td colspan="1" rowspan="1"><p>GCP Secret Manager: <code>--set-secrets MY_KEY=my-secret:latest</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>AWS ECS</strong></p></td><td colspan="1" rowspan="1"><p>Task Definition → <code>environment</code> block (JSON), or <code>--environment</code> (CLI)</p></td><td colspan="1" rowspan="1"><p>AWS Secrets Manager / SSM Parameter Store: <code>valueFrom</code> in the task definition</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Kubernetes</strong></p></td><td colspan="1" rowspan="1"><p><code>ConfigMap</code> mounted as <code>envFrom</code>, or inline <code>env</code> in the Pod spec</p></td><td colspan="1" rowspan="1"><p><code>Secret</code> resources mounted as <code>envFrom</code> or individual <code>valueFrom</code> references</p></td></tr></tbody></table></div><h4><strong>Best practices</strong></h4><ul><li><p><strong>Never embed secrets in container images or source control.</strong> Use your platform's secrets manager.</p></li><li><p><strong>Separate secrets from configuration.</strong> Use a <code>Secret</code> (or Key Vault / Secrets Manager) for <code>AUTH_CLIENT_SECRET</code>, <code>AUTH_SIGNING_KEY</code>, and <code>ORCHESTRATOR_CLIENT_SECRET</code>. Use plain environment variables for non-sensitive settings like <code>LOG_LEVEL</code>, <code>BASE_URL</code>, and <code>ORCHESTRATOR_URL</code>.</p></li><li><p><code>BASE_URL</code><strong> must match how the orchestrator reaches the registry.</strong> Behind a reverse proxy or ingress, set this to the externally-visible URL (e.g., <code>https://agents.example.com</code>). It is used to build the URLs published in each agent's card.</p></li><li><p><strong>Bcrypt-style </strong><code>$</code><strong> characters do not appear in Agent Registry credentials.</strong> Unlike the orchestrator's <code>HASHED_ADMIN_PASSWORD</code>, none of the Agent Registry's required values contain <code>$</code> interpolation hazards. Standard secret storage is sufficient on every platform.</p></li></ul><h4><strong>Required environment variables (all platforms)</strong></h4><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Required</p></th><th colspan="1" rowspan="1"><p>Notes</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_CLIENT_ID</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>From section 3, <em>Local Container Credentials</em>.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_CLIENT_SECRET</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>Store as a secret.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_SIGNING_KEY</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>Store as a secret.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_URL</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>HTTPS URL of the orchestrator.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_CLIENT_ID</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>From section 3, <em>Orchestrator OAuth2 Client</em>.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_CLIENT_SECRET</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>Store as a secret.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BASE_URL</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span> <em>(production)</em></p></td><td colspan="1" rowspan="1"><p>Externally-visible URL the orchestrator uses to reach this container. Defaults to <code>http://localhost:8050</code> in development.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>PORT</code></p></td><td colspan="1" rowspan="1"><p>Optional</p></td><td colspan="1" rowspan="1"><p>Default <code>8050</code>. Override only when colocating containers.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>CUSTOM_WORKFLOWS_DIR</code></p></td><td colspan="1" rowspan="1"><p>Optional</p></td><td colspan="1" rowspan="1"><p>Container-internal path for mounted custom agents. Standard value: <code>/custom-workflows</code>.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>MCP_ENABLED</code></p></td><td colspan="1" rowspan="1"><p>Optional</p></td><td colspan="1" rowspan="1"><p><strong>Must be unset or </strong><code>false</code><strong> in production.</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>TUNNEL_ENABLED</code></p></td><td colspan="1" rowspan="1"><p>Optional</p></td><td colspan="1" rowspan="1"><p><strong>Must be unset or </strong><code>false</code><strong> in production.</strong></p></td></tr></tbody></table></div><hr><h3><strong>5.1 Local Development (Docker Desktop)</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Use this option</strong> for building, testing, and tunneling custom agents into a deployed orchestrator. Both the bundled agents and any agents in your workspace are served, with hot-reload on every save.</p></div></blockquote><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Prerequisites:</strong> Docker Desktop on Windows or macOS (or Docker Engine + Compose V2 on Linux), authenticated access to <code>copilotoci.azurecr.io</code>, and a deployed orchestrator you can reach.</p></div></blockquote><h4><strong>Prepare the workspace</strong></h4><ol><li><p>Choose a directory on your host for custom agents — this is where you will create and edit agent code:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>C:\Users\you\my_agents\
  ├── .vscode\
  │   └── mcp.json
  ├── docker-compose.yml      # created in step 2
  ├── .env                    # created in step 3
  └── (agent folders appear here as you create them)
</code></pre></li><li><p>Save the following <code>docker-compose.yml</code> next to your <code>.env</code>. It pulls the image directly — no source checkout required.</p><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>services:agent-registry:
    image: copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0
    ports:
      - "8050:8050"
    env_file: .env
    volumes:
      # Mount your custom-agents directory into the container
      - .:/custom-workflows
    restart: unless-stopped</code></pre></li><li><p>Create a <code>.env</code> file next to the compose file with the values from Steps 1 and 2:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Reach a locally-running orchestrator from inside the container
ORCHESTRATOR_URL=http://host.docker.internal:8080
ORCHESTRATOR_CLIENT_ID=&lt;from Step 1&gt;
ORCHESTRATOR_CLIENT_SECRET=&lt;from Step 1&gt;

AUTH_CLIENT_ID=&lt;from Step 1&gt;
AUTH_CLIENT_SECRET=&lt;from Step 1&gt;
AUTH_SIGNING_KEY=&lt;from Step 1&gt;

# Custom-agents directory inside the container (matches the volume mount above)
CUSTOM_WORKFLOWS_DIR=/custom-workflows

# Development features
MCP_ENABLED=true
TUNNEL_ENABLED=true
TUNNEL_USER_ID=alice.smith@company.com   # MUST exactly match your Spotfire login</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>\u26a0\ufe0f </strong><code>TUNNEL_USER_ID</code><strong> is how Spotfire Copilot routes tunneled agents to your session.</strong> It must match the username of the Spotfire user you are logged in as, exactly — including case, domain, and any realm portion. Set it wrong and your agents will not appear in Spotfire even though the tunnel is live.</p></div></blockquote></li></ol><h4><strong>Start the container</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>docker compose up -d
</code></pre><p>When the container starts it will:</p><ul><li><p>Discover every agent under your custom-agents directory and serve it at <code>/agents/&lt;slug&gt;/</code></p></li><li><p>Open an outbound WebSocket to the orchestrator and register each agent scoped to <code>TUNNEL_USER_ID</code></p></li><li><p>Watch the custom-agents directory for changes and re-expose edited agents in-process without a restart</p></li><li><p>Expose the MCP development server at <code>http://localhost:8050/mcp/</code> for use from VS Code</p></li></ul><h4><strong>Verify and use</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Liveness
curl http://localhost:8050/healthz

# Readiness (after the tunnel has connected)
curl http://localhost:8050/readyz
</code></pre><p>Open Spotfire Copilot in your browser — your tunneled agents appear under your user account, provided <code>TUNNEL_USER_ID</code> matches your Spotfire login.</p><p>For the workspace layout, type stubs, and MCP server setup, see section 7, <em>Step 5 — Set Up the Local Agent Workspace</em>.</p><hr><h3><strong>5.2 Azure Container Apps</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Prerequisites:</strong> Azure subscription, <code>az</code> CLI installed and authenticated, an Azure Container Apps Environment, and an OCI image-pull credential or registry secret for <code>copilotoci.azurecr.io</code>.</p></div></blockquote><h4><strong>Provision the environment</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>az group create --name SpotfireAgents --location eastus

az containerapp env create \
  --name agents-env \
  --resource-group SpotfireAgents \
  --location eastus
</code></pre><h4><strong>Store secrets in Azure Key Vault</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>az keyvault create --name spotfire-agents-kv \
  --resource-group SpotfireAgents --location eastus

az keyvault secret set --vault-name spotfire-agents-kv --name auth-client-secret    --value "&lt;AUTH_CLIENT_SECRET&gt;"
az keyvault secret set --vault-name spotfire-agents-kv --name auth-signing-key      --value "&lt;AUTH_SIGNING_KEY&gt;"
az keyvault secret set --vault-name spotfire-agents-kv --name orchestrator-secret   --value "&lt;ORCHESTRATOR_CLIENT_SECRET&gt;"</code></pre><h4><strong>Configure the image-pull credential</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>az containerapp env identity assign \
  --name agents-env --resource-group SpotfireAgents --system-assigned

# OR, attach OCI registry credentials directly on the container app:
az containerapp registry set \
  --name agent-registry \
  --resource-group SpotfireAgents \
  --server copilotoci.azurecr.io \
  --username '&lt;registry-username&gt;' \
  --password '&lt;registry-password-or-token&gt;'</code></pre><h4><strong>Deploy the Agent Registry</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>az containerapp create \
  --name agent-registry \
  --resource-group SpotfireAgents \
  --environment agents-env \
  --image copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0 \
  --target-port 8050 \
  --ingress external \
  --min-replicas 1 \
  --secrets \
    auth-client-secret=keyvaultref:https://spotfire-agents-kv.vault.azure.net/secrets/auth-client-secret,identityref:/subscriptions/.../managedIdentities/agents-id \
    auth-signing-key=keyvaultref:https://spotfire-agents-kv.vault.azure.net/secrets/auth-signing-key,identityref:... \
    orchestrator-secret=keyvaultref:https://spotfire-agents-kv.vault.azure.net/secrets/orchestrator-secret,identityref:... \
  --env-vars \
    AUTH_CLIENT_ID=&lt;AUTH_CLIENT_ID&gt; \
    AUTH_CLIENT_SECRET=secretref:auth-client-secret \
    AUTH_SIGNING_KEY=secretref:auth-signing-key \
    ORCHESTRATOR_URL=https://orchestrator.example.com \
    ORCHESTRATOR_CLIENT_ID=&lt;ORCHESTRATOR_CLIENT_ID&gt; \
    ORCHESTRATOR_CLIENT_SECRET=secretref:orchestrator-secret \
    BASE_URL=https://agent-registry.&lt;random&gt;.eastus.azurecontainerapps.io \
    LOG_LEVEL=INFO
</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><code>BASE_URL</code><strong> chicken-and-egg.</strong> The externally-visible URL is allocated by Azure when the Container App is created. Deploy first with a placeholder, capture the FQDN with <code>az containerapp show ... --query properties.configuration.ingress.fqdn</code>, then update <code>BASE_URL</code> to <code>https://&lt;that-fqdn&gt;</code> and revise the app.</p></div></blockquote><p>Health probe path: <code>/healthz</code> on port 8050.</p><hr><h3><strong>5.3 GCP Cloud Run</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Prerequisites:</strong> GCP project, <code>gcloud</code> CLI installed and authenticated, and GCP Secret Manager enabled.</p></div></blockquote><h4><strong>Store secrets in GCP Secret Manager</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>echo -n "&lt;AUTH_CLIENT_SECRET&gt;"        | gcloud secrets create agent-auth-client-secret  --data-file=-
echo -n "&lt;AUTH_SIGNING_KEY&gt;"          | gcloud secrets create agent-auth-signing-key    --data-file=-
echo -n "&lt;ORCHESTRATOR_CLIENT_SECRET&gt;"| gcloud secrets create agent-orchestrator-secret --data-file=-

# Grant the Cloud Run service account accessfor s in agent-auth-client-secret agent-auth-signing-key agent-orchestrator-secret ; do
  gcloud secrets add-iam-policy-binding "$s" \
    --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"done</code></pre><h4><strong>Configure the image-pull credential</strong></h4><p>Cloud Run can pull from Azure Container Registry using a service-account-mounted Docker config, or via Workload Identity Federation. The simplest approach is a per-region Artifact Registry mirror — pull the image once with your OCI credentials, push to Artifact Registry, and reference that copy.</p><h4><strong>Deploy</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>gcloud run deploy agent-registry \
  --image copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0 \
  --port 8050 \
  --region us-central1 \
  --min-instances 1 \
  --set-env-vars \
    AUTH_CLIENT_ID=&lt;AUTH_CLIENT_ID&gt;,\
    ORCHESTRATOR_URL=https://orchestrator.example.com,\
    ORCHESTRATOR_CLIENT_ID=&lt;ORCHESTRATOR_CLIENT_ID&gt;,\
    LOG_LEVEL=INFO \
  --set-secrets \
    AUTH_CLIENT_SECRET=agent-auth-client-secret:latest,\
    AUTH_SIGNING_KEY=agent-auth-signing-key:latest,\
    ORCHESTRATOR_CLIENT_SECRET=agent-orchestrator-secret:latest

# After deploy, capture the URL and update BASE_URL
URL=$(gcloud run services describe agent-registry --region us-central1 --format='value(status.url)')
gcloud run services update agent-registry --region us-central1 \
  --update-env-vars BASE_URL=$URL</code></pre><p>Startup probe path: <code>/readyz</code> on port 8050. Liveness probe path: <code>/healthz</code> on port 8050.</p><hr><h3><strong>5.4 AWS (ECS / Fargate)</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Prerequisites:</strong> AWS account, <code>aws</code> CLI installed and authenticated, an ECS cluster, AWS Secrets Manager enabled, and an ECR mirror (or AWS-managed pull secret) for the OCI image.</p></div></blockquote><h4><strong>Store secrets</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>aws secretsmanager create-secret --name agent-registry/auth-client-secret  --secret-string "&lt;AUTH_CLIENT_SECRET&gt;"
aws secretsmanager create-secret --name agent-registry/auth-signing-key    --secret-string "&lt;AUTH_SIGNING_KEY&gt;"
aws secretsmanager create-secret --name agent-registry/orchestrator-secret --secret-string "&lt;ORCHESTRATOR_CLIENT_SECRET&gt;"</code></pre><h4><strong>Task role</strong></h4><p>The Agent Registry itself does not require AWS service permissions. The task role only needs to read its secrets:</p><pre spellcheck="" class="ipsCode language-json" data-language="JSON"><code>{"Version": "2012-10-17","Statement": [
    {
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "arn:aws:secretsmanager:*:*:secret:agent-registry/*"
    }]}</code></pre><h4><strong>Task definition</strong></h4><pre spellcheck="" class="ipsCode language-json" data-language="JSON"><code>{"family": "agent-registry","networkMode": "awsvpc","requiresCompatibilities": ["FARGATE"],"cpu": "1024","memory": "2048","taskRoleArn": "arn:aws:iam::...:role/agent-registry-task","executionRoleArn": "arn:aws:iam::...:role/ecsTaskExecutionRole","containerDefinitions": [
    {
      "name": "agent-registry",
      "image": "copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0",
      "portMappings": [{ "containerPort": 8050 }],
      "environment": [
        { "name": "AUTH_CLIENT_ID",         "value": "&lt;AUTH_CLIENT_ID&gt;" },
        { "name": "ORCHESTRATOR_URL",       "value": "https://orchestrator.example.com" },
        { "name": "ORCHESTRATOR_CLIENT_ID", "value": "&lt;ORCHESTRATOR_CLIENT_ID&gt;" },
        { "name": "BASE_URL",               "value": "https://agents.example.com" },
        { "name": "LOG_LEVEL",              "value": "INFO" }
      ],
      "secrets": [
        { "name": "AUTH_CLIENT_SECRET",         "valueFrom": "arn:aws:secretsmanager:...:secret:agent-registry/auth-client-secret" },
        { "name": "AUTH_SIGNING_KEY",           "valueFrom": "arn:aws:secretsmanager:...:secret:agent-registry/auth-signing-key" },
        { "name": "ORCHESTRATOR_CLIENT_SECRET", "valueFrom": "arn:aws:secretsmanager:...:secret:agent-registry/orchestrator-secret" }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8050/healthz || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 30
      }
    }]}</code></pre><hr><h3><strong>5.5 Kubernetes</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Applies to:</strong> AKS (Azure), GKE (Google), EKS (Amazon), or any conformant Kubernetes cluster (v1.25+).</p></div></blockquote><h4><strong>Image-pull secret</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>kubectl create namespace spotfire-agents

kubectl -n spotfire-agents create secret docker-registry copilotoci-pull \
  --docker-server=copilotoci.azurecr.io \
  --docker-username='&lt;registry-username&gt;' \
  --docker-password='&lt;registry-password-or-token&gt;'</code></pre><h4><strong>Application secrets and config</strong></h4><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>apiVersion: v1kind: Secretmetadata:name: agent-registry-secretsnamespace: spotfire-agentstype: OpaquestringData:AUTH_CLIENT_SECRET: "&lt;AUTH_CLIENT_SECRET&gt;"AUTH_SIGNING_KEY: "&lt;AUTH_SIGNING_KEY&gt;"ORCHESTRATOR_CLIENT_SECRET: "&lt;ORCHESTRATOR_CLIENT_SECRET&gt;"---apiVersion: v1kind: ConfigMapmetadata:name: agent-registry-confignamespace: spotfire-agentsdata:AUTH_CLIENT_ID: "&lt;AUTH_CLIENT_ID&gt;"ORCHESTRATOR_URL: "https://orchestrator.example.com"ORCHESTRATOR_CLIENT_ID: "&lt;ORCHESTRATOR_CLIENT_ID&gt;"BASE_URL: "https://agents.example.com"LOG_LEVEL: "INFO"</code></pre><h4><strong>Deployment</strong></h4><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>apiVersion: apps/v1kind: Deploymentmetadata:name: agent-registrynamespace: spotfire-agentsspec:replicas: 2selector:
    matchLabels:
      app: agent-registrytemplate:
    metadata:
      labels:
        app: agent-registry
    spec:
      imagePullSecrets:
        - name: copilotoci-pull
      containers:
        - name: agent-registry
          image: copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0
          ports:
            - containerPort: 8050
          envFrom:
            - configMapRef:
                name: agent-registry-config
            - secretRef:
                name: agent-registry-secrets
          livenessProbe:
            httpGet: { path: /healthz, port: 8050 }
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet: { path: /readyz, port: 8050 }
            initialDelaySeconds: 10
            periodSeconds: 5
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi</code></pre><h4><strong>Service and Ingress</strong></h4><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>apiVersion: v1kind: Servicemetadata:name: agent-registrynamespace: spotfire-agentsspec:selector:
    app: agent-registryports:
    - port: 8050
      targetPort: 8050---apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: agent-registrynamespace: spotfire-agentsspec:tls:
    - hosts: [agents.example.com]
      secretName: agent-registry-tlsrules:
    - host: agents.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: agent-registry
                port:
                  number: 8050</code></pre><h4><strong>Mounting custom agents</strong></h4><p>To serve custom agents in a Kubernetes deployment, mount them at <code>CUSTOM_WORKFLOWS_DIR</code> from a <code>ConfigMap</code>, a <code>PersistentVolumeClaim</code> backed by your cloud's file storage (Azure Files, GCP Filestore, AWS EFS), or a sidecar that syncs from Git:</p><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>spec:template:
    spec:
      containers:
        - name: agent-registry
          env:
            - name: CUSTOM_WORKFLOWS_DIR
              value: /custom-workflows
          volumeMounts:
            - name: custom-agents
              mountPath: /custom-workflows
              readOnly: true
      volumes:
        - name: custom-agents
          persistentVolumeClaim:
            claimName: custom-agents-pvc</code></pre><hr><h3><strong>5.6 On-Premise (Docker Compose)</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Use this option</strong> for production deployments on a single host, or for proof-of-concept work in environments without cloud infrastructure.</p></div></blockquote><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Prerequisites:</strong> Docker Engine 20.10+ with Docker Compose V2. Outbound HTTPS to <code>copilotoci.azurecr.io</code> (or access to your air-gapped mirror).</p></div></blockquote><h4><strong>Create a deployment directory</strong></h4><p>Pick any directory on the host \u2014 for example <code>/opt/spotfire-agent-registry/</code> \u2014 and create the following two files in it:</p><p><code>docker-compose.yml</code></p><pre spellcheck="" class="ipsCode language-yaml" data-language="YAML"><code>services:agent-registry:
    image: copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0
    ports:
      - "${PORT:-8050}:8050"
    env_file: .env
    volumes:
      # Optional: mount a host directory containing additional agents.
      # Mount read-only \u2014 the container never needs to write to it.
      - /opt/spotfire-agents:/custom-workflows:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8050/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3</code></pre><p><code>.env</code> \u2014 fill in with values from Steps 1 and 2:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Local container credentials (from Step 1)
AUTH_CLIENT_ID=&lt;from Step 1&gt;
AUTH_CLIENT_SECRET=&lt;from Step 1&gt;
AUTH_SIGNING_KEY=&lt;from Step 1&gt;

# Orchestrator relay credentials (from Step 1)
ORCHESTRATOR_URL=https://orchestrator.example.com
ORCHESTRATOR_CLIENT_ID=&lt;from Step 1&gt;
ORCHESTRATOR_CLIENT_SECRET=&lt;from Step 1&gt;

# Public URL the orchestrator uses to reach this host
BASE_URL=https://agents.example.com

# Custom-agents directory inside the container (matches the volume mount above)
CUSTOM_WORKFLOWS_DIR=/custom-workflows
</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p>Remove the <code>volumes:</code> block entirely if you do not need to mount custom agents \u2014 the container ships with a set of bundled agents and will run without any mount.</p></div></blockquote><h4><strong>Start, inspect, and stop</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Start
docker compose up -d

# Tail logs
docker compose logs -f agent-registry

# Stop
docker compose down

# Update to a newer image
docker compose pull &amp;&amp; docker compose up -d
</code></pre><hr><h2><strong>6. Step 4 — Verify and Register Agents</strong></h2><h3><strong>Health checks</strong></h3><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># On-premise / local
curl http://localhost:8050/healthz
curl http://localhost:8050/readyz

# Cloud
curl https://agents.example.com/healthz
curl https://agents.example.com/readyz
</code></pre><p>Both should return <code>200 OK</code> with <code>{"status":"ok"}</code>.</p><h3><strong>Confirm agent cards are published</strong></h3><p>Every discovered agent exposes its card at <code>${BASE_URL}/agents/&lt;slug&gt;/.well-known/agent-card.json</code>. The slug is the folder name with underscores replaced by hyphens.</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Bundled mock agent — always present
curl https://agents.example.com/agents/mock-multiturn/.well-known/agent-card.json

# Your custom agent
curl https://agents.example.com/agents/turbine-analyst/.well-known/agent-card.json
</code></pre><p>If the response is a JSON document describing the agent's name, description, and skills, the agent is discoverable.</p><h3><strong>Register an agent in the orchestrator</strong></h3><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Only required for HTTP-direct deployments.</strong> Tunneled agents (development mode) register themselves automatically when the tunnel connects.</p></div></blockquote><p>In the orchestrator admin console (<code>https://orchestrator.example.com/admin</code>):</p><ol><li><p>Navigate to the <strong>Agents</strong> tab.</p></li><li><p>Click <strong>Register Agent</strong> and provide:</p><ul><li><p><strong>Endpoint URL:</strong> <code>${BASE_URL}/agents/&lt;slug&gt;/</code> — <strong>the trailing slash matters.</strong></p></li><li><p><strong>Authentication:</strong> OAuth2 client credentials</p></li><li><p><strong>Token URL:</strong> <code>${BASE_URL}/token</code></p></li><li><p><strong>Client ID / Client Secret:</strong> the <code>AUTH_CLIENT_ID</code> and <code>AUTH_CLIENT_SECRET</code> from section 3, <em>Step 1 — Generate Credentials</em></p></li></ul></li><li><p>Click <strong>Test Connection</strong> — the console will fetch the agent card and confirm authentication works.</p></li><li><p><strong>Save</strong> to make the agent visible to Spotfire users.</p></li></ol><p>Repeat for each agent you want exposed. The orchestrator's admin console caches the agent card; if you change an agent's metadata, re-test or restart registration to refresh the cache.</p><hr><h2><strong>7. Step 5 — Set Up the Local Agent Workspace (Developers)</strong></h2><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Applies to:</strong> Local development workstations running the container with <code>MCP_ENABLED=true</code> and <code>TUNNEL_ENABLED=true</code> (see section 5.1, <em>Local Development</em>). Skip this section for production deployments.</p></div></blockquote><h3><strong>Workspace Layout</strong></h3><p>Create a directory anywhere on your host for custom agents. The Agent Registry mounts this directory at <code>/custom-workflows</code> and auto-discovers anything inside it:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>my_agents/
├── .vscode/
│   └── mcp.json                                    # MCP server config for VS Code
├── docker-compose.yml                              # from §5.1
├── .env                                            # from §5.1
├── agent_registry_toolkit_for_spotfire_stubs-*.whl # Type stubs (copied in by the `setup_workspace` MCP tool)
└── turbine_analyst/                                # Your first agent
    ├── description.txt                             # No-code agent root file
    ├── SPEC.md                                     # Design spec (written by confirm_design)
    ├── TDD.md                                      # Technical design (written by confirm_technical_design)
    ├── prompts/
    │   ├── base/
    │   │   ├── system/
    │   │   │   ├── role_intro.txt
    │   │   │   ├── intent_classification.txt
    │   │   │   ├── output_rules.txt
    │   │   │   └── data_schema.txt
    │   │   ├── config/
    │   │   │   ├── messages.json
    │   │   │   ├── column_patterns.json
    │   │   │   └── orchestrator_params.json
    │   │   └── ui/
    │   │       ├── welcome.txt
    │   │       └── metadata_hint.txt
    │   └── intents/
    │       └── analyze_data.txt
    └── stages/                                     # Code agents only
        └── &lt;intent_name&gt;.py
</code></pre><p>The <code>docker-compose.yml</code> from section 5.1 mounts the directory containing the compose file at <code>/custom-workflows</code>, so the workspace and the mount line up automatically. There is nothing to point at — drop new agent folders next to your <code>.env</code> and they appear at <code>/agents/&lt;slug&gt;/</code>.</p><h3><strong>Type Stubs for IDE Autocomplete</strong></h3><p>Code-based agents import from <code>agent_registry_toolkit_for_spotfire</code>, which is installed inside the container at runtime. To get IDE autocomplete, type checking, and inline documentation on your host, install the type-stubs wheel into a local virtual environment.</p><h4><strong>1. Get the wheel into your workspace</strong></h4><p>The wheel is shipped inside the container image. To copy it into your workspace, start the container with <code>MCP_ENABLED=true</code>, open your workspace in VS Code, and ask Copilot Chat (Agent mode):</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>run setup_workspace
</code></pre><p>The <code>setup_workspace</code> MCP tool copies the prebuilt <code>agent_registry_toolkit_for_spotfire_stubs-*.whl</code> from inside the container into the root of your custom-agents directory, alongside a <code>pyproject.toml</code> and a <code>.vscode/mcp.json</code> if either is missing. It is safe to re-run — existing files are left untouched.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p>The <code>setup_workspace</code> tool is part of the MCP development server. It is only available when <code>MCP_ENABLED=true</code>. Production deployments do not expose it.</p></div></blockquote><h4><strong>2. Install the wheel into a local virtual environment</strong></h4><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cd my_agents
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linuxsource .venv/bin/activate

pip install agent_registry_toolkit_for_spotfire_stubs-*.whl
</code></pre><p>After installation, your IDE resolves <code>from agent_registry_toolkit_for_spotfire import ...</code> with full type information. The stubs cover every public module: <code>ops</code>, <code>session</code>, <code>schema_discovery</code>, <code>schema_pipeline</code>, <code>llm</code>, <code>sql_helpers</code>, <code>workflow_loop</code>, <code>filter_state</code>, <code>intent_router</code>, <code>response_parser</code>, <code>stage_executor</code>, and <code>validation</code>.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p>When you upgrade to a newer container image, re-run <code>setup_workspace</code> and <code>pip install --force-reinstall</code> to refresh the stubs against the new toolkit version.</p></div></blockquote><h3><strong>Enabling the MCP Development Server</strong></h3><p>The MCP server is mounted at <code>http://localhost:8050/mcp/</code> when <code>MCP_ENABLED=true</code>. It is exempt from bearer-token authentication for ease of use during development.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>VS Code 1.109 or later is required.</strong> The MCP Apps UI surface (design form, welcome screens, interactive components rendered in Copilot Chat) targets the stable MCP Apps protocol shipped in VS Code 1.109. On earlier builds, MCP tools that return component blocks degrade to plain JSON and the design form will not render. Confirm your VS Code version via <strong>Help → About</strong> before opening an issue.</p></div></blockquote><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong><span class="ipsEmoji" title="">⚠️</span> The MCP server exposes unauthenticated endpoints that can create files, list agent internals, and read templates. It must be disabled in production.</strong> The production deployment examples in sections 5.2 – 5.6 leave <code>MCP_ENABLED</code> unset, so it is off by default in production.</p></div></blockquote><h4><strong>Configure VS Code</strong></h4><p>Create <code>my_agents/.vscode/mcp.json</code>:</p><pre spellcheck="" class="ipsCode language-json" data-language="JSON"><code>{"servers": {
    "spotfire-agent-dev": {
      "type": "http",
      "url": "http://localhost:8050/mcp/"
    }}}</code></pre><ol><li><p>Make sure the container is running with <code>MCP_ENABLED=true</code>.</p></li><li><p>Open the <code>my_agents</code> folder in VS Code.</p></li><li><p>VS Code / GitHub Copilot will auto-discover the MCP server.</p></li><li><p>In Copilot Chat (Agent mode), ask <code>list agents</code> — Copilot invokes the <code>list_agents</code> MCP tool and returns the discovered agents.</p></li></ol><p>For the full development workflow (Design → TDD → Scaffold → Dry-run), see <a rel="">Agent Registry Toolkit User Guide</a>.</p><h3><strong>Hot-Reload Behaviour</strong></h3><p>The container watches the mounted custom-agents directory for changes:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>What changed</p></th><th colspan="1" rowspan="1"><p>How it reloads</p></th><th colspan="1" rowspan="1"><p>User-visible delay</p></th></tr><tr><td colspan="1" rowspan="1"><p>Agent files under <code>/custom-workflows</code></p></td><td colspan="1" rowspan="1"><p>In-process re-import of the affected workflow only</p></td><td colspan="1" rowspan="1"><p>&lt;1 second</p></td></tr><tr><td colspan="1" rowspan="1"><p>Adding or removing an entire agent folder</p></td><td colspan="1" rowspan="1"><p>Container restart required: <code>docker compose restart</code></p></td><td colspan="1" rowspan="1"><p>5–10 seconds</p></td></tr><tr><td colspan="1" rowspan="1"><p>New container image version</p></td><td colspan="1" rowspan="1"><p><code>docker compose pull &amp;&amp; docker compose up -d</code></p></td><td colspan="1" rowspan="1"><p>10–30 seconds</p></td></tr></tbody></table></div><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Logs are visible on the host.</strong> Conversation logs are written to <code>&lt;your custom-agents directory&gt;/logs/&lt;slug&gt;/</code> automatically, so they appear directly in your workspace without needing <code>docker exec</code>. See the per-agent log files for full Python tracebacks if an agent crashes.</p></div></blockquote><hr><h2><strong>8. Authentication Reference</strong></h2><p>The Agent Registry uses <strong>OAuth2 client credentials</strong> end-to-end. Two flows are in play:</p><h3><strong>Flow 1 — Spotfire / orchestrator → Agent Registry</strong></h3><p>A caller (the orchestrator, or a test script) obtains a JWT from the Agent Registry's <code>/token</code> endpoint and uses it as a bearer token on every request to <code>/agents/&lt;slug&gt;/</code>.</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Step 1 — Get a token
TOKEN=$(curl -s -X POST http://localhost:8050/token \
  -d "grant_type=client_credentials&amp;client_id=${AUTH_CLIENT_ID}&amp;client_secret=${AUTH_CLIENT_SECRET}" \
  | python -c "import sys, json; print(json.load(sys.stdin)['access_token'])")

# Step 2 — Call an agent
curl -X POST http://localhost:8050/agents/mock-multiturn \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "role": "user",
        "parts": [{"kind": "text", "text": "hello"}],
        "messageId": "msg-1"
      }
    }
  }'</code></pre><p>Tokens are signed with <code>AUTH_SIGNING_KEY</code> and expire after <code>AUTH_TOKEN_TTL</code> seconds (default <code>3600</code>).</p><h3><strong>Flow 2 — Agent Registry → Orchestrator</strong></h3><p>The Agent Registry uses <code>ORCHESTRATOR_CLIENT_ID</code> / <code>ORCHESTRATOR_CLIENT_SECRET</code> to obtain an orchestrator token whenever it needs to:</p><ul><li><p>Relay an LLM call (every prompt-relay agent and every code agent using <code>create_llm_caller</code>)</p></li><li><p>Query the orchestrator's vector store (RAG)</p></li><li><p>Register or update an agent (used by the tunnel)</p></li><li><p>Open the <code>/tunnel/connect</code> WebSocket</p></li></ul><p>Tokens are cached in-process and refreshed automatically as they near expiry.</p><hr><h2><strong>9. Environment Variable Reference</strong></h2><h3><strong>Required variables (container will not start or operate without these)</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Example</p></th><th colspan="1" rowspan="1"><p>Description</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_CLIENT_ID</code></p></td><td colspan="1" rowspan="1"><p><code>agent-registry-prod</code></p></td><td colspan="1" rowspan="1"><p>Client ID for the Agent Registry's <code>/token</code> endpoint.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_CLIENT_SECRET</code></p></td><td colspan="1" rowspan="1"><p><em>(32-byte random)</em></p></td><td colspan="1" rowspan="1"><p>Client secret for the above. Store as a secret.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_SIGNING_KEY</code></p></td><td colspan="1" rowspan="1"><p><em>(32-byte random)</em></p></td><td colspan="1" rowspan="1"><p>HMAC key for signing issued JWTs. Store as a secret.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_URL</code></p></td><td colspan="1" rowspan="1"><p><code>https://orchestrator.example.com</code></p></td><td colspan="1" rowspan="1"><p>Base URL of the deployed orchestrator.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_CLIENT_ID</code></p></td><td colspan="1" rowspan="1"><p><em>(from orchestrator)</em></p></td><td colspan="1" rowspan="1"><p>OAuth2 client ID registered in the orchestrator with the <code>agent_developer</code> scope profile.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>ORCHESTRATOR_CLIENT_SECRET</code></p></td><td colspan="1" rowspan="1"><p><em>(from orchestrator)</em></p></td><td colspan="1" rowspan="1"><p>Corresponding client secret. Store as a secret.</p></td></tr></tbody></table></div><h3><strong>Server variables</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Default</p></th><th colspan="1" rowspan="1"><p>Description</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>PORT</code></p></td><td colspan="1" rowspan="1"><p><code>8050</code></p></td><td colspan="1" rowspan="1"><p>Port the uvicorn server listens on inside the container.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BASE_URL</code></p></td><td colspan="1" rowspan="1"><p><code>http://localhost:${PORT}</code></p></td><td colspan="1" rowspan="1"><p>Externally-visible URL used to build agent card URLs and the OAuth2 token endpoint URL. Override behind a reverse proxy or cloud ingress.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>AUTH_TOKEN_TTL</code></p></td><td colspan="1" rowspan="1"><p><code>3600</code></p></td><td colspan="1" rowspan="1"><p>JWT lifetime in seconds.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>LOG_LEVEL</code></p></td><td colspan="1" rowspan="1"><p><code>INFO</code></p></td><td colspan="1" rowspan="1"><p>Logging verbosity: <code>DEBUG</code>, <code>INFO</code>, <code>WARNING</code>, <code>ERROR</code>.</p></td></tr></tbody></table></div><h3><strong>Custom agent variables</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Default</p></th><th colspan="1" rowspan="1"><p>Description</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>CUSTOM_WORKFLOWS_DIR</code></p></td><td colspan="1" rowspan="1"><p><em>(unset)</em></p></td><td colspan="1" rowspan="1"><p><strong>Container-internal</strong> path that the registry scans for custom agents. Set this and bind-mount your host directory to that path (the section 5.1 and section 5.6 compose files both use <code>/custom-workflows</code>). If unset, only bundled agents are served.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>CONVERSATION_LOGS_DIR</code></p></td><td colspan="1" rowspan="1"><p><code>$CUSTOM_WORKFLOWS_DIR/logs</code> or <code>./logs</code></p></td><td colspan="1" rowspan="1"><p>Directory for per-agent conversation log files.</p></td></tr></tbody></table></div><h3><strong>Development variables</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Default</p></th><th colspan="1" rowspan="1"><p>Description</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>MCP_ENABLED</code></p></td><td colspan="1" rowspan="1"><p><em>(unset)</em></p></td><td colspan="1" rowspan="1"><p>Enable the MCP development server at <code>/mcp</code>. Required for the <code>setup_workspace</code>, <code>list_agents</code>, <code>dry_run_agent</code>, and other development tools. <strong>Must be unset or </strong><code>false</code><strong> in production.</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>GENERATE_STUBS</code></p></td><td colspan="1" rowspan="1"><p><code>false</code></p></td><td colspan="1" rowspan="1"><p>Set to <code>true</code> only when iterating on the toolkit source itself. Regenerates the in-container <code>.pyi</code> stubs on every reload. Has no effect for users who only consume the shipped image — use the <code>setup_workspace</code> MCP tool to obtain the prebuilt wheel.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DEBUGPY</code></p></td><td colspan="1" rowspan="1"><p><em>(unset)</em></p></td><td colspan="1" rowspan="1"><p>Set to <code>1</code> or <code>true</code> to start <code>debugpy</code> on port <code>5678</code> for VS Code remote debugging.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DEBUGPY_PORT</code></p></td><td colspan="1" rowspan="1"><p><code>5678</code></p></td><td colspan="1" rowspan="1"><p>Port <code>debugpy</code> listens on.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>DEBUGPY_WAIT</code></p></td><td colspan="1" rowspan="1"><p><em>(unset)</em></p></td><td colspan="1" rowspan="1"><p>Block container startup until a debugger attaches.</p></td></tr></tbody></table></div><h3><strong>Tunnel variables</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Variable</p></th><th colspan="1" rowspan="1"><p>Default</p></th><th colspan="1" rowspan="1"><p>Description</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>TUNNEL_ENABLED</code></p></td><td colspan="1" rowspan="1"><p><em>(unset)</em></p></td><td colspan="1" rowspan="1"><p>Enable the outbound WebSocket tunnel to the orchestrator. <strong>Must be unset or </strong><code>false</code><strong> in production.</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>TUNNEL_USER_ID</code></p></td><td colspan="1" rowspan="1"><p><em>(prompted)</em></p></td><td colspan="1" rowspan="1"><p>Spotfire username under which tunneled agents are surfaced. <strong>Must exactly match the username of the active Spotfire session</strong> — case-sensitive, including any domain/realm portion. If unset, the container prompts at startup.</p></td></tr></tbody></table></div><hr><h2><strong>10. Troubleshooting</strong></h2><h3><strong>Authentication errors</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Symptom</p></th><th colspan="1" rowspan="1"><p>Cause</p></th><th colspan="1" rowspan="1"><p>Fix</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>401 Unauthorized</code> from <code>/agents/&lt;slug&gt;</code></p></td><td colspan="1" rowspan="1"><p>Missing or expired bearer token</p></td><td colspan="1" rowspan="1"><p>Re-obtain a token from <code>/token</code> (see section 8, <em>Authentication Reference</em>).</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>/token</code> returns <code>invalid_client</code></p></td><td colspan="1" rowspan="1"><p><code>AUTH_CLIENT_ID</code> / <code>AUTH_CLIENT_SECRET</code> do not match</p></td><td colspan="1" rowspan="1"><p>Confirm the values in your environment match what the caller is sending.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Orchestrator relay fails with <code>401</code></p></td><td colspan="1" rowspan="1"><p>Wrong or expired <code>ORCHESTRATOR_CLIENT_*</code> credentials</p></td><td colspan="1" rowspan="1"><p>Re-create the OAuth2 client in the orchestrator with the <code>agent_developer</code> scope profile and update both variables.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tunnel handshake rejected with <code>403 insufficient_scope</code></p></td><td colspan="1" rowspan="1"><p>Orchestrator client missing <code>agents:write</code></p></td><td colspan="1" rowspan="1"><p>Recreate the client with the <code>agent_developer</code> profile (or grant <code>agents:write</code> explicitly).</p></td></tr></tbody></table></div><h3><strong>Container startup issues</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Symptom</p></th><th colspan="1" rowspan="1"><p>Cause</p></th><th colspan="1" rowspan="1"><p>Fix</p></th></tr><tr><td colspan="1" rowspan="1"><p>Container exits immediately</p></td><td colspan="1" rowspan="1"><p>Missing <code>AUTH_CLIENT_ID</code>, <code>AUTH_CLIENT_SECRET</code>, or <code>AUTH_SIGNING_KEY</code></p></td><td colspan="1" rowspan="1"><p>Ensure all three are set in your environment.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Health probe hitting <code>/health</code> returns 404 → restart loop</p></td><td colspan="1" rowspan="1"><p>The Agent Registry has <strong>no </strong><code>/health</code><strong> endpoint</strong></p></td><td colspan="1" rowspan="1"><p>Set the probe path to <code>/healthz</code> (liveness) and <code>/readyz</code> (readiness).</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BASE_URL</code> mismatch — orchestrator can't reach the registry</p></td><td colspan="1" rowspan="1"><p><code>BASE_URL</code> is set to <code>http://localhost:8050</code> in a cloud deployment</p></td><td colspan="1" rowspan="1"><p>Set <code>BASE_URL</code> to the externally-visible URL (Container App FQDN, Cloud Run URL, or Ingress hostname).</p></td></tr><tr><td colspan="1" rowspan="1"><p>Custom agents not discovered</p></td><td colspan="1" rowspan="1"><p><code>CUSTOM_WORKFLOWS_DIR</code> not set, or volume mount missing</p></td><td colspan="1" rowspan="1"><p>Set <code>CUSTOM_WORKFLOWS_DIR=/custom-workflows</code> in <code>.env</code> and bind-mount your host agent directory to <code>/custom-workflows</code> in <code>docker-compose.yml</code> (see section 5.1).</p></td></tr></tbody></table></div><h3><strong>Local development issues</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Symptom</p></th><th colspan="1" rowspan="1"><p>Cause</p></th><th colspan="1" rowspan="1"><p>Fix</p></th></tr><tr><td colspan="1" rowspan="1"><p>Local Python process bound to <code>:8050</code></p></td><td colspan="1" rowspan="1"><p>A stale <code>uv run</code> / uvicorn from a previous session is still running</p></td><td colspan="1" rowspan="1"><p><code>netstat -ano | findstr ":8050"</code> (Windows) or <code>lsof -i:8050</code> (macOS/Linux), then kill the host process. Docker traffic routes non-deterministically when two listeners bind the same port.</p></td></tr><tr><td colspan="1" rowspan="1"><p>MCP tools not appearing in Copilot Chat</p></td><td colspan="1" rowspan="1"><p><code>MCP_ENABLED</code> not set, or <code>.vscode/mcp.json</code> missing</p></td><td colspan="1" rowspan="1"><p>Set <code>MCP_ENABLED=true</code>, restart the container, and confirm <code>.vscode/mcp.json</code> points at <code>http://localhost:8050/mcp/</code>.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Hot-reload picks up some edits but not others</p></td><td colspan="1" rowspan="1"><p>Watching the wrong directory</p></td><td colspan="1" rowspan="1"><p>Make sure your agent folder is <strong>inside</strong> the host directory that is bind-mounted to <code>/custom-workflows</code>. Adding or removing an entire agent folder still requires <code>docker compose restart</code>.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tunnel connects but agents not visible in Spotfire Copilot</p></td><td colspan="1" rowspan="1"><p><code>TUNNEL_USER_ID</code> does not match the Spotfire username you are logged in as</p></td><td colspan="1" rowspan="1"><p>Set <code>TUNNEL_USER_ID</code> to your exact Spotfire username (case-sensitive, including any domain or realm portion).</p></td></tr></tbody></table></div><h3><strong>Orchestrator connectivity</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Symptom</p></th><th colspan="1" rowspan="1"><p>Cause</p></th><th colspan="1" rowspan="1"><p>Fix</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>Connection refused</code> when calling orchestrator from inside container</p></td><td colspan="1" rowspan="1"><p>Using <code>localhost</code> instead of <code>host.docker.internal</code></p></td><td colspan="1" rowspan="1"><p>When the orchestrator runs on the host, set <code>ORCHESTRATOR_URL=http://host.docker.internal:8080</code>. <code>localhost</code> resolves to the container itself.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Connection reset</code> after a few minutes of idle tunnel</p></td><td colspan="1" rowspan="1"><p>Network path drops idle WebSockets</p></td><td colspan="1" rowspan="1"><p>Increase the orchestrator's <code>TUNNEL_PING_INTERVAL</code> and <code>TUNNEL_PONG_TIMEOUT</code> (see the orchestrator's <code>WebSocket tunnel</code> env vars).</p></td></tr><tr><td colspan="1" rowspan="1"><p>Agent registered but orchestrator returns 502 for requests</p></td><td colspan="1" rowspan="1"><p>Orchestrator cannot reach <code>BASE_URL</code> over the network</p></td><td colspan="1" rowspan="1"><p>Confirm DNS resolution and outbound connectivity from the orchestrator to <code>BASE_URL</code>. For TLS endpoints, verify the certificate chain.</p></td></tr></tbody></table></div><h3><strong>Useful diagnostic commands</strong></h3><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># ── On-premise / local Docker Compose ────────────────────────────
docker compose ps
docker compose logs -f dev    # or 'prod'

# ── Inside the container ─────────────────────────────────────────
docker compose exec dev curl -s http://localhost:8050/healthz
docker compose exec dev curl -s http://localhost:8050/readyz

# ── Kubernetes ───────────────────────────────────────────────────
kubectl -n spotfire-agents get pods
kubectl -n spotfire-agents logs deployment/agent-registry --tail=100

# ── Cloud (any platform) ─────────────────────────────────────────
curl -s https://agents.example.com/healthz | python -m json.tool
curl -s https://agents.example.com/readyz  | python -m json.tool
</code></pre><hr><h2><strong>11. Security Best Practices</strong></h2><h3><strong>Disable development features in production</strong></h3><ul><li><p><code>MCP_ENABLED</code><strong> must be unset or </strong><code>false</code><strong>.</strong> The MCP server exposes unauthenticated endpoints that can scaffold files, read templates, and inspect agent internals. The production deployment examples in sections 5.2 – 5.6 leave it unset.</p></li><li><p><code>TUNNEL_ENABLED</code><strong> must be unset or </strong><code>false</code><strong>.</strong> The tunnel registers agents under a developer's user ID; that flow is only meaningful on a developer workstation.</p></li><li><p><code>DEBUGPY</code><strong> must be unset.</strong> Exposing port 5678 in production allows arbitrary remote code execution via the Python debugger protocol.</p></li></ul><h3><strong>Credential management</strong></h3><ul><li><p><strong>Never embed </strong><code>AUTH_CLIENT_SECRET</code><strong>, </strong><code>AUTH_SIGNING_KEY</code><strong>, or </strong><code>ORCHESTRATOR_CLIENT_SECRET</code><strong> in container images or source control.</strong> Use your cloud platform's secrets manager (Azure Key Vault, GCP Secret Manager, AWS Secrets Manager) or Kubernetes Secrets.</p></li><li><p><strong>Rotate </strong><code>AUTH_SIGNING_KEY</code><strong> periodically.</strong> Rotation invalidates all outstanding tokens — schedule it during a maintenance window. Update the secret in your secrets store and restart the container.</p></li><li><p><strong>Scope the orchestrator client to </strong><code>agent_developer</code><strong>.</strong> Do not grant additional scopes "just in case" — the Agent Registry does not need them.</p></li></ul><h3><strong>Network security</strong></h3><ul><li><p><strong>Always terminate TLS in front of the Agent Registry in production.</strong> Cloud platforms (Container Apps, Cloud Run, ALB on ECS) terminate TLS at the ingress layer; on-premise, place a reverse proxy (nginx, Traefik, Caddy) in front of port 8050.</p></li><li><p><strong>Mount custom-agent volumes read-only in production.</strong> Use <code>:ro</code> on Docker Compose volume specs and <code>readOnly: true</code> in Kubernetes <code>volumeMounts</code>. The container never writes to <code>CUSTOM_WORKFLOWS_DIR</code> in normal operation.</p></li><li><p><strong>Restrict outbound network egress</strong> to the orchestrator's URL and the OCI registry. The Agent Registry has no other outbound dependencies in production.</p></li></ul><h3><strong>Container security</strong></h3><ul><li><p><strong>Always pull pinned versions.</strong> Reference <code>copilotoci.azurecr.io/spotfirecopilot/agent-container:1.1.0</code>, not <code>:latest</code>. Pinned tags make rollback deterministic.</p></li><li><p><strong>Run as non-root.</strong> The base image already runs as a non-root user — do not override the user in your manifests.</p></li><li><p><strong>Set memory and CPU limits.</strong> A runaway agent should be capped rather than allowed to consume the host. The Kubernetes example in §5.5 includes sensible defaults.</p></li></ul><h3><strong>Audit and monitoring</strong></h3><ul><li><p><strong>Stream logs to your platform's logging backend</strong> (Azure Monitor, GCP Cloud Logging, CloudWatch). The container writes to stdout in JSON-line format and respects <code>LOG_LEVEL</code>.</p></li><li><p><strong>Conversation logs land on disk</strong> under <code>$CUSTOM_WORKFLOWS_DIR/logs/&lt;slug&gt;/</code>. Ensure your log volume has sufficient capacity and rotation in production deployments that retain logs.</p></li><li><p><strong>Review orchestrator OAuth2 client activity</strong> in the orchestrator's admin console — the <strong>Security Audit</strong> tab shows token issuance, failed authentications, and agent-registration events.</p></li></ul><hr><p><em>Copyright <span class="ipsEmoji">©</span> 2006 – 2026 Cloud Software Group, Inc. All rights reserved.</em></p>]]></description><guid isPermaLink="false">3609</guid><pubDate>Wed, 13 May 2026 15:05:59 +0000</pubDate></item></channel></rss>
