Repository navigation
Release 0.4.1: control-plane authentication and ingestion, MCP stdio environment
Security release. It changes defaults, so read the migration section before upgrading.
Who should upgrade
Everyone running >= 0.1.0, <= 0.4.0, in particular anyone who runs the control plane (context_runtime.control_plane.app, the image built by the
Dockerfile) on a network others can reach, and anyone who starts stdio MCP servers through MCPClient.stdio from a process that holds credentials in
its environment.
What changed
Control plane
- Authentication fails closed. With neither
CONTEXT_RUNTIME_API_KEYnorAGENTIC_OS_API_KEYset, every route exceptGET /healthanswers503. With a key
set, a missing or wrong credential is401. Before, the mutating routes (/dispatch,/approvals/{id}/{decision},/up,/down,/agent/run,
/agent/outcome,/librechat/ingest, ...) accepted anyone when no key was set. - Read routes need a credential.
/approvals,/jobs/{id},/status,/api/fleet,/notify/status,/agent/policy/{module},/modules, the
dashboards (/,/overview,/m/*), the/vibex/*reads,/librechat/retrieve|compare|explain|image|policyand/v1/*were open with or without a
key. They now require one. - Access classes. Every route is
public,readeroradmin(table in SECURITY.md).GET /healthis the only public route and answers
{"status": "ok"}. A route that is not classified is refused, and a test compares the live route table with the expected classes. - Tenant keys.
CR_TENANT_KEYSkeeps its meaning (a key bound to tenants,*for all). A tenant key now works on thereaderroutes only. The control-plane
key works on all routes; to choose a non-default tenant it must also be listed inCR_TENANT_KEYS(for exampleADMIN_KEY=*), as before.
CR_ALLOW_ANONYMOUS_TENANTS=1lets every authenticated caller use every tenant and does not admit callers without a credential. - Ingestion is limited to a corpus root.
POST /librechat/ingestindexes only insideCONTEXT_RUNTIME_CORPUS_ROOT, by identifier
({"corpus": "name"}) or by a path relative to the root. No root configured: ingestion is disabled (503). Absolute paths,.., hidden names, backslashes,
%and control characters are400; a directory that resolves outside the root, and a corpus that contains a symlink to somewhere outside itself, are404. - Constant-time comparison. The control-plane key and tenant keys are compared with
hmac.compare_digest. - Documentation routes.
/docs,/redocand/openapi.jsonare not served. - Container image. The
Dockerfileinstallsagent-harnessat the commitpyproject.tomlpins and checks the archive's sha256; before, the image of0.4.0failed at
start withModuleNotFoundError: agent_harness.deploy/proxmox-demo/Dockerfile.contextruntimehas the same fix. - Deployment templates.
deploy/librechatanddeploy/financebenchno longer default to a known key.deploy/proxmox-demorequiresCONTEXT_RUNTIME_API_KEY
andCR_PUBLIC_READ_KEY; its proxy is an allowlist: it forwards only the reader routes (below), adds the read-only tenant key, and answers every other
/librechat/*and/v1/*path with 404.
MCP client
MCPClient.stdiostarts the server with an allowlisted environment, not a copy of the parent's (merged onmainbefore this release: commitd0440b3, pull
request #67, and its guard test, pull request #68).inherit_env=TrueorCONTEXT_RUNTIME_MCP_INHERIT_ENV=1restores the previous behaviour and logs a
warning. The allowlist is documented in SECURITY.md.
Also in this release (already on main, no behaviour change)
- Version declared as
0.4.0inpyproject.tomlandcontext_runtime.__version__(thev0.4.0tag itself still declares0.2.0), now0.4.1. - Licence wording in
README.mdand a comment and docstring (no code change).
Migration
-
Set a control-plane key before starting:
CONTEXT_RUNTIME_API_KEY=<random value>(for exampleopenssl rand -hex 24). Without it the service starts and
answers503everywhere but/health. -
Send the key from every client:
X-API-Key: <key>orAuthorization: Bearer <key>. This includes dashboards and scripts that read/approvals,/status,
/api/fleetand the like. Browsers cannot add a header on their own: use a proxy or client that adds it. -
LibreChat and other OpenAI-compatible clients set the key as their endpoint
apiKey(they send it as a Bearer token). If you serve several corpus tenants,
give the client a tenant key bound to them inCR_TENANT_KEYSinstead of the control-plane key, for exampleCR_TENANT_KEYS="<read key>=*". -
Public read-only front ends (a browser panel that calls
/librechat/compare): put a proxy in front of the control plane that forwards only the reader routes and
adds a tenant key bound to the public corpora (CR_TENANT_KEYS="<read key>=*"); do not ship a key in page code and never give the proxy the control-plane key.
deploy/proxmox-demo/Caddyfileis an example (an allowlist; anything not listed is 404 at the proxy). What the demo needs:Demo feature Route Class Credential the proxy adds LibreChat model list and chat (LibreChat sends the key itself, server to server) GET /v1/models,POST /v1/chat/completionsreader the read key (LibreChat's endpoint apiKey)Query Board panel (browser, same origin; runs after each answer) POST /librechat/compare,POST /librechat/explainreader the read key Image thumbnails in the panel (an <img>request cannot send a header)GET /librechat/imagereader the read key Retrieval and policy views POST /librechat/retrieve,GET /librechat/policyreader the read key Learning from judge scores, explicit feedback, the self-contained chat loop /librechat/judge,/librechat/feedback,/librechat/chatadmin none: not exposed on the public demo. The shim already learns from implicit signals in /v1/chat/completions, so the demo's learning continues.Corpus ingestion /librechat/ingestadmin none: never exposed through the proxy; run it directly against the control plane with the administrative key Fleet dashboards, approvals, /vibex/*admin routes admin none: not exposed on the public demo (they are not part of it) -
Ingestion: set
CONTEXT_RUNTIME_CORPUS_ROOTto the directory that holds your corpora and call/librechat/ingestwith{"corpus": "<directory name>"}.
Clients that sent an absolute path now send the path relative to the root. Corpora configured withCR_CORPUS_DIR,CR_TENANTSorCR_SHARDSare read at
startup from the paths you configure and are not affected. -
Rebuild the image (
docker build .). If you built your own image from the0.4.0Dockerfile, add theagent-harnessinstall lines from the new one. -
Health checks that parsed
modulesfrom/healthshould check the status code only. -
Stdio MCP servers that relied on an inherited variable need it in
MCPClient.stdio(cmd, env={...})(orinherit_env=True). -
Keep the service on a trusted network or
127.0.0.1as well.
export CONTEXT_RUNTIME_API_KEY="<random value>"
export CONTEXT_RUNTIME_CORPUS_ROOT=/srv/corpora
curl -s http://127.0.0.1:8080/health
curl -s -H "X-API-Key: $CONTEXT_RUNTIME_API_KEY" http://127.0.0.1:8080/approvals
curl -s -H "X-API-Key: $CONTEXT_RUNTIME_API_KEY" -H 'content-type: application/json' \
-d '{"corpus": "finance"}' http://127.0.0.1:8080/librechat/ingestChecking whether a deployment was exposed
On a deployment of <= 0.4.0 that is reachable by others, a 200 answer to GET /approvals or to POST /librechat/ingest without an X-API-Key header means the
routes answered anonymous callers. Rotate any credential that appeared in approval text or job results, and review GET /librechat/policy and the indexed
corpora for content you did not add.
What was verified
- Tested: the route table (every route has an access class; one public route);
503with no key,401with a missing or wrong key and success with the key on every
route; anonymous reads of the formerly open routes; unauthorised writes and approval decisions; tenant keys on thereaderroutes and refused on theadmin
routes; ingestion outside the corpus root, traversal, absolute paths, encoded and Windows-style separators, and symlinked files and directories; the
Dockerfilebuild and start on loopback (/healthanswers, a protected route is503,401,200); the MCP allowlist andCONTEXT_RUNTIME_MCP_INHERIT_ENV=1; the demo proxy: the control plane behind the unmodifieddeploy/proxmox-demo/Caddyfile(Caddy 2.11.7) on loopback, where a client without any credential gets the model list, retrieval, comparison, explanation, policy and chat, while ingestion, judging, feedback, the chat loop and every other control-plane route are refused at the proxy, and the control plane itself answers only/healthto a caller without a credential. - Not tested: a deployment behind your own proxy; the
deploy/proxmox-demoLibreChat image (a stub stood in for it); hard links inside a corpus; a corpus directory that is modified while it is being ingested; Windows; the
deploy/proxmox-democompose stack as a whole; third-party MCP servers; timing differences of the old
comparison (none was measured or claimed).
What this release does not claim
- It is not an external audit or a third-party certification; the review that found these issues was AI-assisted and internal.
- It does not protect a deployment that exposes the credential, shares one key between untrusted clients, or places the service on an untrusted network without
other controls. - Existing indexed corpora, approvals and learned policies are not inspected or cleaned by the upgrade.
Install
pip install "context-runtime @ git+https://github.com/redevops-io/context-runtime.git@v0.4.1"
python -c "import context_runtime; print(context_runtime.__version__)"