Skip to content

Latest commit

 

History

History
203 lines (155 loc) · 11.1 KB

AGENTS.md

File metadata and controls

203 lines (155 loc) · 11.1 KB

AI Agent Guide — bosl-high-seas

Deployment model: public repo, git-clone initContainer

This repo is public. The pod's git-clone initContainer (k8s/deployment.yaml) runs git clone --depth 1 https://github.com/boettiger-lab/bosl-high-seas.git on each pod start and copies index.html, docs.html, bosl-logo.svg, layers-input.json, and system-prompt.md into the nginx html dir. Pod content tracks main. The k8s/configmap.yaml ConfigMap holds only the LLM model list and the nginx reverse-proxy template — not website content.

Deployments: NRP only

cirrus is scaled down. Production runs on NRP Nautilus only; deploy there and nowhere else. The cirrus manifests (k8s/cirrus-*.yaml, replicas: 0) and layers-input.cirrus.json are kept so it can be revived, but they are not maintained. Before reviving it, regenerate the mirror (below) and set replicas: 1.

The table records both setups for reference:

NRP Nautilus (prod) cirrus (local k3s, scaled down)
Manifests k8s/{configmap,deployment,service,ingress}.yaml k8s/cirrus-*.yaml
Namespace schmidtdse high-seas
URL https://high-seas.nrp-nautilus.io https://high-seas.carlboettiger.info
Layer config layers-input.json (s3-west.nrp-nautilus.io) layers-input.cirrus.json (minio.carlboettiger.info)
MCP server duckdb-mcp.nrp-nautilus.io duckdb-mcp.carlboettiger.info (mcp ns)
LLM open-llm-proxy → OpenRouter + NRP models in-cluster vLLM qwen3-6 only (vllm ns)
Ingress HAProxy annotations Traefik CRDs + cert-manager + external-dns

layers-input.cirrus.json is a committed copy of layers-input.json with the S3 and MCP hosts rewritten — config.json can override mcp_server_url but not the STAC catalog/collection URLs, so the layer file itself has to differ. While cirrus is scaled down, keeping it in sync is optional; regenerate it before reviving cirrus:

sed -e 's|https://s3-west\.nrp-nautilus\.io|https://minio.carlboettiger.info|g' \
    -e 's|https://duckdb-mcp\.nrp-nautilus\.io/mcp|https://duckdb-mcp.carlboettiger.info/mcp|g' \
    -e 's|"titiler_url": "https://titiler\.nrp-nautilus\.io"|"titiler_url": "https://titiler.carlboettiger.info"|' \
    layers-input.json > layers-input.cirrus.json

The cirrus initContainer copies it over layers-input.json in the nginx html dir.

All three expressions are required. cirrus has its own titiler at https://titiler.carlboettiger.info (verified healthy), and that is what the committed mirror uses. Regenerating with only the first two expressions silently repoints every cirrus raster tile request at NRP.

Deploying to cirrus (only if reviving it)

One-time setup (namespace + a copy of the vLLM API key, which nginx injects into the /api/llm/ Authorization header; secrets can't be read across namespaces):

kubectl create namespace high-seas
kubectl create secret generic vllm-api-key -n high-seas \
  --from-literal=api-key="$(kubectl get secret vllm-api-key -n vllm -o jsonpath='{.data.api-key}' | base64 -d)"
kubectl apply -f k8s/cirrus-configmap.yaml -f k8s/cirrus-service.yaml \
              -f k8s/cirrus-deployment.yaml -f k8s/cirrus-ingress.yaml

Routine content edits — same two-step as NRP (push, then restart so the pod re-clones):

git add <source-files> && git commit -m "<message>" && git push
kubectl rollout restart deployment/bosl-high-seas -n high-seas
kubectl rollout status deployment/bosl-high-seas -n high-seas

Third deployment: private (password-protected) instance (#85)

https://high-seas-private.nrp-nautilus.io is the same app plus the restricted-licence layers (IMMA, KBA) whose hosting agreement requires a password-protected app and exclusion from the source.coop mirror. Protection is by licence policy, not cybersecurity: the files themselves sit in ordinary public-* buckets, and that is accepted under the agreement.

  • Never add a restricted layer to layers-input.json. It goes in private/layers.json (with insert_after) and its licence notes in private/system-prompt-addendum.md.
  • Generated files (layers-input.private.json, system-prompt.private.md, k8s/private-{configmap,deployment}.yaml) come from scripts/build-private.py. Re-run it after ANY edit to layers-input.json, system-prompt.md, k8s/configmap.yaml or k8s/deployment.yaml, and commit the output in the same PR, or the private app drifts.
  • Auth: nginx auth_basic on every path except /health and /api/llm/. geo-agent sends LLM requests with Authorization: Bearer <api_key>, which replaces the browser's saved password, so /api/llm/ instead checks a token: APP_TOKEN, derived at pod start from the htpasswd Secret and served only in the password-protected config.json. Without it the chat fails with 401 and the browser re-prompts for the password. The htpasswd file comes from the bosl-high-seas-private-htpasswd Secret in schmidtdse. It is never committed. The username is bosl, and the passphrase is shared only with approved colleagues.
  • Rotate the password (then share the new one out of band):
    PP='<new passphrase>'
    kubectl -n schmidtdse create secret generic bosl-high-seas-private-htpasswd \
      --from-literal=htpasswd="bosl:$(openssl passwd -apr1 "$PP")" --dry-run=client -o yaml | kubectl apply -f -
    kubectl -n schmidtdse rollout restart deployment/bosl-high-seas-private
  • Deploy: same push-then-restart model as prod, using the private manifests:
    kubectl apply -f k8s/private-configmap.yaml -f k8s/private-deployment.yaml \
                  -f k8s/private-service.yaml -f k8s/private-ingress.yaml
    kubectl -n schmidtdse rollout restart deployment/bosl-high-seas-private
    Check: curl -s -o /dev/null -w '%{http_code}' https://high-seas-private.nrp-nautilus.io/ → 401.

Literature server (private app only)

The private app also connects to a prototype literature server, high-seas-text-query (code: SchmidtDSE/text-query, main). It reads the copyrighted corpus from the private rustfs bucket high-seas-lit with a read-only key that glen-ops mints, so it has no ingress; the private nginx proxies /api/text-query/ to it behind the password. k8s/text-query.yaml holds its ConfigMap (the corpus description the agent reads) and Service, and k8s/text-query-deployment.yaml its Deployment, which is alone in that file because the glen-ops weekly age reset deletes and re-applies it. Apply the Service before restarting the private app: nginx will not start if the route's Service is missing. docs/text-query-walkthrough.md follows one question through the tools and the data.

EBSA explorer (explorer/)

A second, no-LLM app at https://high-seas.nrp-nautilus.io/explorer/ that ranks the 336 EBSAs on five provisional pillars, built on glen-charts (pinned in explorer/index.html). It is configuration, SQL and precomputed data only; see explorer/README.md for units, variables and scoring. The git-clone initContainer copies the whole explorer/ folder, so a push plus a rollout restart deploys it like the main app.

# After changing explorer SQL: re-run the precompute (MCP; about a minute in all) and commit data/
npx github:SchmidtDSE/glen-charts#v0.2.4 precompute --site explorer lists
npx github:SchmidtDSE/glen-charts#v0.2.4 precompute --site explorer results
npx github:SchmidtDSE/glen-charts#v0.2.4 precompute --site explorer sources

The explorer's header carries its own pin of geo-agent (the DSE mark) and the BOSL logo (../bosl-logo.svg); when bumping geo-agent, bump explorer/index.html too.

Repo relationship

Repo Purpose
geo-agent Core library (map, chat, agent, tools). Source of truth for all functionality.
bosl-high-seas Application repo. Configure layers-input.json, system-prompt.md, and k8s/ for this dataset.

Full docs: schmidtdse.github.io/glen/


What you configure (and what you don't)

You configure: layers-input.json, system-prompt.md, index.html, and k8s/ manifests.

You do not write JavaScript. Core modules are loaded from the CDN.


Deployment

If you lack credentials or permissions to run kubectl or git push, do not attempt to discover or work around credentials. Instead, provide the user with the exact commands to run.

# 1. Edit source files (index.html, layers-input.json, system-prompt.md)
# 2. Commit and push to main — the initContainer clones from GitHub
git add <source-files> && git commit -m "<message>" && git push
# 3. Restart the deployment so a new pod re-clones the latest main
kubectl rollout restart deployment/bosl-high-seas -n schmidtdse
kubectl rollout status deployment/bosl-high-seas -n schmidtdse

The git push does not update running pods — step 3 does.

If you change k8s/configmap.yaml (LLM model list or nginx template), apply it before the rollout: kubectl apply -f k8s/configmap.yaml -n schmidtdse. Routine content edits don't need this.

CDN versioning

index.html pins the geo-agent library version. Verify jsDelivr serves a new tag before deploying:

curl -sI https://cdn.jsdelivr.net/gh/SchmidtDSE/glen@vX.Y.Z/app/style.css | grep HTTP
# Must return HTTP/2 200 — a 404 means jsDelivr hasn't indexed the tag yet

For private data modules (rclone sidecar, oauth2-proxy, private parquet credentials): docs/guide/private-deployment


Adding a raster (COG) layer: two things that are not in the schema docs

  1. Check whether the COG's declared nodata matches any actual pixel. geo-agent passes the STAC raster:bands[0].nodata straight to titiler, so a declared nodata that no pixel matches masks nothing and the tile comes back 100% opaque — the layer then covers the basemap across its whole footprint. ship-density declares 2147483647 and contains none, so every asset sets "nodata": 0 to mask the real "nothing recorded" value. config.nodata overrides the STAC value per asset. Verify with the tile's alpha channel, not by eye:

    curl -s -o t.png "$TITILER/cog/tiles/WebMercatorQuad/3/4/2.png?url=$ENCODED&colormap_name=reds&rescale=0,N&nodata=0"
    python3 -c "import rasterio,numpy as np; a=rasterio.open('t.png').read(); print((a[3]==0).mean())"
  2. Set rescale from measured percentiles, not from min/max. These fields are strongly skewed, and geo-agent only supports a linear ramp (no log/sqrt expression), so absolute min/max flattens everything to the bottom colour. Compute the percentile at the overview level the default global view renders from (roughly 1/16 to 1/64 for a global raster), not at native resolution. Precedents: seafloor-carbon-flux uses 2nd-98th percentiles, ship-density the 99th percentile of nonzero pixels.


For full layers-input.json schema, troubleshooting, and configuration reference see the glen-template AGENTS.md or the docs.