The Host Context Controller is a Kubernetes operator and REST API service that sits at the heart of the Clerum platform. It performs these key roles:
- McpServer Operator — Watches
McpServerCRDs and automatically manages Deployments and Services for each MCP server, including secret validation. - Host Operator — Watches
HostCRDs and automatically manages one Deployment, one Service, and one PVC per host, including secret validation. - NetworkPolicy Operator — Watches
ContextandMcpServerCRDs and generates a four-layer NetworkPolicy model (deny-all, infrastructure, context-allow, external-egress) plus recipe binding policies. - REST API — Exposes a curated API that
mcp-hostuses to discover available MCP servers, their status, and auth tokens.
It also reconciles GlobalFileSystem (src/gfsReconciler.ts) and SharedFileSystem (src/sharedFileSystemReconciler.ts) CRDs.
┌─────────────────────┐
│ McpServer CRDs │
└──────────┬──────────┘
│ watch
▼
┌─────────────────┐ REST API ┌───────────────────────────┐ manages ┌─────────────────────┐
│ mcp-host │ ───────────────▶ │ host-context-controller │ ──────────────▶ │ Deployments │
│ (mcp-host ns) │ │ (control-plane ns) │ │ Services │
└─────────────────┘ └───────────────────────────┘ │ NetworkPolicies │
│ └─────────────────────┘
watch │ reads
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌───────────┐ ┌─────────────┐
│ Context CRDs│ │ Secrets │ │ MCP Server │
└─────────────┘ └───────────┘ │ Pods │
└─────────────┘
When an McpServer CRD is created, modified, or deleted, the host-context-controller reconciler:
- ADDED — Validates that referenced secrets exist, then creates a Deployment and Service for the MCP server.
- MODIFIED — Updates the Deployment and Service to match the CRD spec (env vars, image, ports, resources, etc.).
- DELETED — Removes the Deployment and Service.
Remote egress-proxy McpServers keep the author's spec.image as desired-state input; HCC no longer rewrites it to the platform proxy image. The former canonicalizeRemoteEgressProxyImage step and its ImageCanonicalized status condition were removed (PR #205 readiness-safety cleanup): canonicalization mutated the observed CRD cache object and bumped the resource generation, retiring the in-flight reconcile before it could create the runtime resources. The platform-owned proxy image is instead selected at Deployment build time (buildDeployment), leaving the CRD spec untouched.
The reconciler builds Deployments with labels used for pod selection and network policy targeting:
| Label | Value | Purpose |
|---|---|---|
app |
<server-name> |
Standard app selector |
clerum.io/managed-by |
host-context-controller |
Identifies operator-managed pods |
clerum.io/mcpserver |
<server-name> |
Targets a specific MCP server pod |
Before deploying an MCP server, the reconciler checks that:
- The secret referenced by
spec.envSecret.nameexists in the namespace. - All keys listed in
spec.envSecret.keys[].secretKeyare present in the secret data.
If the secret is missing or incomplete, the MCP server will not be deployed. Creating the secrets an McpServer references is not the host-context-controller's responsibility — for those it only validates their existence. (It does create its own Secrets elsewhere, e.g. the per-Host runtime-token Secret in mcp-host.)
The host-context-controller enforces deny-by-default network isolation across all runtime namespaces and generates allow policies from Context and McpServer CRDs. The model has four layers, implemented in src/networkPolicyReconciler.ts, plus recipe-scoped binding policies in src/bindingPolicyReconciler.ts.
One deny-all-<namespace> policy per runtime namespace, configurable via CONTEXT_MAPPER_RUNTIME_NAMESPACES (the shipped deployment sets mcp-server,sandbox-recipes,rpc-proxy,sandbox-ui; the code default is mcp-server,mcp-host,sandbox-recipes,rpc-proxy). Empty pod selector with policyTypes: [Ingress, Egress] — all traffic in both directions is blocked unless a higher layer allows it.
Note: as deployed, mcp-host is not in that list — its deny-all, DNS and K8s-API policies are static manifests (deploy/base/mcp-host/networkpolicies.yaml), not controller-managed.
Baseline plumbing, created once per runtime namespace:
| Policy | Allows |
|---|---|
allow-dns-egress-<ns> |
DNS (UDP+TCP 53) to kube-system; optional extra ipBlock rule for GKE NodeLocal DNSCache via CONTEXT_MAPPER_NODELOCAL_DNS_CIDR |
allow-hcc-api-egress-<ns> |
Egress to the host-context-controller-api-gateway pod in the control-plane namespace, scoped per namespace to the pod classes that need discovery (managed mcp-host pods, mcp-proxy, rpc-proxy, workflow MCP hosts) |
allow-k8s-api-egress-<ns> |
Egress to the Kubernetes API server (TCP 443; CIDRs from CONTEXT_MAPPER_K8S_API_CIDRS, falling back to KUBERNETES_SERVICE_HOST, then to a hardcoded 10.96.0.1/32 when that is unset). Deny by default outside mcp-host: pods in other runtime namespaces must opt in with the platform-owned label clerum.io/k8s-api-egress: "true" |
Namespaces listed in CONTEXT_MAPPER_MINIMAL_INFRA_NAMESPACES get only deny-all + DNS egress (no HCC-API or K8s-API egress).
ensureDefaultPolicies additionally creates one non-suffixed allow-host-context-controller-api policy — not per namespace, but a single policy in CONTEXT_MAPPER_NAMESPACE (mcp-server). It permits ingress from the mcp-host namespace on CONTEXT_MAPPER_PORT to pods matching app: host-context-controller. As shipped, the controller Deployment runs in control-plane, so this policy matches no pod in mcp-server and has no effect; the mcp-host → controller route is actually opened by the allow-hcc-api-egress-<ns> egress policies above.
For each MCP server in a Context CRD's spec.mcpServers, three paired policies open a single route on the server's transport port:
ctx-<contextId>-<serverName>(ingress, MCP server namespace) — the server pod accepts traffic only from mcp-host pods labeled with that context (clerum.io/context), fromrpc-proxypods, and frommcp-proxy.ctx-<contextId>-<serverName>-egress(egress,mcp-hostnamespace) — the context-labeled mcp-host pods may reach that server (required because L0 also denies egress).rpc-egress-<contextId>-<serverName>(egress,rpc-proxynamespace) — rpc-proxy pods may reach that server.
Servers removed from a Context lose their policies on the next reconcile; deleting a Context removes all three families.
McpServer.spec.egressBindings produce ext-egress-* policies in the server's namespace (reconcileExternalEgress in src/networkPolicyReconciler.ts):
exact-host(default) — a CIDR binding, or a public DNS hostname resolved to/32ipBlocks, on one declared port/protocol. CIDRs and resolved IPs are rejected if they overlapPUBLIC_EGRESS_EXCEPT_CIDRS(RFC1918, CGNAT, loopback, link-local incl. cloud metadata, documentation, benchmarking, multicast, reserved ranges). Hostnames likelocalhost,metadata.goog,*.internal,*.svc,*.cluster.localare rejected outright.public-web—0.0.0.0/0on TCP 80/443 withPUBLIC_EGRESS_EXCEPT_CIDRScarved out viaexcept.
Results are written to the McpServer's status.conditions (ExternalEgressReady) and status.resolvedEgressIPs. A failed binding blocks runtime reconciliation of that server, so a workload cannot start before its egress converges. DNS-derived allows are never retained across a failed refresh: HCC revokes the unprovable policy before certifying global readiness, while only the affected runtime remains blocked until DNS recovers. Each lookup has a bounded deadline.
HCC readiness certifies authoritative inventory and revocation safety, not completion of positive policy creation. A renamed or newly requested allow may therefore remain unavailable after HCC becomes Ready until its additive reconciliation succeeds. HCC never keeps an obsolete allow merely to avoid that availability gap: when old and new ownership cannot be proven equivalent, revocation wins and only the affected runtime remains fail-closed.
During an in-place release, HCC is also the controller that stamps
CONTEXT_MAPPER_HOST_IMAGE onto managed mcp-host workloads. The new HCC
producer is consequently running before it replaces those consumers with the
new mcp-host image. For compatibility, an omitted status.authoritative field
retains the legacy fail-closed meaning; only an explicit authoritative: false
asks a new mcp-host to preserve an unchanged live connection.
McpServers carrying a clerum.io/recipe-bindings annotation (set for WorkflowRecipe bindings) get paired bind-<recipe>-<from>-<to>-{egress,ingress} policies connecting the MCP workload in the MCP server namespace with its counterpart in sandbox-recipes on the declared port/protocol (src/bindingPolicyReconciler.ts). Deleting the McpServer removes the recipe's binding policies.
- Startup safety — Ensures L0/L1 defaults, revokes stale or orphaned context-allow, rpc-proxy-egress, and external-egress policies from authoritative Context/McpServer inventories, then certifies readiness safety.
- Startup additive convergence — Reconciles Context allows after certification. The dedicated external-egress coordinator is the sole owner of McpServer egress creation, per-server startup gating, retry, and periodic DNS refresh; a failed binding blocks only that server's runtime.
- Context ADDED/MODIFIED/DELETED — Creates, updates, or removes the L2 policy set for that context.
- McpServer ADDED/MODIFIED/DELETED — Reconciles L3 external egress (before the workload Deployment is created) and recipe binding policies.
Policies select peers with the built-in kubernetes.io/metadata.name namespace label combined with pod label selectors, e.g. the L2 ingress rule:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: mcp-host
podSelector:
matchLabels:
clerum.io/managed-by: host-context-controller
clerum.io/context: context1
ports:
- port: 3000
protocol: TCPWhen a Host CRD is created, modified, or deleted in the host namespace, the host-context-controller host reconciler:
-
ADDED — Validates that
spec.secretRefexists, then creates/updates, in the host namespace (mcp-host):Deployment/<host-name>Service/<host-name>PersistentVolumeClaim/<host-name>-workspaceSecret/host-<host-name>-mcp-host-runtime-tokens(runtime credentials, rotated by the reconciler)ServiceAccount/,Role/andRoleBinding/scoping the pod to its own Host CRD and SecretsNetworkPolicy/allow-rpc-proxy-desktop-<host-name>for desktop hosts
It also reconciles, in the channels namespace (
CONTEXT_MAPPER_CHANNELS_NAMESPACE, defaultchannels), a per-HostService/channel-reader-<host-name>,Deployment/channel-reader-<host-name>andNetworkPolicy/channel-reader-<host-name>-egress. -
MODIFIED — Reconciles the same resources to match current runtime defaults and host identity.
-
DELETED — Deletes the resources listed above, in both namespaces.
Generated resources are labeled for orphan cleanup:
| Label | Value | Purpose |
|---|---|---|
clerum.io/managed-by |
host-context-controller |
Identifies operator-managed host runtime resources |
clerum.io/host |
<host-name> |
Tracks ownership to a specific Host CRD |
On startup, a full host reconciliation pass runs and deletes orphaned host runtime resources whose Host CRD no longer exists — including orphaned channel-reader resources in channels and orphaned per-Host NetworkPolicies (sweepOrphanChannelReaderResources, sweepOrphanHostNetworkPolicies).
The Host-facing MCP contract is authenticated and Host-scoped. HCC verifies the runtime bearer, resolves the live Host and its live Context server-side, and checks the Context grant before it reads credential material. Callers do not supply a Context reference.
GET /health
{ "status": "ok", "ready": true }The server also exposes GET / (API information), GET /ready (readiness; 503 until the controller has started), GET /metrics (Prometheus), and GET /api/v1/desktop/:hostRef (desktop status; requires the CONTEXT_MAPPER_DESKTOP_API_TOKEN bearer token — the check is skipped when that variable is empty).
GET /api/v1/mcpservers
{
"servers": [
{
"name": "mongodb-server",
"contextRef": "context1",
"transport": {
"type": "sse",
"url": "http://mongodb-server.mcp-server.svc.cluster.local:3000/sse"
},
"enabled": true,
"status": { "deployed": true, "ready": true }
}
],
"contextRef": "*",
"timestamp": "2026-02-14T15:11:12.000Z"
}This anonymous, metadata-only route is retained temporarily for the existing
mcp-proxy poller. It is not the mcp-host discovery contract and exposes no
auth selector or Secret metadata. Its removal or authentication belongs to the
separate PR 2 migration.
GET /api/v2/hosts/self/mcpservers
Authorization: Bearer <mcp-host-runtime-token>
The response contains only enabled McpServers granted by the authenticated
Host's live Context. It omits contextRef, auth selectors, and Secret metadata.
An authenticated server includes an opaque credentialRevision so mcp-host can
retire a credential before admitting a changed revision.
The verifier accepts only RS256 tokens from the configured Control API issuer
with an exact HCC audience set (HCC-only, or the bounded HCC + workflow
migration pair), one hostRefs entry, the matching host_uid, and the single
mcp:credential:read capability. The historical workflow scope/standalone
recipe claims identify the reused runtime-token family; they do not grant a
Context. HCC derives that authorization from the live Host and Context objects.
{
"servers": [
{
"name": "mongodb-server",
"transport": {
"type": "sse",
"url": "http://mongodb-server.mcp-server.svc.cluster.local:3000/sse"
},
"enabled": true,
"status": { "deployed": true, "ready": true },
"authRequired": true,
"credentialRevision": "opaque-revision"
}
],
"timestamp": "2026-02-14T15:11:12.000Z"
}POST /api/v2/hosts/self/mcpservers/credential
Authorization: Bearer <mcp-host-runtime-token>
Content-Type: application/json
{ "serverName": "mongodb-server" }
Response (with auth):
{ "token": "credential-value", "credentialRevision": "opaque-revision" }Response (no auth configured):
{ "token": null, "credentialRevision": "opaque-revision" }HCC returns the same opaque 404 {"error":"not_found"} for an ungranted,
missing, disabled, or cross-Context server name. It performs the grant check
before any Secret value read and revalidates the live authority snapshot after
the read. Protected responses are non-cacheable.
The legacy caller-selected routes
GET /api/v1/mcpservers/context/:contextRef and
GET /api/v1/mcpservers/:name/auth are tombstoned with a generic HTTP 410 at
both the gateway and HCC application layers; there is no v1 fallback.
HCC reads environment variables from src/config.ts, src/gfsConfig.ts,
src/logger.ts, and src/k8sApiCidrs.ts. Defaults below are the literal fallbacks
in code; where the deploy manifest overrides one, that is noted.
Parsing semantics. getEnv(key, default) uses process.env[key] ?? default,
so an empty string wins over the default — that is how the minikube overlay
disables the image-pull secret with value: "". getEnvInt / getEnvBool treat
an empty string as unset and fall back to the default; getEnvBool accepts
true (any case) or 1.
Three variables fail the process closed on bad input rather than programming
a permissive policy — see the NetworkPolicy and GFS tables:
CONTEXT_MAPPER_K8S_API_CIDRS, CONTEXT_MAPPER_NODELOCAL_DNS_CIDR, and
CONTEXT_MAPPER_GFSC_IMAGE_PULL_POLICY.
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_PORT |
8081 |
HTTP server port. |
CLERUM_DEV_MODE |
false |
Read McpServers/Contexts/auth tokens from env instead of the K8s API. Also skips the INTERNAL_CONTROL_JWT_HCC_HMAC_SECRET startup assertion below. |
LOG_LEVEL |
info |
debug | info | warn | error. Unrecognized values fall back to info. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_NAMESPACE |
mcp-server |
Where McpServer / Context CRDs live and managed server workloads are created. |
CONTEXT_MAPPER_CONTROL_PLANE_NAMESPACE |
control-plane |
Where control-plane gateway services live. |
CONTEXT_MAPPER_HOST_NAMESPACE |
mcp-host |
Where mcp-host pods run (drives NetworkPolicy generation). |
CONTEXT_MAPPER_GFS_NAMESPACE |
gfs |
Where the GlobalFileSystem controller (gfsc) runs. |
CONTEXT_MAPPER_RPC_PROXY_NAMESPACE |
rpc-proxy |
Where rpc-proxy runs (L2 egress policy generation). |
CONTEXT_MAPPER_CHANNELS_NAMESPACE |
channels |
Per-Host channel-reader Deployments/Services. |
CONTEXT_MAPPER_SANDBOX_NAMESPACE |
sandbox-recipes |
Workflow-recipe namespace. Read only by the GFS factory. |
HCC_TARGET_NAMESPACE |
mcp-host |
Namespace HCC names when asking control-api for caller-bound 1st-party mcp-host credentials. It must exactly equal CONTEXT_MAPPER_HOST_NAMESPACE; HCC exits 1 otherwise. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_MCPSERVER_IMAGE_PULL_POLICY |
IfNotPresent |
Pull policy for reconciled McpServer workloads. |
CONTEXT_MAPPER_EGRESS_PROXY_IMAGE |
clerum/nginx-egress-proxy:0.1.0 |
Per-server nginx egress proxy for remote MCP servers (spec.remote). |
CONTEXT_MAPPER_STDIO_BRIDGE_IMAGE |
(registry-qualified stdio-bridge) |
stdio-bridge sidecar image for managed stdio MCP servers. Manifest pins clerum/stdio-bridge:0.9.5. |
CONTEXT_MAPPER_STDIO_BRIDGE_REQUEST_MEMORY |
32Mi |
stdio-bridge sidecar memory request. |
CONTEXT_MAPPER_STDIO_BRIDGE_REQUEST_CPU |
50m |
stdio-bridge sidecar CPU request. |
CONTEXT_MAPPER_STDIO_BRIDGE_LIMIT_MEMORY |
128Mi |
stdio-bridge sidecar memory limit. |
CONTEXT_MAPPER_STDIO_BRIDGE_LIMIT_CPU |
200m |
stdio-bridge sidecar CPU limit. |
HCC_EXTERNAL_EGRESS_RESYNC_SEC |
300 |
Periodic external-egress DNS resync interval (seconds). 0 disables periodic refresh; watch events still reconcile. |
HCC_EXTERNAL_EGRESS_DNS_TIMEOUT_MS |
5000 |
Per-hostname DNS lookup deadline in milliseconds. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_HOST_IMAGE |
(registry-qualified mcp-host) |
Image for reconciled Host Deployments. Manifest pins clerum/mcp-host:0.9.5. |
CONTEXT_MAPPER_HOST_IMAGE_PULL_POLICY |
Always |
Pull policy. Minikube overlay sets IfNotPresent. |
CONTEXT_MAPPER_HOST_IMAGE_PULL_SECRET |
clerum |
imagePullSecrets name. Minikube overlay sets "" to disable (empty string overrides the default). |
CONTEXT_MAPPER_HOST_PORT |
8080 |
Container/service port for the host runtime. |
CONTEXT_MAPPER_HOST_CONFIGMAP_NAME |
mcp-host-config |
ConfigMap loaded into host containers via envFrom. |
CONTEXT_MAPPER_HOST_SERVICE_ACCOUNT |
mcp-host |
ServiceAccount used by Host Deployments. |
CONTEXT_MAPPER_HOST_WORKSPACE_STORAGE_CLASS |
do-block-storage-retain |
StorageClass for per-host workspace PVCs. Minikube overlay sets standard. |
CONTEXT_MAPPER_HOST_WORKSPACE_SIZE |
10Gi |
Requested per-host workspace PVC size. |
CONTEXT_MAPPER_HOST_WORKSPACE_PATH |
/workspace |
Workspace mount path inside host containers. |
CONTEXT_MAPPER_HOST_RESOURCES_REQUEST_MEMORY |
128Mi |
Host container memory request. |
CONTEXT_MAPPER_HOST_RESOURCES_REQUEST_CPU |
100m |
Host container CPU request. |
CONTEXT_MAPPER_HOST_RESOURCES_LIMIT_MEMORY |
512Mi |
Host container memory limit. |
CONTEXT_MAPPER_HOST_RESOURCES_LIMIT_CPU |
500m |
Host container CPU limit. |
CONTEXT_MAPPER_HOST_RESYNC_SEC |
300 |
Periodic Host fullReconcile (seconds); guards against dropped watch events and drives auth-degraded self-heal. 0 disables. |
HCC_MCP_HOST_RUNTIME_BOOTSTRAP_REFRESH_BEFORE_SEC |
900 |
How long before expiry HCC refreshes the mcp-host bootstrap credential Secret. |
| Variable | Default | Description |
|---|---|---|
HCC_MCP_HOST_JWT_PUBLIC_KEY |
`` (empty) | RS256 public key used to verify protected v2 Host MCP requests. HCC fails startup when it is absent, malformed, private, or a placeholder. |
HCC_MCP_HOST_JWT_ISSUER |
control-api |
Exact issuer accepted for protected v2 Host MCP requests. |
HCC_MCP_HOST_JWT_MAX_TTL_SECONDS |
600 |
Maximum accepted exp - iat; the deploy manifest sources this from Control API's access-token TTL contract. |
HCC_MCP_HOST_API_RATE_LIMIT_PER_MINUTE |
120 |
Per verified Host UID and action fixed-window limit for inventory and credential requests. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_DESKTOP_IMAGE |
clerum/mcp-host-desktop:latest |
Desktop sidecar image. |
CONTEXT_MAPPER_DESKTOP_PORT |
3000 |
Desktop container port. |
CONTEXT_MAPPER_DESKTOP_RESOURCES_REQUEST_MEMORY |
256Mi |
Desktop memory request. |
CONTEXT_MAPPER_DESKTOP_RESOURCES_REQUEST_CPU |
250m |
Desktop CPU request. |
CONTEXT_MAPPER_DESKTOP_RESOURCES_LIMIT_MEMORY |
4Gi |
Desktop memory limit. |
CONTEXT_MAPPER_DESKTOP_RESOURCES_LIMIT_CPU |
1 |
Desktop CPU limit. Author the apiserver-canonical form (1, not 1000m) so the Deployment no-op gate can match. |
CONTEXT_MAPPER_DESKTOP_API_TOKEN |
`` (empty) | Bearer token required by GET /api/v1/desktop/:hostRef. When empty the check is skipped and the endpoint is unauthenticated — a dev-mode convenience. Set it in any shared cluster. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_CHANNEL_READER_IMAGE |
clerum/channel-reader:0.9.5 |
Image for per-Host channel-reader Deployments. |
CONTEXT_MAPPER_CHANNEL_READER_IMAGE_PULL_POLICY |
Always |
Pull policy. Minikube overlay sets IfNotPresent to use locally-loaded images. |
CONTEXT_MAPPER_CHANNEL_READER_HANDOFF_PORT |
8099 |
Internal handoff port on per-Host channel-reader Services. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_WFC_IMAGE |
(registry-qualified wfc) |
Per-SharedFileSystem WFC image. Manifest pins clerum/workspace-files-controller:0.9.5. |
CONTEXT_MAPPER_WFC_IMAGE_PULL_POLICY |
IfNotPresent |
Pull policy for WFC Deployments. |
CONTEXT_MAPPER_WFC_IMAGE_PULL_SECRET |
clerum |
imagePullSecrets name. Minikube overlay sets "" to disable. |
CONTEXT_MAPPER_WFC_PORT |
8086 |
WFC container/service port. |
CONTEXT_MAPPER_WFC_INIT_IMAGE |
busybox:1.36 |
Init container that seeds directories and chowns the fresh PVC. |
CONTEXT_MAPPER_WFC_RESOURCES_REQUEST_MEMORY |
64Mi |
WFC memory request. |
CONTEXT_MAPPER_WFC_RESOURCES_REQUEST_CPU |
50m |
WFC CPU request. |
CONTEXT_MAPPER_WFC_RESOURCES_LIMIT_MEMORY |
128Mi |
WFC memory limit. |
CONTEXT_MAPPER_WFC_RESOURCES_LIMIT_CPU |
200m |
WFC CPU limit. |
CONTEXT_MAPPER_WFC_JWT_PUBLIC_KEY_CM |
mcp-host-config |
ConfigMap holding the JWT public key the WFC verifies against. |
CONTEXT_MAPPER_WFC_JWT_PUBLIC_KEY_CM_KEY |
CLERUM_AUTH_JWT_PUBLIC_KEY |
Key within that ConfigMap. |
CONTEXT_MAPPER_WFC_MAX_UPLOAD_BYTES |
104857600 (100 MiB) |
Upload size cap forwarded to the WFC container. |
CONTEXT_MAPPER_WFC_MAX_LIST_ENTRIES |
5000 |
Max directory entries the WFC will list. |
CONTEXT_MAPPER_WFC_MAX_PATH_DEPTH |
32 |
Max path depth the WFC will traverse. |
CONTEXT_MAPPER_SFS_RESYNC_SEC |
60 |
Periodic SharedFileSystem fullReconcile (seconds). The SFS watch fires only on CRD changes — not on PVC binding or WFC readiness — so this is what drives Initializing/Degraded → Ready. 0 disables. |
Read by src/gfsConfig.ts. Invalid values throw at startup rather than silently defaulting.
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_GFSC_PORT |
8087 |
gfsc service port. |
CONTEXT_MAPPER_GFSC_IMAGE |
clerum/gfs-controller:test |
gfsc writer Deployment image. The default is a :test tag — override it. |
CONTEXT_MAPPER_GFSC_IMAGE_PULL_POLICY |
IfNotPresent |
Must be exactly Always | IfNotPresent | Never; any other non-empty value throws at startup. |
CONTEXT_MAPPER_GFSC_IMAGE_PULL_SECRET |
(unset) | Optional imagePullSecrets name for gfsc. |
CONTEXT_MAPPER_GFSC_PRIORITY_CLASS |
(unset) | Optional priorityClassName for the gfsc Deployment. |
CONTEXT_MAPPER_GFSC_INIT_IMAGE |
busybox:1.36 |
gfsc init-container image. |
CONTEXT_MAPPER_GFSC_REQUEST_MEMORY |
128Mi |
gfsc memory request. |
CONTEXT_MAPPER_GFSC_REQUEST_CPU |
100m |
gfsc CPU request. |
CONTEXT_MAPPER_GFSC_LIMIT_MEMORY |
256Mi |
gfsc memory limit. |
CONTEXT_MAPPER_GFSC_LIMIT_CPU |
500m |
gfsc CPU limit. |
CONTEXT_MAPPER_GFS_POSTGRES_APP_LABEL |
control-postgres |
app pod label selecting the Postgres pods gfsc may reach (NetworkPolicy). |
CONTEXT_MAPPER_GFS_POSTGRES_PORT |
5432 |
Postgres port for the gfsc egress policy. |
CONTEXT_MAPPER_GFS_JWT_CONFIGMAP |
gfs-config |
ConfigMap holding the JWT public key gfsc verifies against. |
CONTEXT_MAPPER_GFS_JWT_CONFIGMAP_KEY |
jwt-public-key |
Key within that ConfigMap. |
CONTEXT_MAPPER_GFS_PG_SECRET |
gfs-controller-db |
Secret holding the gfsc Postgres connection string. |
CONTEXT_MAPPER_GFS_PG_SECRET_KEY |
connection-string |
Key within that Secret. |
CONTEXT_MAPPER_GFS_DRIVE_NAME |
main |
Default GFS drive name. |
CONTEXT_MAPPER_GFS_TOKEN_AUDIENCE |
gfs-controller |
Expected aud claim on GFS access tokens. |
CONTEXT_MAPPER_GFS_RESYNC_SEC |
60 |
Periodic GlobalFileSystem fullReconcile (seconds); drives convergence to Ready and the root-directory seed. 0 disables. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_RUNTIME_NAMESPACES |
mcp-server,mcp-host,sandbox-recipes,rpc-proxy |
Namespaces that get L0 deny-all + L1 infrastructure policies. Manifest overrides to mcp-server,sandbox-recipes,rpc-proxy,sandbox-ui — note it drops mcp-host, whose policies are static (deploy/base/mcp-host/networkpolicies.yaml), because an HCC deny-all would block mcp-host from reaching LLM APIs. |
CONTEXT_MAPPER_MINIMAL_INFRA_NAMESPACES |
`` (empty) | Subset of runtime namespaces that get only deny-all + DNS egress — no HCC-API or K8s-API egress. Manifest sets sandbox-ui. |
CONTEXT_MAPPER_K8S_API_CIDRS |
(unset → []) |
K8s API-server CIDRs for allow-k8s-api-egress-*. Empty falls back to KUBERNETES_SERVICE_HOST. Validated fail-closed at module load: a malformed entry, or one broader than /24 (IPv4) or /120 (IPv6), crashes startup rather than programming a permissive policy. |
CONTEXT_MAPPER_NODELOCAL_DNS_CIDR |
`` (empty) | DNS infrastructure CIDR for NodeLocal DNSCache / kube-dns. Must be exactly one IPv4 /32; anything else throws at startup. |
CONTEXT_MAPPER_NETPOL_RESYNC_SEC |
0 |
Periodic NetworkPolicy coordinated convergence (seconds). Re-enters the single-flight pass with ensureDefaults: true so a tick covers orphan sweep (class A) and, when that flag is set, L0/L1 defaults (class D). Class D also has a separate cheap path via CONTEXT_MAPPER_NETPOL_DEFAULTS_RESYNC_SEC (#488). A tick while a pass is in flight is skipped (clerum_hcc_netpol_resync_ticks_skipped_total{reason="pass-in-flight"}) and does not coalesce; event-driven trailing is unchanged. 0 disables the interval (merge default). Startup still converges once; start() already applied defaults as a bootstrap barrier. Default 0 logs a console.warn on every boot (same pattern as Host/SFS/GFS/LlmHook) — expected; it names the self-heal gap. A non-canonical value (25m, 1e3) fails loud at load instead of coercing through parseInt. Enable is a separate ops decision (issue #478, not this merge): with the skip guard the chosen value is 1500; without the guard the floor is 3600. 300 is not a valid enable while passes last minutes. Treadmill ⇔ pass duration ≥ period. Enable preconditions live on #478: image deployed, census read, histogram p99/max ≪ T, then infra dev first. |
CONTEXT_MAPPER_NETPOL_DEFAULTS_RESYNC_SEC |
0 |
Periodic L0/L1 defaults-only tick (seconds). Calls ensureDefaultPolicies() only — never fullReconcile / runInitialNetworkPolicyConvergence. Write sets are disjoint from the typed sweep (policy-type ∈ {context-allow, rpc-proxy-egress, external-egress}), so a concurrent full pass is safe and this timer does not share initialConvergenceRuns or increment {reason="pass-in-flight"}. Overlapping defaults-only ticks serialize (clerum_hcc_netpol_resync_ticks_skipped_total{reason="defaults-only-in-flight"}). 0 disables the interval (merge default) and does not warn. Not shipped in deploy/base or public overlays. A non-canonical value fails loud at load. Enable is a separate ops decision (issue #488). |
CONTEXT_MAPPER_NETPOL_ORPHAN_DELETE_CAP |
10 |
Absolute orphan-delete cap for a namespace-wide NetworkPolicy sweep. Candidates strictly above this count refuse deletes and increment clerum_hcc_netpol_orphan_sweep_capped_total; the pass still certifies (#478: alert and do not delete). Governs every fullReconcile (startup, event-driven, timer). Absolute is checked first, so at a fleet ≥ 50 with these defaults the percent rule never binds. One cap over the sum of all lanes (not per-lane): a trip leaves those orphans in place until an operator raises the env; the signal is that counter. 0 refuses every orphan delete; a negative or non-canonical value (2e1, 10.9) fails loud at load. Census: scripts/ops/hcc-netpol-orphan-census.sh — live_cap_would_trip is Deployment env only (UNSET when both keys are omitted, not none); controller_cap_would_trip applies compiled defaults when unset. Census listed/orphan sets include the controller repairable class (managed-by + reserved name, policy-type absent). When the Deployment omits CONTEXT_MAPPER_RPC_PROXY_NAMESPACE, the census assumes the compiled rpc-proxy default. |
CONTEXT_MAPPER_NETPOL_ORPHAN_DELETE_CAP_PERCENT |
20 |
Percent orphan-delete cap against the listed managed fleet. Strict >. Inert on a tiny inventory where listed * percent / 100 < 1. Values above 100 never fire. Same every-fullReconcile scope as the absolute cap; only binds when the fleet is small enough that 20% is below the absolute cap of 10. Non-canonical values fail loud at load. |
KUBERNETES_SERVICE_HOST |
(injected by Kubernetes) | Not set by the operator — the /32 fallback source when CONTEXT_MAPPER_K8S_API_CIDRS is empty. |
| Variable | Default | Description |
|---|---|---|
CONTEXT_MAPPER_ALLOWED_IMAGE_PREFIXES |
see packages/image-policy |
Comma-separated trusted image prefixes for local-mode McpServer images. Manifest sets ghcr.io/evenfire-ai/,mongodb/,mcr.microsoft.com/,clerum/. |
CONTEXT_MAPPER_ENFORCE_IMAGE_ALLOWLIST |
false |
false = audit mode (log would-be denials without blocking). true = enforce. Manifest sets "false". |
| Variable | Default | Description |
|---|---|---|
CONTROL_API_BASE_URL |
http://control-api.control-plane.svc.cluster.local:8090 |
Cluster-internal control-api URL. HCC calls it directly to issue mcp-host runtime tokens for the pods it provisions. |
INTERNAL_CONTROL_JWT_HCC_HMAC_SECRET |
`` (empty) | HS256 secret for signing InternalControl JWTs. Required whenever CLERUM_DEV_MODE is not true — the process logs [HCC] FATAL and exits 1 if empty or still a replace-with-… placeholder. |
MCP_HOST_GATEWAY_URL |
http://nginx-workflow-approval-gateway.control-plane.svc:8092 |
The nginx allowlist proxy mediating mcp-host → control-api. HCC injects this into the mcp-host pods it provisions; it does not call it itself. |
Parsed only when CLERUM_DEV_MODE is truthy. A JSON parse failure logs an error and yields an empty result rather than crashing.
| Variable | Default | Description |
|---|---|---|
CLERUM_MCP_SERVERS |
[] |
JSON array of McpServer objects. |
CLERUM_CONTEXTS |
[] |
JSON array of Context objects used by the development provider. Caller-selected Context discovery is not exposed. |
CLERUM_MCP_AUTH |
{} |
JSON object mapping MCP server name → auth token for development-provider fixtures. |
The shipped Roles and RoleBindings are the source of truth — see deploy/base/<namespace>/rbac.yaml. The controller's ServiceAccount lives in control-plane and is bound into mcp-server, mcp-host, channels, rpc-proxy, sandbox-recipes, sandbox-ui and gfs. The table below is an overview of the two main namespaces, not a complete Role definition — do not build a Role from it.
In mcp-server (deploy/base/mcp-server/rbac.yaml):
| API Group | Resource | Verbs | Purpose |
|---|---|---|---|
clerum.io |
mcpservers |
get, list, watch, patch | Watch McpServer CRDs |
clerum.io |
mcpservers/status |
get, patch | Publish readiness / secret-resolution conditions |
clerum.io |
contexts |
get, list, watch | Watch Context CRDs for NetworkPolicy reconciliation |
networking.k8s.io |
networkpolicies |
get, list, create, update, delete | Manage generated NetworkPolicies |
apps |
deployments |
get, list, create, update, delete | Manage MCP server Deployments |
| (core) | services |
get, list, create, update, delete | Manage MCP server Services |
| (core) | configmaps |
get, list, create, update, delete | Manage the remote egress proxy nginx config |
| (core) | secrets |
get, list, watch | Read auth tokens, validate envSecret, re-enqueue on change |
In mcp-host (deploy/base/mcp-host/rbac.yaml), the controller additionally manages per-Host identity and SharedFileSystem resources: hosts (get, list, watch), sharedfilesystems + sharedfilesystems/status, batch/jobs, deployments, services, persistentvolumeclaims, networkpolicies, serviceaccounts, roles/rolebindings, configmaps (get, watch, list), pods (get, list), and secrets with get, watch, list, create, update, patch, delete — it creates the per-Host host-<hostRef>-mcp-host-runtime-tokens Secret and the per-Host ServiceAccount + Role + RoleBinding on every reconcile.
| Component | Namespace | Purpose |
|---|---|---|
| host-context-controller | control-plane |
Operator and API service |
| MCP server pods + CRDs | mcp-server |
McpServer and Context source of truth + managed server workloads |
| mcp-host | mcp-host |
Consumer of the REST API, target of NetworkPolicy from rules |
| channel-reader | channels |
Communication layer |
Run host-context-controller locally without Kubernetes by providing MCP servers via environment variables. In dev mode, no reconciliation occurs (no Deployments, Services, or NetworkPolicies are created).
Startup still requires a real trusted Control API RS256 public key in
HCC_MCP_HOST_JWT_PUBLIC_KEY, including in dev mode. Export it from the
approved local public-key source before either command below. .env.example
deliberately does not include a placeholder key, and HCC has no insecure
verifier fallback.
# Install dependencies
npm install
# Load HCC_MCP_HOST_JWT_PUBLIC_KEY from the trusted Control API public-key source.
# Option 1: Use .env file (recommended)
cp .env.example .env
# Edit .env with your servers
make dev
# Option 2: Run with inline example servers (quick test)
make dev-examplemake dev requires a .env file — it sources ./.env and exits with an error if the file is absent. Because the recipe re-sources .env after make has exported the environment, any key defined in .env overrides a value passed inline on the command line; a variable not present in .env is passed through untouched. To provide your own servers, put them in .env:
CLERUM_DEV_MODE=true
CLERUM_MCP_SERVERS='[{"name":"filesystem","spec":{"contextRef":"dev-context","description":"Filesystem operations","image":"mcp/filesystem:latest","transport":{"type":"sse","url":"http://localhost:3001/sse","port":3001},"enabled":true}}]'
CLERUM_CONTEXTS='[{"name":"dev-context","namespace":"dev","spec":{"contextId":"dev-context","description":"Dev context","mcpServers":["filesystem"]}}]'
CLERUM_MCP_AUTH='{"github":"ghp_your_token_here"}'Dev mode retains the metadata-only global v1 inventory for local mcp-proxy
compatibility. It does not restore either caller-selected v1 Host route. The
protected v2 contract requires live Kubernetes Host/Context authority and
therefore fails closed when that authority is unavailable.
Run against the cluster in your current kubeconfig (watches K8s for McpServer and Context CRDs) with CLERUM_DEV_MODE unset. Production startup requires both the existing INTERNAL_CONTROL_JWT_HCC_HMAC_SECRET signing material and the real Control API public key in HCC_MCP_HOST_JWT_PUBLIC_KEY. Load both from their approved sources before starting; missing or invalid verifier material fails startup closed.
make build
npm startmake dev also works, but only if a .env file exists (see above) — it is the same target, so it aborts when .env is absent. Leave CLERUM_DEV_MODE out of that .env and load both required values through the approved local configuration path to run against a real cluster.
host-context-controller/Dockerfile copies packages/workflow-recipe-capability-policy, packages/image-policy and host-context-controller/, so the build context must be the repo root, not this directory. Build from the repo root and point -f at the Dockerfile:
# From the repo root
export IMAGE=ghcr.io/<org>/host-context-controller:$(git describe --tags --always)
# Build
docker build -f host-context-controller/Dockerfile -t "$IMAGE" .
# Push
docker push "$IMAGE"
# Multi-platform build + push (ARM host targeting an amd64 cluster)
docker buildx build --platform linux/amd64 -f host-context-controller/Dockerfile -t "$IMAGE" --push .For a local minikube cluster, scripts/minikube/build-images.sh builds this image (and the other services) with the correct root context.
The component Makefile's
docker-build/docker-push/docker-push-crosstargets pass this directory as the build context, so theCOPY packages/...layer cannot resolve and the build fails. Use the root-context commands above.
The component Makefile does not deploy. The controller's ServiceAccount, Deployment and Service live in deploy/base/control-plane/host-context-controller.yaml; namespaces are in deploy/base/namespaces.yaml and RBAC in deploy/base/<namespace>/rbac.yaml. All are applied with kustomize from the repo root, e.g. kubectl apply -k deploy/overlays/minikube.