PRIVATE AI LAB · PART 7
A browser chat interface for my PAIS agent
I wanted to chat with my private agent outside the PAIS Playground. In my lab I deployed a custom web application on Kubernetes: NGINX serves the interface and terminates HTTPS, while a Node.js backend handles VCF Automation sign-in and streams answers from my PAIS agent.
My design: one application pod · NGINX + Node.js · LoadBalancer service · authorization code with PKCE
Update: reconnecting chat after my PAIS migration
October 21, 2026: My chat application still runs in its separate general-purpose VKS cluster. I moved its Llama agent and GPU inference to a new PAIS instance configured through VCF Automation. The generic example’s environment-variable design supports this change; the original source download remains an example rather than an export of my live deployment.
- Record the replacement PAIS HTTPS base URL and the newly generated agent ID. The agent display name and model routing name are not substitutes for that ID.
- In Automation’s OIDC Services, edit the relying party trusted by the new PAIS instance. Preserve its PAIS redirect URI and add the existing
https://chat.lab.example.com/auth/callback. Keep the client public and PKCE required. - Update
PAIS_BASE_URL,PAIS_AGENT_ID, andOIDC_CLIENT_IDin the backend configuration. Keep the issuer and public chat origin consistent with the actual environment. - Add the new PAIS public issuing-CA chain to the backend trust bundle while preserving the CAs needed for Automation and other existing connections. The server certificate must cover the actual hostname or IP address in its SAN. I kept TLS validation enabled.
- Roll out the backend configuration while preserving the current NGINX routes, including
/mcpwhen installed. Expect memory-only sessions to require sign-in again. - Verify browser authentication, a real agent response, and any attached MCP tool end to end. Rollout and health checks alone do not establish those integrations.
My post-migration checks passed: all nine existing authentication regression tests, chat deployment readiness, browser sign-in through the replacement client, and a real workload-status tool response. The backend trusts the replacement PAIS CA; direct Chrome access to PAIS administration still needed that private CA installed through normal trust management. This browser trust step is separate from successful backend TLS validation.
When using the rollback example below, remember that reverting a Deployment does not revert an OIDC relying party, CA ConfigMap, or a backend file mounted from a separate ConfigMap. Retain the corresponding configuration together. See the updated deployment post for the instance migration context.
This follows creating my Llama agent. My PAIS instance and agent must already be working. This interface is custom lab software that I maintain separately; it is not a built-in PAIS component.
1. Understand the application boundaries
My browser connects to the chat application’s HTTPS hostname. A Kubernetes LoadBalancer service forwards port 443 to NGINX on port 8443. Inside the same pod, NGINX proxies authentication and chat requests to the backend on 127.0.0.1:3000. The backend calls the PAIS agent API using the signed-in user’s access token.
My chat application runs in a general-purpose VKS workload cluster associated with my application Supervisor namespace. It does not run directly on Supervisor, on DLVM1, or inside the PAIS-managed model runtime cluster. The chat pod needs no GPU; inference stays on the PAIS model worker.
My initial design intentionally uses one replica. Login state and access tokens are stored in backend memory, while conversation history is stored in browser localStorage and partitioned by authenticated identity. Restarting the backend or expiring a token requires sign-in again. Signing out does not erase the saved browser conversations.
2. Download the example source
Download the PAIS browser chat example source bundle. It includes the static interface, backend, Dockerfiles, NGINX configuration, authentication tests, Deployment, Service, and README. The bundle contains generic examples and no credentials, private keys, kubeconfigs, or internal CA files.
The download is adapted from my working application so the issuer, PAIS URL, and agent ID can be configured through environment variables. Its nine token-validation tests and frontend syntax check passed. I have not deployed this generic copy into a second environment; perform the end-to-end checks below after supplying real settings.
Get-FileHash -Algorithm SHA256 .\pais-chat-example-blog-1.1.0.zip
Expand-Archive .\pais-chat-example-blog-1.1.0.zip -DestinationPath C:\Lab\PrivateAI\browser-chat
Set-Location C:\Lab\PrivateAI\browser-chat\pais-chat-example
node --version
node --test auth.test.mjs
node --check public/app.jsExpected ZIP SHA-256:
9f9bb0e20ceb0a1c175f818e28f3c75799290d476b12d64b7d0deeec3b57bd9fUse Node.js 24 for the source checks. Read the README before building. The Dockerfiles preserve the base image digests from my lab build; review and update those intentionally as part of ongoing maintenance.
3. Prepare DNS names and network access
| Example name | Purpose |
|---|---|
| chat.lab.example.com | Browser-facing application |
| pais.lab.example.com | PAIS agent API |
| auto.lab.example.com | VCF Automation identity provider |
| harbor.lab.example.com | Private application container images |
Replace these names throughout the manifest. Allow browser HTTPS access to chat and the identity provider, pod HTTPS access to PAIS and VCF Automation, and node HTTPS access to Harbor. Confirm DNS resolution from the nodes and pod network as well as the workstation.
I create the final chat DNS record after the LoadBalancer address is assigned. No direct browser connection to the backend port is needed.
4. Register the browser application’s callback
My backend uses the same public VCF Automation client that PAIS trusts, so the user’s access token can be forwarded to the agent API. Keep the existing PAIS redirect URI and add this exact URI:
https://chat.lab.example.com/auth/callback- Open the client/relying party configuration in VCF Automation using an authorized administrator account.
- Select the public client already used by my PAIS configuration.
- Add the chat callback URI without removing the existing PAIS URI.
- Keep authorization code flow and PKCE S256 required.
- Record the client ID and the exact issuer URL for the Deployment.
The example backend targets VCF Automation’s /oidc issuer and its /oauth2/authorize, /oauth2/token, and /oauth2/jwks endpoints. It validates RS256 ID tokens. It is not a discovery-driven OIDC client for arbitrary providers. A different provider requires code changes and authentication testing.
5. Connect to my application VKS cluster
Select the general-purpose cluster that will host the chat application. Verify the context before creating resources:
kubectl config get-contexts
kubectl config use-context REPLACE_WITH_APPLICATION_VKS_CONTEXT
kubectl config current-context
kubectl get nodes
kubectl create namespace pais-chatThe namespace command fails harmlessly if it already exists. Inspect an existing namespace and its workloads before reusing it. My application cluster supports LoadBalancer services; another cluster needs its supported ingress or load balancer design configured first.
6. Prepare the private application images
Create a private Harbor project named ai-apps. Use separate upload and pull-only identities, following the account pattern in my Harbor post. Model artifacts remain in ai-models; these are ordinary application container images.
Authenticate to Harbor using the uploader account through Docker’s prompt, then build and push both images from the extracted source directory:
docker login harbor.lab.example.com
docker build --platform linux/amd64 -f Dockerfile.nginx -t harbor.lab.example.com/ai-apps/pais-chat-web:blog-1.1.0 .
if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' }
docker build --platform linux/amd64 -f Dockerfile.backend -t harbor.lab.example.com/ai-apps/pais-chat-backend:blog-1.1.0 .
if ($LASTEXITCODE -ne 0) { throw 'Backend build failed' }
docker push harbor.lab.example.com/ai-apps/pais-chat-web:blog-1.1.0
if ($LASTEXITCODE -ne 0) { throw 'Frontend push failed' }
docker push harbor.lab.example.com/ai-apps/pais-chat-backend:blog-1.1.0
if ($LASTEXITCODE -ne 0) { throw 'Backend push failed' }Record each pushed digest in Harbor and replace the corresponding image tag in deployment.yaml with repository@sha256:digest. My working deployment used digest-pinned images.
For disconnected builds, stage the exact Docker base images and required tooling on a connected system first. Model uploads and application image builds are separate workflows.
7. Create the registry pull Secret
Use the pull-only robot for the ai-apps project. This example prompts locally and creates a Docker configuration Secret without placing a literal password in the command history:
$robot = Get-Credential -Message 'Enter the ai-apps pull-only robot username and secret'
try {
$pair = $robot.UserName + ':' + $robot.GetNetworkCredential().Password
$auth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
$config = @{auths=@{'harbor.lab.example.com'=@{auth=$auth}}} | ConvertTo-Json -Depth 6 -Compress
@{
apiVersion='v1'; kind='Secret'
metadata=@{name='harbor-ai-apps'; namespace='pais-chat'}
type='kubernetes.io/dockerconfigjson'
stringData=@{'.dockerconfigjson'=$config}
} | ConvertTo-Json -Depth 8 -Compress | kubectl create -f -
if ($LASTEXITCODE -ne 0) { throw 'Pull Secret creation failed' }
} finally {
$robot=$null; $pair=$null; $auth=$null; $config=$null
}This creates a new Secret and fails if it already exists. Plan credential rotation for an existing deployment. Registry trust must also be configured on the cluster nodes; a Secret provides authentication, not certificate trust.
8. Create the TLS Secret and outbound trust bundle
Request a server certificate for chat.lab.example.com using the internal CA. Save the full server chain and matching private key outside the source/build directory.
kubectl create secret tls pais-chat-tls -n pais-chat --cert='C:\Lab\Certificates\chat-fullchain.pem' --key='C:\Lab\Certificates\chat.key'
kubectl create configmap platform-ca -n pais-chat --from-file=ca.crt='C:\Lab\Certificates\platform-ca-chain.pem' --dry-run=client -o yaml | kubectl apply -f -
kubectl get secret pais-chat-tls,harbor-ai-apps -n pais-chat
kubectl get configmap platform-ca -n pais-chatThe ConfigMap contains the public CA chain needed to trust PAIS and VCF Automation. If different CAs issue their certificates, include the required trust chains. The TLS Secret contains the chat server key and certificate. Never add private keys to a container image.
9. Configure the Deployment
Edit the included deployment.yaml. Supply the real values for these backend environment variables:
| Variable | Value |
|---|---|
| PUBLIC_ORIGIN | https://chat.lab.example.com — no trailing slash |
| OIDC_CLIENT_ID | The public VCF Automation client ID used by PAIS |
| OIDC_ISSUER | https://auto.lab.example.com/oidc — no trailing slash |
| PAIS_BASE_URL | https://pais.lab.example.com — no trailing slash |
| PAIS_AGENT_ID | The generated ID recorded when creating my agent |
| NODE_EXTRA_CA_CERTS | /trust/ca.crt — mounted from platform-ca |
The agent ID is not the model routing name. Confirm that the selected agent is Available and uses the intended Llama endpoint. The UI labels in public/index.html, public/app.js, and the backend’s /api/me response can be adapted before rebuilding to match another agent.
My sample keeps one replica and uses Recreate updates to avoid distributing memory-only sessions between pods. Updates interrupt active sessions. Adding replicas requires shared session storage and an appropriate authentication/session design; changing the replica count alone is insufficient.
Both containers run as non-root, with read-only root filesystems and dropped capabilities. NGINX receives a bounded temporary volume. Kubernetes API service account token mounting is disabled because the application does not need cluster API access.
10. Check streaming and apply the manifests
The included NGINX configuration proxies /api/ and /auth/ to the local backend. Response buffering is disabled so streamed tokens reach the browser as they arrive. The proxy timeouts accommodate the backend’s response timeout.
The included Service maps HTTPS port 443 to NGINX port 8443. There is no separately exposed backend Service.
kubectl apply --dry-run=server -f .\deployment.yaml -f .\service.yaml
kubectl apply -f .\deployment.yaml -f .\service.yaml
kubectl rollout status deployment/pais-chat -n pais-chat --timeout=180s
kubectl get pods,services -n pais-chat
kubectl logs deployment/pais-chat -n pais-chat -c backend --tail=50My readiness check verifies the backend is reachable through NGINX. It does not verify the identity provider, PAIS, or model inference. Use the end-to-end checks below. Kubernetes documents LoadBalancer service behavior in its Service guide.
11. Point DNS at the chat Service
Read the Service’s assigned external address:
kubectl get service pais-chat -n pais-chat
kubectl describe service pais-chat -n pais-chatCreate the A record for chat.lab.example.com pointing to that address. Allow routed HTTPS access from the workstation subnet. Do not point it at a pod address or copy the PAIS ingress address; the chat application has its own Service.
Resolve-DnsName chat.lab.example.com
Test-NetConnection chat.lab.example.com -Port 443
curl.exe https://chat.lab.example.com/healthzVerify the browser trusts the certificate and its hostname matches the requested URL. If the load balancer remains Pending, troubleshoot the cluster’s load balancer integration and address allocation first.
12. Validate sign-in, streaming, and conversation behavior
- Open the chat hostname in a fresh browser session.
- Select Sign in with VCF Automation and authenticate as an authorized PAIS organization user.
- Confirm the callback returns to the chat application and displays the signed-in identity.
- Ask a short question and check that the answer streams progressively.
- Ask a contextual follow-up in the same conversation.
- Reload the page and confirm the conversation is restored in that browser.
- Sign out and test another authorized identity using an appropriate separate browser profile.
- Check that an unauthenticated chat request is rejected and an expired session requests sign-in again.
My original deployment passed HTTPS, VCF sign-in, streaming, restoration, contextual follow-up, and nine authentication regression tests. The generic bundle must pass the same checks with its supplied environment settings.
Browser history is not encrypted application storage. User-based storage keys help separate normal UI views but are not a security boundary against someone with access to the same browser profile. A shared workstation needs an appropriate profile and retention policy.
13. Operate and troubleshoot the application
- ImagePullBackOff: check the image digest, robot expiration, registry CA trust, and node DNS.
- Failed callback: compare PUBLIC_ORIGIN, the registered redirect URI, client ID, issuer, and clock synchronization.
- 502 on chat: inspect backend logs, PAIS connectivity, agent availability, and the user’s authorization.
- All text arrives at once: check buffering on NGINX and any upstream proxy in front of the Service.
- Sign-in disappears after restart: this is expected with memory-only sessions.
- Internal .local names fail: inspect unicast DNS routing/search configuration on cluster nodes. My original Photon nodes needed a domain correction; check replacement nodes too.
Renew the TLS certificate and rotate the robot credential before expiration. After updating the certificate Secret or trust ConfigMap, roll the deployment so NGINX and Node load the new material. Expect a brief interruption and a fresh sign-in.
kubectl rollout restart deployment/pais-chat -n pais-chat
kubectl rollout status deployment/pais-chat -n pais-chat
# For a failed application image update with a prior revision:
kubectl rollout undo deployment/pais-chat -n pais-chatDeployment rollback does not roll back separately changed Secrets, DNS, or VCF Automation client settings. Keep the previous manifests, image digests, and configuration records.
I now have a browser interface that uses my existing PAIS agent and private model endpoint. This adds a convenient application layer; it does not add MCP permissions or new infrastructure tools to the agent.
