Configuration
The installer writes ~/.vectorstep/config/gateway.yaml for you; edit it in
place. Values support ${VAR_NAME} environment variable substitution
throughout, resolved from whatever’s in ~/.vectorstep/.env.
server
Section titled “server”| Field | Default | Description |
|---|---|---|
host |
0.0.0.0 |
Bind address |
port |
18780 |
Bind port. Use a different port from OpenClaw (18789) if running both. |
metrics_auth |
false |
GET /metrics is open by default (Prometheus scrapers connect with no credential); set true to require the invoke/admin bearer token. See Gateway authentication. |
No TLS by default — the Gateway serves plain HTTP/WS until you set this.
Same shape as VectorStep’s own server.tls block (see Deployment →
TLS), deliberately: an operator running
both services shouldn’t have to learn two schemas.
server: host: 0.0.0.0 port: 18780 tls: cert: /etc/vectorstep-gateway/tls/server.crt key: /etc/vectorstep-gateway/tls/server.key key_password: ${VS_TLS_KEY_PASSWORD} # optional, if the key is encrypted client_ca: /etc/vectorstep-gateway/tls/ca.crt # optional; presence enables mTLSAs with VectorStep, startup fails fast — naming the path — if cert is set
without key, or if a configured file doesn’t exist. No certificate
generation or ACME support is built in.
On the VectorStep side, once the Gateway terminates TLS, point
executors.gateway at wss:// and give it what it needs to verify the
Gateway’s certificate:
executors: gateway: url: wss://gateway.internal:18780/rpc token: ${VECTORSTEP_GATEWAY_TOKEN} rest_url: https://gateway.internal:18780 tls: verify: true # default ca: /etc/vectorstep/tls/ca.crt # optional, for a private CA client_cert: /etc/vectorstep/tls/client.crt # optional, for mTLS client_key: /etc/vectorstep/tls/client.keyverify defaults to true. If the Gateway’s certificate is signed by a
private CA (the common case for an internal deployment), set ca to that
CA’s certificate rather than turning verification off. verify: false
disables both hostname and certificate checking and logs a warning on every
connection — it should only ever appear as a deliberate, visible line in
your own config, never a default. The same tls block also governs the REST
calls VectorStep’s UI makes to the Gateway (/agents, /mcp/tools, etc.) —
one block, one behaviour, for both transports.
identity
Section titled “identity”| Field | Default | Description |
|---|---|---|
path |
~/.vectorstep-gateway/identity (native) — the installer’s Docker config sets it to /data/identity instead |
Where identity files are stored. Auto-generated on first run. |
Two tokens — admin and invoke — are minted into <path>/device-auth.json
on first run; see Gateway authentication
for what each grants and who holds it. An identity directory from before this
split (a single operator token) is migrated automatically on load: the
existing token becomes admin and keeps working unchanged, and a new
invoke token is minted alongside it — logged once at startup, since
VectorStep’s own config then needs updating to the new invoke token.
/data/identity sits on the container’s named Docker volume, not its
ephemeral filesystem — the compose file mounts /data there — so a
recreated container keeps its identity, including both tokens VectorStep
and any Gateway MCP client were configured with. See
Docker for the full container config.
limits
Section titled “limits”| Field | Default | Description |
|---|---|---|
max_agent_iterations |
20 |
Max LLM ↔ tool call loops before aborting a run |
request_timeout_seconds |
180 |
Timeout for a single LLM API call |
max_agent_iterations_ceiling |
100 |
Highest max_iterations an agent or a pipeline step may raise its own loop limit to. A larger value is capped at this. Never lowers max_agent_iterations itself. |
request_timeout_ceiling_seconds |
1800 |
Highest per-LLM-call timeout an agent or step may raise request_timeout_seconds to. Also sets the HTTP-client timeout of the providers, so an allowed override is never cut short by the client. |
mcp_tool_timeout_seconds |
30 |
Timeout for a single MCP tool call |
llm_retry_attempts |
2 |
Retries on the same model after a retryable error (429/5xx/529/timeout/connection error) before falling over to the next entry in model_fallbacks |
llm_retry_base_delay_seconds |
1.0 |
Base delay for exponential backoff between retries (doubles each attempt: 1s, 2s, 4s, …) |
max_concurrent_runs |
10 |
Gateway-wide cap on simultaneously executing agent runs. Once at capacity, new requests are still accepted (Frame 1) but queue until a slot frees up. |
trace_tool_result_max_chars |
3000 |
Gateway-wide default cap on a tool_result trace event’s content, truncated with a trailing "… [truncated]" marker. This only affects the trace copy — the streamed/persisted record a caller (or a downstream grounding judge) inspects. The LLM conversation itself always receives the tool’s full, untruncated output regardless of this setting; nothing about the agent’s own reasoning is affected by it. A caller can override this per-request via the agent request’s traceToolResultMax (see the WebSocket protocol) without changing the gateway-wide default. |
mcp_servers
Section titled “mcp_servers”Each key is a server name agents can reference in their tools: list. The
gateway spawns each as a subprocess on startup using stdio JSON-RPC transport.
For the step-by-step on Docker, native Linux and Kubernetes — secrets, restart,
verification and pre-installing servers in your own image — see Adding MCP tool
servers.
mcp_servers: grafana: command: npx args: ["-y", "@grafana/mcp-grafana"] env: GRAFANA_URL: ${GRAFANA_URL} GRAFANA_TOKEN: ${GRAFANA_TOKEN}| Field | Required | Description |
|---|---|---|
command |
Yes | Executable to run (npx, python3, etc.) |
args |
No | Arguments list |
env |
No | Environment variables for the subprocess. Supports ${VAR_NAME} substitution. |
providers
Section titled “providers”Each key becomes a model prefix for routing. Configure only the providers you need. See Providers for how the prefix determines routing and for Azure’s deployment-name specifics.
providers: anthropic: api_key: ${ANTHROPIC_API_KEY} openrouter: api_key: ${OPENROUTER_API_KEY} base_url: https://openrouter.ai/api/v1 ollama-local: base_url: http://localhost:11434/v1 ollama-cloud: api_key: ${OLLAMA_API_KEY} base_url: https://ollama.com/api google: api_key: ${GOOGLE_API_KEY} base_url: https://generativelanguage.googleapis.com/v1beta/openai azure: api_key: ${AZURE_OPENAI_API_KEY} resource_name: ${AZURE_OPENAI_RESOURCE} # e.g. "my-company-openai" api_version: "2025-01-01-preview" # optional, this is the default openai: api_key: ${OPENAI_API_KEY} base_url: https://api.openai.com/v1 # optional, this is the default yolo: api_key: ${YOLO_API_KEY} base_url: https://your-provider.example.com/v1Most providers support api_key and base_url. Azure uses different fields:
| Field | Required | Description |
|---|---|---|
api_key |
No | API key. Empty string = no auth header sent. |
base_url |
No | Override the provider’s default endpoint (not used for azure). |
Azure-specific fields (under providers.azure):
| Field | Required | Description |
|---|---|---|
api_key |
Yes | Azure OpenAI API key from Azure AI Foundry. |
resource_name |
Yes | Azure resource name — the subdomain part of {resource_name}.openai.azure.com. |
api_version |
No | Azure API version. Default: 2025-01-01-preview. |
Default endpoints:
| Provider key | Default endpoint | Notes |
|---|---|---|
anthropic |
SDK default | Uses Anthropic Python SDK natively |
openrouter |
https://openrouter.ai/api/v1 |
OpenAI-compat |
ollama-local |
http://localhost:11434/v1 |
Local Ollama OpenAI-compat endpoint |
ollama-cloud |
https://ollama.com/api |
Native Ollama /api/chat endpoint |
google |
https://generativelanguage.googleapis.com/v1beta/openai |
OpenAI-compat |
azure |
https://{resource_name}.openai.azure.com/openai/deployments/{deployment}/chat/completions |
OpenAI-compat, auth via api-key header |
openai |
https://api.openai.com/v1 |
Native OpenAI API |
yolo |
None — base_url required |
Generic OpenAI-compat custom endpoint, e.g. a self-hosted or third-party API |
logging
Section titled “logging”| Field | Default | Description |
|---|---|---|
level |
INFO |
Log level: DEBUG, INFO, WARNING, or ERROR |
dir |
"" (disabled) |
Directory for rotating log files. Omit to log to stdout only. When set, creates gateway.log (all application logs, 10 MB x 5 rotating) and access.log (uvicorn HTTP access logs, kept separate so request noise doesn’t pollute gateway.log or stdout) — same format and rotation policy as VectorStep’s service.log/access.log split, so both services’ logs can be correlated directly. |
format |
text |
text is one readable line per record; json is one JSON object per line for log shippers. Both carry the trace, run and step fields described in Observability and both scrub known credential shapes. |
observability
Section titled “observability”Controls OpenTelemetry tracing. Disabled by default — all
tracer.start_as_current_span() calls are no-ops until enabled.
observability: otel: enabled: true exporter: otlp # otlp | console endpoint: https://otlp-gateway-prod-eu-west-0.grafana.net/otlp/v1/traces service_name: vectorstep-gateway| Field | Default | Description |
|---|---|---|
enabled |
false |
Enable OTel tracing |
exporter |
otlp |
otlp sends to an OTLP HTTP endpoint; console prints spans to stdout |
endpoint |
http://localhost:4318/v1/traces |
OTLP HTTP endpoint. For Grafana Cloud, use your region’s OTLP gateway URL with a Basic Auth header set via OTEL_EXPORTER_OTLP_HEADERS. |
service_name |
vectorstep-gateway |
service.name resource attribute on all spans |
When OTel is enabled, the gateway emits three span types per agent run:
| Span | Parent | Key attributes |
|---|---|---|
agent.run |
VectorStep gen_ai.gateway span (via W3C traceparent) |
agent.name, gen_ai.request.model, vectorstep.gateway.iterations, vectorstep.gateway.tool_calls, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
llm_call |
agent.run |
llm_call.iteration, llm_call.attempt, gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, llm_call.error/llm_call.retryable (failed attempts only) |
tool_call <name> |
agent.run |
tool.name, tool.is_error |
VectorStep injects a W3C traceparent header into the agent WebSocket request
params, and the gateway extracts it to make agent.run a child of VectorStep’s
pipeline span — giving you a single unified trace across both services in
Grafana Tempo.
Exporting logs over OTLP
Section titled “Exporting logs over OTLP”The Gateway can also send its logs to the same collector, in addition to stdout:
observability: otel: logs: enabled: true # default false endpoint: null # default: the traces endpoint with /v1/traces replaced by /v1/logs level: INFO # may be stricter than logging.levelStdout stays the record: if the collector is unreachable the exporter drops records, so
keep logging to stdout as well. Records use the same service.name as traces and carry
the trace and span IDs, plus vectorstep.run.id, vectorstep.step and
vectorstep.pipeline.name when the service supplied them. Auth headers come from the standard OTEL_EXPORTER_OTLP_HEADERS (or
OTEL_EXPORTER_OTLP_LOGS_HEADERS) environment variable, as for traces. If endpoint
is unset and the traces endpoint does not end in /v1/traces, the Gateway refuses to
start rather than guess a URL. There is no content capture on the Gateway; see
Observability.
Grafana Cloud setup:
- Get your OTLP endpoint from Grafana Cloud portal → Connections → OpenTelemetry
- Set
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <base64(instanceId:apiKey)>in your environment - Enable
observability.otel.enabled: trueand setendpointto your Grafana Cloud OTLP URL
tool_policy
Section titled “tool_policy”Optional. Operator-owned allow/deny rules evaluated on every tool call,
independent of any agent’s own tools: allowlist. Omit it entirely for the
previous unconditional-execution behaviour. See
Tool policy for the full schema, matching
semantics, the audit trail, and default-deny visibility filtering.
tool_policy: default: allow # allow | deny rules: - deny: {server: atlassian, tool: jira_delete_issue} reason: "Destructive Jira operations are operator-only" - allow: {server: grafana}| Field | Default | Description |
|---|---|---|
default |
allow |
Decision when no rule matches |
rules |
[] |
Ordered list — first match wins |
rules[].allow / rules[].deny |
— | Exactly one per rule; match block: server, tool (glob), agent (glob), input_regex |
rules[].reason |
— | Required on deny, optional on allow |
security
Section titled “security”Governs skill script execution — the one piece of the skills feature that is deliberately not reachable through any write API, even though a skill’s content (including bundled scripts, as inert files) is.
skills_dir: ./skills # directory containing skill subdirectories, mirrors agents_dir
security: skills: allow_script_execution: false allowed: - skill: incident-triage path: scripts/classify_severity.py sha256: 3f2a1c9e...| Field | Default | Description |
|---|---|---|
skills_dir |
./skills |
Directory containing skill subdirectories (skills/<name>/SKILL.md), same shape as agents_dir. |
security.skills.allow_script_execution |
false |
Whether run_skill_script is ever offered to an agent, regardless of what’s in allowed. |
security.skills.allowed |
[] |
List of {skill, path, sha256} — the only scripts that can actually run. path is relative to that skill’s own directory (skills/<skill>/), not a shared scripts directory. sha256 is required and is re-verified before every single execution, not just at startup. |
config.yaml is loaded once at gateway startup and is not writable via any
API — same reasoning as tool_policy above, applied to a narrower surface.
POST /reload / SIGHUP reload agent and skill content from disk but never
re-read config.yaml, so approving a new script for execution needs an
actual restart. See Skills — Running a skill’s bundled
scripts for the
full threat model and process containment this ports from VectorStep’s own
executor: shell pipeline steps.
writes_enabled
Section titled “writes_enabled”writes_enabled: false # default: trueWhen false, every agent and skill write endpoint (POST/PUT/DELETE on
/agents and /skills) refuses with 403 and {"type": "writes_disabled"}.
Use it when agents are assembled from a ConfigMap rather than written through
the API, so it fails clearly instead of with a bare 500 from a read-only
filesystem. /agents/validate, /skills/validate, POST /reload and all reads
keep working, and the check runs after the admin-role check. Like the rest of
config.yaml it is read at startup — restart to change it. See Changing config
on Kubernetes.