The maintained reference runtime is a small provider-neutral execution loop. It demonstrates how a model-backed agent behaves as a Kubernetes workload without adopting an agent framework.
The runtime includes a deterministic fake provider plus direct Anthropic
Messages and OpenAI-compatible Chat Completions HTTP transports.
The image bundles two static binaries:
kontext-reporter (PID 1)
└─ kontext-reference
The runtime emits versioned JSONL lifecycle, usage, output, and error events to
stdout. Internally, it gives its final envelope to the bundled reporter through
the KONTEXT_RESULT: capture prefix. The reporter preserves process semantics,
compacts the envelope, and writes the authoritative Pod termination message.
Consumers use AgentRun.status or that termination message for terminal data;
raw logs remain events and diagnostics.
No private chain-of-thought is emitted.
| Environment variable | Required | Meaning |
|---|---|---|
KONTEXT_GOAL |
yes | Concrete task for this run |
KONTEXT_PROVIDER |
yes | fake, anthropic, openai, or openai-compatible |
KONTEXT_MODEL |
yes | Opaque provider model identifier |
KONTEXT_RUN_NAME |
no | Run metadata; defaults to unknown-run |
KONTEXT_AGENT_NAME |
no | Agent metadata; defaults to run name |
KONTEXT_TOOLS |
no | Comma-separated allowlist of built-in or discovered MCP tools |
KONTEXT_MCP_CONFIG |
no | Inline JSON MCP server configuration |
KONTEXT_MCP_CONFIG_FILE |
no | Path to a JSON MCP server configuration file |
KONTEXT_BUDGET_TOKENS |
no | Cumulative provider-reported token limit across all requests |
KONTEXT_BUDGET_WALLCLOCK |
no | Optional Go duration; omitted means disabled |
KONTEXT_BUDGET_DOLLARS |
no | Optional non-negative recorded budget |
KONTEXT_MAX_TURNS |
no | Maximum provider completions in one run |
KONTEXT_MAX_TOOL_CALLS |
no | Maximum executed tool calls in one run |
KONTEXT_MAX_TOOL_RESULT_BYTES |
no | Maximum bytes returned from one tool call |
KONTEXT_MAX_TOTAL_TOOL_OUTPUT_BYTES |
no | Maximum tool-result bytes returned across the run |
KONTEXT_EMIT_TOOL_OUTPUT |
no | Include bounded tool content in events; defaults to false |
KONTEXT_PROVIDER_ENDPOINT |
no | Exact absolute HTTP(S) request endpoint |
KONTEXT_PROVIDER_BASE_URL |
no | Absolute HTTP(S) base URL; provider path is appended |
ANTHROPIC_API_KEY |
Anthropic only | Anthropic API key, normally injected from a Secret |
OPENAI_API_KEY |
OpenAI-compatible only | Bearer token, normally injected from a Secret |
KONTEXT_FAKE_SCENARIO |
no | success, failure, malformed, delay, tool, or test-only tool_sequence |
KONTEXT_FAKE_DELAY |
delay only | Positive Go duration such as 250ms |
KONTEXT_FAKE_TOOL_NAME |
tool only | Deterministic fake-provider tool name |
KONTEXT_FAKE_TOOL_ARGUMENTS |
tool only | Deterministic fake-provider JSON arguments |
KONTEXT_FAKE_TOOL_SEQUENCE |
tool_sequence only | Test-only JSON array of deterministic {name,arguments} calls |
There is deliberately no hidden five-minute deadline. The Kubernetes
controller remains authoritative for KONTEXT_BUDGET_WALLCLOCK; the runtime
parses the value but does not start a competing timer. It reacts promptly when
the reporter forwards controller cancellation signals.
Model identifiers pass through unchanged.
KONTEXT_MCP_CONFIG and KONTEXT_MCP_CONFIG_FILE are mutually exclusive. If
both are set, startup fails instead of choosing one. If neither is set (or the
selected value/file is empty), the runtime starts with no MCP servers.
The MCP document has the form {"servers":[...]} and rejects unknown fields.
Every server needs a unique name and one transport:
stdiorequires an absolutecommand;args, literalenv,envFrom, and a non-negative Gotimeoutare optional.envFrommaps child variable names to runtime variable names. The child receives only these explicit values; the runtime environment is not inherited wholesale.httprequires an absolute HTTP(S)endpointwithout userinfo, query, or fragment. Literalheaders,headersFromEnv, a non-negative Gotimeout, and non-negativemaxRetriesare optional. A configuredmaxRetries: 0disables SDK reconnect attempts; omission keeps the SDK default. Configured headers are injected only for that exact origin, and cross-origin redirects are rejected.
HTTP/MCP-owned and hop-by-hop headers cannot be configured through headers
or headersFromEnv, case-insensitively. This includes Host,
Content-Length, Transfer-Encoding, Connection, Upgrade, Accept,
Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method,
Mcp-Name, Last-Event-ID, Keep-Alive, TE, Trailer, and proxy
authentication/connection headers.
Environment references are resolved before connecting. Missing references, invalid names, malformed durations, transport-specific field mismatches, header newlines, and endpoint credentials fail startup without logging referenced values.
KONTEXT_PROVIDER_ENDPOINT and KONTEXT_PROVIDER_BASE_URL are mutually
exclusive. A base URL may contain a path prefix. The runtime appends
/v1/messages for Anthropic or /chat/completions for OpenAI-compatible
providers. An exact endpoint is used without modification.
The token budget covers the whole run, not one provider request. After each response, the runtime adds that request's reported input and output usage. If the provider also reports a larger total, the runtime uses that total for the budget check. Missing usage is not estimated.
Tool loops resend the goal, prior assistant messages, tool calls, bounded tool results, and tool definitions. Providers therefore count some conversation and tool context again on every follow-up request. Reasoning models may also include reasoning tokens in completion usage even when the visible answer is short.
The runtime sends the remaining budget as the provider's completion limit
where the API supports one, then checks measured cumulative usage after the
response. Provider completion limits do not include every kind of input usage,
so a response may push the measured run total over budget. In that case the
run fails with token_limit_exceeded; the token budget is not a provider-side
hard cap.
Leave headroom for repeated context, tool results, and provider-specific reasoning usage. Exact usage can vary between otherwise identical live runs as models and provider accounting change. The example budgets are starting points, not sizing guarantees.
- Sends one non-streaming
POSTto the Messages API withanthropic-version: 2023-06-01. - Defaults to
https://api.anthropic.com/v1/messages. - Sends
KONTEXT_BUDGET_TOKENSasmax_tokens. Because Anthropic requires that field, the transport uses4096when the budget is omitted. - Normalizes text and client
tool_useblocks; current documented stop reasons; input/output token usage; and therequest-idheader.
openaiandopenai-compatibleuse the same transport.- Sends one non-streaming
POSTto the Chat Completions API and defaults tohttps://api.openai.com/v1/chat/completions. - Treats the base URL as the OpenAI API root (normally ending in
/v1) and appends/chat/completions. - Sends
model, textmessages, and optionalmax_completion_tokens. - Normalizes assistant text, function
tool_calls, documented finish reasons, optional prompt/completion/total usage, optionalcompletion_tokens_details.reasoning_tokens, and thex-request-idheader.
OpenAI completion totals can exceed the tokenized visible text because they
may include hidden reasoning tokens. When the API reports that breakdown, the
runtime preserves it as optional usage.reasoningTokens in the usage event
and final versioned result envelope. It never estimates the value when the
detail is absent, and a reported zero remains distinct from absence.
OpenAI-compatible means compatibility with that request, response, auth, and
tool-call shape. It does not include the Responses API, streaming, embeddings,
realtime transports, Azure deployment URL construction, or vendor-specific
extensions. Endpoint operators are responsible for accepting a bearer token
in Authorization; a non-secret placeholder may be used only when an
in-cluster endpoint ignores auth.
Both transports receive only the tools explicitly listed in KONTEXT_TOOLS.
The runtime executes normalized calls, returns normalized results to the
provider, and continues until final output or a configured limit.
HTTP failures use stable result error codes including
authentication_failed, rate_limited, provider_timeout,
provider_network_error, provider_endpoint_error,
provider_request_rejected, and invalid_provider_response. Retryability,
and provider request IDs are retained where available. HTTP status informs the
normalized error code but is not included in the terminal envelope. Response
bodies are bounded to 4 MiB.
The maintained runtime includes three tools:
read_knowledgereads one UTF-8 file below/kontext/knowledge. Absolute paths, parent traversal, escaping symlinks, directories, and oversized reads are rejected or truncated. The mounted ConfigMap is static context for the run, not production RAG.kubernetes_readperforms current-namespacegetorlistoperations for a fixed resource allowlist. It has no namespace argument and never permits Secrets. Kubernetes RBAC remains authoritative.shellruns/bin/sh -cas the runtime container user in a required absolute working directory. Its direct child environment excludes provider, Kubernetes, and other credential-shaped variables. Stdout and stderr stream to container logs while the result returned to the model remains bounded.
Allowlisting a tool makes it visible to the model; it does not grant new infrastructure permissions. The Pod's ServiceAccount, security context, mounted volumes, filesystem permissions, and NetworkPolicy determine what a tool can actually access. Availability also does not guarantee model use; versioned tool events are the evidence that a call occurred.
Environment filtering is defense in depth, not a Secret boundary. The shell
runs in the same container as the runtime. Filtering changes only the direct
child's environment; it does not isolate files, sibling process metadata, or
other resources available inside that container. Do not expose shell to
workloads that must not access the runtime container's credentials.
Tool errors are returned to the model so it may recover within the remaining limits. A tool error alone does not fail the run. The model may still produce a successful final response. Unknown or non-allowlisted calls follow this path as structured tool errors and are never executed. Cancellation and runtime or provider failures fail the run immediately.
The runtime uses github.com/modelcontextprotocol/go-sdk v1.6.1 for stdio and
Streamable HTTP MCP sessions. It initializes each configured server, follows
all discovery pages, canonicalizes object input schemas, combines the
discovered definitions with built-ins, rejects name collisions, and then
applies KONTEXT_TOOLS. Definitions are frozen for the run.
MCP calls use the same turn, call-count, per-result, cumulative-output, and
wallclock controls as built-ins. Structured content is compact JSON; otherwise
text blocks are joined in order. Image, audio, embedded resource, and resource
link fallback results currently return mcp_unsupported_content; valid
structured content takes precedence over fallback blocks. A fixed 8 MiB
capture ceiling applies before the engine's configured output limits.
HTTP protocol responses stream through a counting body under a 64 MiB wire
ceiling before SDK decoding. This preserves incremental Streamable HTTP/SSE
delivery while leaving room for an 8 MiB normalized result plus JSON/SSE
framing and worst-case JSON escaping. Declared Content-Length is rejected
early; chunked and streaming bodies fail with mcp_http_wire_limit_exceeded
when their count crosses the ceiling. Stdio uses the SDK's IOTransport with
a 64 MiB limit on each newline-delimited frame before SDK JSON decoding; an
oversized frame fails with mcp_stdio_frame_limit_exceeded. Each non-empty
physical line must itself be one complete JSON value; split or multiline JSON
fails with mcp_stdio_invalid_frame. Limits apply per line, not cumulatively
across independent messages. External error text returned to the model or
events is capped at 4 KiB, and values resolved through headersFromEnv or
stdio envFrom are redacted. Structured content is redacted recursively at
string-value level before JSON is re-encoded.
Discovery is bounded before KONTEXT_TOOLS is applied: at most 256 tools,
16 KiB per description, 256 KiB per schema, 280 KiB per complete definition,
and 4 MiB of definitions in total. Tool names must satisfy MCP's 1–128
character [A-Za-z0-9_.-]+ constraint. The runtime retains each resolved
frozen schema and rejects non-object or schema-invalid provider arguments as
mcp_invalid_arguments without contacting or staling the server session.
A protocol or transport failure is returned to the model and marks that session stale. The failed call is never repeated because it may have side effects. Before a later call, the runtime creates a new session and verifies that its complete tool names, descriptions, and schemas match the frozen definitions.
SDK v1.6.1 negotiates MCP protocol 2025-11-25 by default and accepts
2025-06-18, 2025-03-26, and 2024-11-05. The SDK requires Go 1.25; this
repository declares Go 1.26.6. Its runtime dependency footprint adds
google/jsonschema-go, segmentio/encoding, yosida95/uritemplate,
golang.org/x/oauth2, and their small transitive dependencies. Kontext uses
the same JSON Schema package to reject malformed discovered schemas while
returning the original canonicalized keywords to providers.
Sessions are per run. Cleanup uses a fresh bounded context even after
cancellation. Stdio servers run in their own process group; shutdown escalates
from graceful close to TERM and KILL for the full group. A cleanup failure
fails an otherwise successful run with tool_cleanup_failed, but does not
replace an earlier run failure. Servers close concurrently under the shared
cleanup deadline, and HTTP DELETE requests have their own two-second ceiling.
Terminal output is emitted only after cleanup succeeds.
Assistant text from a turn that requests tools is conversation context, not the run's final output. The runtime publishes output only from a terminal provider response with no tool calls and a successful terminal stop reason. If a configured limit is reached before that response, the run fails without promoting earlier assistant text to final output.
Tool events include identity, timing, byte count, error code, and truncation
metadata. Tool content is omitted from events unless
KONTEXT_EMIT_TOOL_OUTPUT=true because event logs may leave the workload's
namespace or retention boundary.
The fake provider's tool_sequence scenario is acceptance-test support, not a
model simulation. It requests one configured call per turn, verifies that each
tool is exposed, gives calls unique deterministic IDs, and emits a terminal
response containing the bounded results in order. Invalid JSON, empty
sequences, missing names or arguments, non-object arguments, and unknown
fields fail provider configuration. The original single-call tool scenario
remains unchanged.
The turn, tool-call, per-result, and total-output limits are configurable run
limits. They are disabled when their environment variables are omitted or set
to 0; examples use finite values. The result limits bound content returned to
the model. Separately, each built-in tool has a fixed 8 MiB capture safety
ceiling. Disabling a configured result limit does not remove that ceiling, and
the per-result setting cannot raise it. Shell stdout and stderr continue to
stream to container logs after model-facing capture reaches the ceiling.
The maintained browser example in
deploy/examples/v1alpha1/reference-playwright-mcp.yaml runs Playwright MCP as
a separate Deployment and Service. It is not an AgentRun sidecar and does
not require browser fields in the Kontext CRDs. The reference runtime connects
to its standalone Streamable HTTP endpoint at /mcp on port 8931.
The example pins Playwright MCP 0.0.78 to the official immutable multiarch
image:
mcr.microsoft.com/playwright/mcp@sha256:3d871c22ea2d4cca0966e2cfb1860e1cb03eb7353725a3d6cffd133296fb04eb
The image is roughly 416 MB compressed and publishes amd64 and arm64 variants.
The vendor does not publish a fixed resource minimum. The example's tested
defaults are a 250m CPU / 512Mi memory request and a 1 CPU / 1Gi memory
limit; these are operational starting points, not vendor guarantees.
The container overrides the entrypoint with node /app/cli.js and passes:
--headless --browser chromium --no-sandbox --isolated --image-responses omit --port 8931 --host 0.0.0.0
Because the server is reached through a Kubernetes Service, the example also
adds --allowed-hosts playwright-mcp.kontext-network-policy-e2e.svc.cluster.local:8931. Playwright
MCP otherwise returns HTTP 403 for that Host header; the example does not use
the unrestricted * value.
The pinned official image disables the Chromium sandbox in this restricted
container setup. Kubernetes isolation and NetworkPolicy therefore remain
security boundaries; Playwright MCP itself is not a security boundary. The
container still runs non-root under restricted Pod Security, drops all
capabilities, forbids privilege escalation, uses RuntimeDefault seccomp, and
has a read-only root filesystem. Explicit bounded emptyDir mounts provide
only /tmp and /dev/shm; HOME, TMPDIR, and XDG paths point into /tmp.
The Playwright ServiceAccount disables token automount and receives no model
provider credentials. Calico policy allows browser egress only to DNS and the
fixture Service, allows MCP ingress only from the named AgentRun Pods, and
allows those Pods to reach only DNS and Playwright. Cloud metadata
169.254.169.254 and unrelated cluster destinations have no allow rule.
NetworkPolicy enforcement is tested only in the Calico-backed
scripts/e2e-kind-network-policy.sh job; kindnet does not enforce these
policies.
Playwright MCP 0.0.78 exposes the acceptance operations
browser_navigate {url}, browser_snapshot {}, browser_type {target,text,...}, and browser_click {target,...}. In this version target
accepts a unique selector, so the deterministic fixture uses stable CSS
selectors (#name and #submit) instead of accessibility snapshot refs.
The fixture keeps its state marker before deterministic accessibility padding.
The primary browser run limits each result to 480 bytes and the cumulative
output to 2,048 bytes, asserts at least one truncated result, and still finds
State: Kontext accepted while tool event content remains omitted. The
wallclock case does not accept early cancellation as evidence: it observes the
second tool-use turn and at least one Chromium process before requiring
BudgetExceeded, runtime Pod deletion, and zero remaining Chromium processes.
The browser server sets PLAYWRIGHT_MCP_PING_TIMEOUT_MS=30000, above the
12-second controller budget, so its heartbeat cannot terminate the active
browser_wait_for first.
KONTEXT_GOAL="Explain the runtime contract" \
KONTEXT_PROVIDER=fake \
KONTEXT_MODEL="any-opaque-model-id" \
go run ./runtimes/referenceBuild the production image:
make docker-build-referenceThe controller reads spec.secretRef and injects only the maintained
transport's expected environment variable. Create Secrets without putting
credentials in source control:
kubectl create secret generic kontext-anthropic \
--from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
kubectl create secret generic kontext-openai \
--from-literal=OPENAI_API_KEY="$OPENAI_API_KEY"MCP authentication can use generic Secret-backed spec.env, then reference
that runtime variable from headersFromEnv or stdio envFrom:
spec:
env:
- name: MCP_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: mcp-credentials
key: tokenThe Secret value is resolved by Kubernetes directly into the runtime container. It is not copied into the AgentRun object.
The focused Secret-auth integration test builds the runtime Pod environment
from a generic spec.env.valueFrom.secretKeyRef, verifies the resulting
variable name is the one referenced by headersFromEnv, simulates kubelet
resolution of that variable, and uses it against a real in-process
authenticated Streamable HTTP MCP server. It also verifies resolved values are
redacted from returned errors and captured stderr. This is an integration of
Pod construction, runtime configuration, MCP authentication, and redaction;
it does not create a Kubernetes Secret or run the authenticated fixture in
kind. The keyless Playwright kind scenario separately covers real Pods,
NetworkPolicy, and browser lifecycle without introducing credentials.
Example templates live in deploy/examples/v1alpha1/:
provider-secrets.example.yamlreference-anthropic-run.yamlreference-openai-compatible-run.yamlreference-fake-tool-run.yamlreference-tools-run.yamlreference-playwright-mcp.yamlandreference-playwright-*-run.yaml
The Provider acceptance GitHub Actions workflow is dispatch-only and uses
the provider-acceptance environment. Repository maintainers must protect
that environment with required reviewers and store ANTHROPIC_API_KEY and
OPENAI_API_KEY as environment secrets. Pull-request CI never receives or
uses these credentials.
Each dispatch builds the maintained operator and reference runtime, loads them
into an ephemeral kind cluster, and injects only the selected provider key
through a namespace-local Secret. The default tool scenario mounts a tiny
ConfigMap, exposes only read_knowledge, and requires exactly one call before
an exact final response. Acceptance requires exactly one matching tool event
with count: 1, isError: false, and no error code. The text scenario
preserves the ordinary one-turn transport check.
The tool scenario permits at most two provider turns, one tool call, 256 bytes per tool result and in total, 2,048 cumulative measured tokens, and 90 seconds of wallclock time. A normal dispatch should stay below roughly 2,000 provider tokens, but this is not a hard billing ceiling: usage is checked only after a response, provider token accounting varies, and the repeated tool context can cross the configured budget. Before approval, select a small non-reasoning model, verify its current pricing and the endpoint, and do not use this workflow for untrusted code. Failure artifacts omit workload logs, Agent/AgentRun descriptions, and cluster event summaries that can contain runtime-specific values. Remaining controller and cluster diagnostics are retained briefly; maintainers should still review them before sharing. The model and endpoint inputs are visible workflow metadata. The acceptance script rejects endpoint userinfo, queries, fragments, and whitespace so credentials cannot be embedded there.
The script writes a bounded kontext.dev/eval/v1alpha1
ProviderAcceptanceRecord on success and safe failure paths. It includes the
provider, opaque model, scenario, commit/run identity, phase, duration,
measured usage, turns, tool calls, and pass/fail only. It excludes API keys,
Secret values, raw logs, and model output. The workflow uploads this record on
success or failure with short retention.
For each release, dispatch the workflow for every maintained transport being
published, approve the protected environment, require a passing run, and
retain the provider-acceptance-<run>-<attempt> artifact with the release
evidence. Without those protected credentials, no local or keyless run counts
as authenticated acceptance. Immutable versioned image publication remains a
separate release step.
The reference binary uses the official MCP Go SDK v1.6.1 plus the standard
library and in-repository Kontext packages. The bundled reporter additionally
uses golang.org/x/sys for Linux process supervision. The production image
contains:
gcr.io/distroless/static:nonroot- BusyBox
shand applets copied into the final image for the allowlisted shell tool /kontext-reporter/kontext-reference
It contains no package manager, agent framework, retrieval system, or persistent storage.
- Provider turns and tool calls continue only within configured limits.
- Conversation state exists only in memory for that run.
- Tool results are bounded and full shell output remains in container logs.
- No automatic provider or tool-call retries, planning, memory, retrieval, subagents, background work, or provider-specific model aliases. MCP transport reconnection never repeats a failed tool call.