diff --git a/apps/agent-zero/agent-zero.yaml b/apps/agent-zero/agent-zero.yaml index d3c797d..ff8a890 100644 --- a/apps/agent-zero/agent-zero.yaml +++ b/apps/agent-zero/agent-zero.yaml @@ -127,6 +127,18 @@ metadata: spec: itemPath: "vaults/IAmWorkin/items/Print.Web API Keys" +--- +# Knowledge MCP bearer token for the direct Agent Zero -> Knowledge.Web path. +# The 1Password item currently stores the raw token in its concealed PASSWORD +# field, which the operator syncs to Secret key `password`. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: knowledge-mcp-tokens + namespace: agent-zero +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore Knowledge MCP Tokens" + --- apiVersion: apps/v1 kind: Deployment @@ -242,13 +254,54 @@ spec: sed -i 's/^ //' /a0/usr/plugins/_model_config/config.json # Phase 0 Chat MCP pilot: Agent Zero does not interpolate env vars # inside A0_SET_mcp_servers JSON, so build the final JSON here from - # the secret-backed CHAT_MCP_API_KEY env var before initialize.sh. - # Use the in-cluster Chat service URL rather than the public - # Traefik hostname so the pod stays off the private VIP lane that - # the default egress rule blocks. - if [ -n "${CHAT_MCP_API_KEY:-}" ]; then - export A0_SET_mcp_servers="{\"mcpServers\":{\"fc-chat\":{\"type\":\"streamable-http\",\"url\":\"http://chat-web.fc-chat.svc/mcp\",\"headers\":{\"X-Api-Key\":\"${CHAT_MCP_API_KEY}\"}}}}" + # the secret-backed env vars before initialize.sh. Keep the local + # corpus_search.py tool mounted either way so outage fallback + # remains available even when fc_knowledge is not advertised. + KNOWLEDGE_MCP_ENABLED=false + if [ -n "${KNOWLEDGE_MCP_BEARER_TOKEN:-}" ]; then + if curl -sf --connect-timeout 3 "${KNOWLEDGE_MCP_HEALTH_URL}" > /dev/null && \ + curl -sf --connect-timeout 5 \ + -H "Authorization: Bearer ${KNOWLEDGE_MCP_BEARER_TOKEN}" \ + -H "Accept: application/json, text/event-stream" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"fc-knowledge-bootstrap","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"agent-zero-bootstrap","version":"1.0"}}}' \ + "${KNOWLEDGE_MCP_URL}" > /dev/null; then + KNOWLEDGE_MCP_ENABLED=true + echo "fc_knowledge enabled from ${KNOWLEDGE_MCP_URL}." + else + echo "fc_knowledge unavailable or unauthorized; keeping local corpus_search.py as the fallback path." + fi + else + echo "fc_knowledge token missing; keeping local corpus_search.py as the fallback path." fi + + export A0_SET_mcp_servers="$( + python3 - <<'PY' +import json +import os + +servers = {} + +chat_key = os.getenv("CHAT_MCP_API_KEY") +if chat_key: + servers["fc_chat"] = { + "type": "streamable-http", + "url": "http://chat-web.fc-chat.svc/mcp", + "headers": {"X-Api-Key": chat_key}, + } + +if os.getenv("KNOWLEDGE_MCP_ENABLED", "false").lower() == "true": + token = os.getenv("KNOWLEDGE_MCP_BEARER_TOKEN", "") + if token: + servers["fc_knowledge"] = { + "type": "streamable-http", + "url": os.getenv("KNOWLEDGE_MCP_URL", "http://knowledge-web.knowledge.svc/mcp"), + "headers": {"Authorization": f"Bearer {token}"}, + } + +print(json.dumps({"mcpServers": servers}, separators=(",", ":"))) +PY + )" # Run the original entrypoint exec /exe/initialize.sh $BRANCH ports: @@ -351,6 +404,19 @@ spec: name: chat-mcp-api-key key: api-key optional: true + # FlowerCore.Knowledge MCP Phase 1 — direct Agent Zero client path. + # Probe /healthz first, then try an authenticated initialize call. + # If either fails, Agent Zero boots without fc_knowledge and keeps + # the local corpus_search.py tool as the outage-safe path. + - name: KNOWLEDGE_MCP_URL + value: "http://knowledge-web.knowledge.svc/mcp" + - name: KNOWLEDGE_MCP_HEALTH_URL + value: "http://knowledge-web.knowledge.svc/healthz" + - name: KNOWLEDGE_MCP_BEARER_TOKEN + valueFrom: + secretKeyRef: + name: knowledge-mcp-tokens + key: password # Print.Web — Thermal printer service on edge2. # PRINT_WEB_URL: internal HTTP (bypasses Traefik TLS — print_web.py # runs in-cluster and can reach edge2 directly on the PROD VLAN). @@ -575,6 +641,17 @@ spec: protocol: TCP - port: 8080 protocol: TCP + # FlowerCore.Knowledge MCP (Phase 1) — in-cluster direct route with + # anonymous /healthz probe plus authenticated /mcp initialize/tool calls. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: knowledge + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP # Intranet search API — use in-cluster svc so traffic stays inside # the cluster and is not blocked by the private-range egress denylist. - to: diff --git a/apps/knowledge/README.md b/apps/knowledge/README.md index 34679ae..28bb121 100644 --- a/apps/knowledge/README.md +++ b/apps/knowledge/README.md @@ -5,7 +5,9 @@ Phase 2.4 closed. Pod running, certificate issued (step-ca-acme), PVC bound (Longhorn 20Gi RWO), ArgoCD `infra-knowledge` synced. `/healthz` returns 200, `/api/v1/editions` returns `[]` (initial-deploy state — no *.db files in the PVC yet; Phase 2.5+ admin UI handles bulk -population). +population). Phase 1 of the Agent Zero MCP rollout keeps `/healthz` +anonymous and gates `/mcp` behind `Authorization: Bearer ` built +from the 1Password item `FlowerCore Knowledge MCP Tokens`. - Plan: [`../../../FlowerCore.Notes/docs/ai-agents/flowercore-knowledge-service-plan.md`](../../../FlowerCore.Notes/docs/ai-agents/flowercore-knowledge-service-plan.md) - Sprint: [`../../../FlowerCore.Notes/docs/ai-station/sprint-e-xxl-plan.md`](../../../FlowerCore.Notes/docs/ai-station/sprint-e-xxl-plan.md) (Track B) @@ -19,6 +21,12 @@ search to the rest of the FC ecosystem (Agent Zero, Chat.Web persona memory, AiStation embeddings explorer, TtsReader chapter context, BMO bot, Pi nodes via `fc-index sync`). +Phase 1 MCP routing is explicit: + +- in-cluster Agent Zero → `http://knowledge-web.knowledge.svc/mcp` +- workstation Agent Zero → `https://knowledge.iamworkin.lan/mcp` +- probe URL for both lanes → `/healthz` + ## Deployment order (do NOT skip / reorder) ### 1. FlowerCore.DNS public A record — knowledge.iamworkin.lan -> 10.0.56.200 diff --git a/apps/knowledge/knowledge.yaml b/apps/knowledge/knowledge.yaml index bcd2913..d38ba83 100644 --- a/apps/knowledge/knowledge.yaml +++ b/apps/knowledge/knowledge.yaml @@ -40,16 +40,16 @@ metadata: labels: app.kubernetes.io/part-of: bluejay-infra --- -# MCP API key — synced from 1Password so /mcp stays gated without baking -# secrets into Git. The PASSWORD category maps the concealed field to Secret -# key `password`, which the Deployment reads into FlowerCore:Mcp:ApiKey:Key. +# MCP bearer token for the read-only Agent Zero Phase 1 lane. The 1Password +# item currently stores the raw token in its concealed PASSWORD field, which +# the operator syncs into the namespaced Secret key `password`. apiVersion: onepassword.com/v1 kind: OnePasswordItem metadata: - name: knowledge-mcp-api-key + name: knowledge-mcp-tokens namespace: knowledge spec: - itemPath: "vaults/IAmWorkin/items/KnowledgeApiKey" + itemPath: "vaults/IAmWorkin/items/FlowerCore Knowledge MCP Tokens" --- apiVersion: v1 kind: PersistentVolumeClaim @@ -102,8 +102,17 @@ spec: - name: web # Placeholder tag — bump to the image you built + imported to ALL # RKE2 nodes via scripts/deploy-knowledge.sh before applying. - image: localhost/fc-knowledge-web:v202604272200 + image: localhost/fc-knowledge-web:v20260429225548 imagePullPolicy: Never + command: + - /bin/sh + - -c + args: + - | + if [ -n "${KNOWLEDGE_MCP_BEARER_TOKEN:-}" ]; then + export FlowerCore__Mcp__ApiKey__Key="Bearer ${KNOWLEDGE_MCP_BEARER_TOKEN}" + fi + exec dotnet FlowerCore.Knowledge.Web.dll ports: - containerPort: 8080 name: http @@ -135,10 +144,12 @@ spec: # workstation GPU when present. - name: FlowerCore__Ollama__BaseUrl value: "http://10.0.57.17:11434" - - name: FlowerCore__Mcp__ApiKey__Key + - name: FlowerCore__Mcp__ApiKey__HeaderName + value: "Authorization" + - name: KNOWLEDGE_MCP_BEARER_TOKEN valueFrom: secretKeyRef: - name: knowledge-mcp-api-key + name: knowledge-mcp-tokens key: password resources: requests: