Security Hardening¶
This page is for production operators deploying the Agentic Workflows V2 FastAPI server. It describes every in-process security control, its environment-variable knobs, its default behavior, and practical recipes for tuning it to your environment.
This page is NOT an ADR, a code specification, or a threat model. For decision rationale
see the linked ADRs; for the formal threat model see
docs/OWASP_LLM_THREAT_MODEL.md
(excluded from the built site). For the complete env-var
index see the Configuration Reference.
At a glance¶
| Control | Environment variable(s) | Default | Sprint 1 reference |
|---|---|---|---|
| API key authentication | AGENTIC_API_KEY |
Not set — all routes open | ADR-018 |
| Global rate limiting | AGENTIC_RATE_LIMIT_DEFAULT, AGENTIC_RATE_LIMIT_DISABLED |
60/minute per IP |
ADR-018 |
| 401 brute-force throttle — window | AGENTIC_AUTH_LOCKOUT_WINDOW_SECONDS |
60 seconds |
ADR-018 |
| 401 brute-force throttle — threshold | AGENTIC_AUTH_LOCKOUT_THRESHOLD |
5 failures |
ADR-018 |
| 401 brute-force throttle — lockout | AGENTIC_AUTH_LOCKOUT_DURATION_SECONDS |
300 seconds |
ADR-018 |
| DAG workflow timeout | No env var — set per call via timeout kwarg |
None (unlimited) | ADR-019 |
| Adapter eager validation | AGENTIC_DEFAULT_ADAPTER |
langchain |
ADR-020 |
| Filesystem sandbox | AGENTIC_FILE_BASE_DIR |
Not set — file tools refuse all operations | — |
| Shell tool allowlist | AGENTIC_SHELL_ALLOWED_COMMANDS |
Not set — all shell commands disabled | — |
| Sanitization fail-closed | AGENTIC_SANITIZER_FAIL_OPEN |
Not set — 503 returned when layer unavailable | — |
| CORS origins | AGENTIC_CORS_ORIGINS |
localhost dev ports (5173, 8000, 8010) | — |
| Block private IPs in HTTP tool | AGENTIC_BLOCK_PRIVATE_IPS |
Not set — private IPs blocked (default ON; set to 0 to opt out) |
P1 #13 |
1. API key authentication¶
How it works¶
When AGENTIC_API_KEY is set in the environment, the APIKeyMiddleware (layer 3 in the
middleware stack, see §Middleware order below) gates every request whose path starts with
/api/. The key must be supplied via one of two headers:
Token comparison uses secrets.compare_digest() to prevent timing side-channel attacks.
The key is re-read from the environment on every request (via _get_api_key() →
get_secret("AGENTIC_API_KEY")). This means you can rotate the key without restarting the
server — new requests will immediately use the updated value.
When AGENTIC_API_KEY is not set, the middleware is a no-op and all routes are publicly
accessible. The server logs a warning at startup:
Never run without a key in internet-exposed environments
A server without AGENTIC_API_KEY accepts unauthenticated workflow runs, which may
execute arbitrary LLM calls against your provider quotas.
Public path exemptions¶
The following paths bypass authentication regardless of whether a key is set:
/api/health/docs/openapi.json/redoc
All other paths under /api/ require a valid key. Non-API routes (UI static files, WebSocket
upgrade) also bypass authentication so the React frontend can load without credentials.
WebSocket origin validation¶
WebSocket connections go through a separate origin check (is_websocket_origin_allowed()).
A connection is allowed when any of the following is true:
- The
Originheader is absent (non-browser clients such as curl or SDK calls). - The origin matches the HTTP
Hostheader of the request exactly. - The origin hostname is
localhost,127.0.0.1, or::1(any port — Vite dev server uses dynamic ports). - The origin appears in the
AGENTIC_CORS_ORIGINSallowlist (see §9).
Recipe: rotate the key without a restart¶
# Update the secret in your environment manager (e.g., Kubernetes secret, AWS Secrets Manager)
# then export in the server's process environment. No restart needed — auth.py reads the
# env var on every request.
export AGENTIC_API_KEY="$(openssl rand -hex 32)"
Because the key is re-read per request, the old key stops working the moment the environment variable changes. Brief request-level races (in-flight requests that already read the old key at header extraction) complete normally; the new key is enforced on the next request.
2. Global rate limiting¶
How it works¶
The server uses slowapi for per-IP global rate limiting.
slowapi is built on the limits library and is applied at the outermost middleware layer
(layer 1), before auth and sanitization, so abusive clients are rejected before any auth work
is done.
The key function is remote_address — the client's IP as seen by the server process. Every
unique IP is tracked independently in an in-process memory store.
Default limit: 60/minute per IP. Configurable via AGENTIC_RATE_LIMIT_DEFAULT.
Response on breach:
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds-to-next-window>
Content-Type: application/json
{"detail": "Rate limit exceeded: <detail>"}
Public paths are exempt. /api/health, /docs, /openapi.json, and /redoc return
None from the key function, which instructs slowapi to skip enforcement.
Disabling for tests¶
Do not disable in production
AGENTIC_RATE_LIMIT_DISABLED=1 removes all per-IP rate gating. Use only in isolated
test environments.
Middleware order¶
The server middleware stack (outermost to innermost, i.e., the order in which an inbound request passes through each layer) is:
1. SlowAPIMiddleware — global per-IP rate limit
2. SanitizationASGIMiddleware — prompt-injection / secret redaction
3. APIKeyMiddleware — bearer-token auth + per-IP 401 throttle
4. CORSMiddleware — CORS preflight / header injection
app.add_middleware prepends each layer, so the first add_middleware call (CORS) becomes
the innermost layer and the last call (SlowAPI) becomes the outermost.
Tuning recipes¶
Low-traffic development environment:
Relaxes the cap to avoid friction during development while still exercising the rate-limit code path.
High-traffic production behind a load balancer:
Multi-replica caveat
The rate-limit counter is in-process. In a multi-replica deployment behind a load
balancer, each replica tracks only the requests it receives. The effective per-IP rate
cap is N × configured_limit where N is the number of replicas. A caller can bypass
the limit by distributing requests across replicas.
The planned Sprint 2 Redis migration will replace the in-process store with a shared counter, making the limit cluster-wide. Until then, enforce rate limiting at the ingress / API-gateway layer for multi-replica deployments.
Reference
See ADR-018 for the design rationale and the Sprint 2 Redis migration plan.
3. 401 brute-force throttle (AuthThrottle)¶
How it works¶
AuthThrottle (in server/auth.py) tracks failed authentication attempts per client IP
using monotonic timestamps. It is integrated into APIKeyMiddleware and runs at layer 3.
Sliding window: Only failures within the last AGENTIC_AUTH_LOCKOUT_WINDOW_SECONDS
seconds count toward the threshold. Failures older than the window are evicted on the next
request from that IP.
Defaults (verified against server/auth.py):
| Parameter | Default | Override |
|---|---|---|
| Sliding window | 60 seconds |
AGENTIC_AUTH_LOCKOUT_WINDOW_SECONDS |
| Failure threshold | 5 failures |
AGENTIC_AUTH_LOCKOUT_THRESHOLD |
| Lockout duration | 300 seconds |
AGENTIC_AUTH_LOCKOUT_DURATION_SECONDS |
Lockout semantics¶
When an IP reaches the threshold, the AuthThrottle sets a locked_until timestamp. Every
subsequent request from that IP — including the threshold-triggering request — receives:
HTTP/1.1 429 Too Many Requests
Retry-After: <remaining-lockout-seconds>
Content-Type: application/json
{
"detail": "Too many failed authentication attempts. Please retry later.",
"retry_after": <remaining-lockout-seconds>
}
The lockout is checked before credential verification. Locked IPs are rejected before
the server even reads the Authorization or X-API-Key header, so lockout cannot be
cleared by supplying a correct key during the lockout period.
Successful authentication clears the failure counter. A valid key from a previously failing IP resets its failure history entirely.
Eviction¶
The _evict_expired() method cleans up state entries lazily on each request — no background
task is needed. An IP's entry is removed when its lockout has expired and its failure
window has also expired.
In-process limitation¶
Multi-replica caveat
AuthThrottle state is per-process. In a multi-replica deployment, a distributed
attacker who spreads probes across replicas can stay under each replica's threshold while
collectively exceeding the intended lockout threshold.
See KNOWN_LIMITATIONS.md §4.2. Sprint 2 will migrate to a Redis-backed shared store.
Recipe: investigate a locked-out client¶
The server logs a warning for every lockout event and every rejected locked-IP request:
WARNING: Auth throttle: IP 203.0.113.42 locked out for 300 seconds after 5 failures
WARNING: Auth throttle: rejecting locked IP 203.0.113.42 (retry after 287s)
To find lockout events for a specific IP:
# journalctl / docker logs / file log depending on your deployment
grep "locked out\|locked IP" server.log | grep "203.0.113.42"
Authentication failures (before lockout) are also logged:
Recipe: lowering thresholds for high-value endpoints¶
The threshold and lockout duration currently apply globally across all endpoints. If you need tighter thresholds, lower the environment variables:
AGENTIC_AUTH_LOCKOUT_WINDOW_SECONDS=30
AGENTIC_AUTH_LOCKOUT_THRESHOLD=3
AGENTIC_AUTH_LOCKOUT_DURATION_SECONDS=900
Note
Per-endpoint throttle granularity is listed as future work in ADR-018. Today the configuration is global.
4. DAG workflow timeout¶
Where it applies¶
DAGExecutor.execute(workflow, ..., timeout=N) accepts an optional timeout parameter
(seconds, float). When set, the entire scheduling loop is wrapped with
asyncio.wait_for(scheduling_loop(), timeout=N).
There is no environment variable for the default timeout. The value is set per call by the server route layer. Today this means you configure it by adjusting the route-level call site — it is not a startup configuration knob.
What happens on timeout¶
When the timeout elapses:
- All in-flight
asyncio.Taskobjects for running steps are cancelled (task.cancel()) and awaited to prevent "Task was destroyed but it is pending" warnings. - Every step still in
RUNNINGstate is transitioned toFAILEDwitherror_type: "TimeoutError"and a descriptive message. - All transitive dependents of those failed steps are cascade-skipped via BFS (the same propagation used for step-level failures).
- Steps that never started are also skipped.
- The returned
WorkflowResulthasoverall_status=FAILED. result.metadatacontains:
OTEL attributes emitted on the active span:
workflow.timeout_exceeded = True
workflow.timeout_seconds = <N> # on the span event "workflow.timeout"
These attributes are queryable in Jaeger/Tempo dashboards to identify timed-out runs.
Relationship to per-step timeouts¶
The workflow-level timeout and per-step timeouts in StepExecutor are additive, not
mutually exclusive. Both can be active simultaneously:
- A step's individual timeout fires if that step alone runs too long.
- The workflow timeout fires if the cumulative execution across all steps (including dependencies and waiting time) exceeds the ceiling.
Set a tight per-step budget for individual agent calls and a looser workflow ceiling as a backstop.
Tuning guidance¶
A reasonable starting point:
For example, if your most complex workflow takes about 4 minutes under normal conditions, set a 6-minute (360 second) timeout. This gives headroom for provider latency spikes while still bounding runaway workflows.
Note
Cooperative cancellation via asyncio.wait_for depends on await points in step
bodies. A tightly-looping synchronous step may not cancel promptly. If you have steps
with synchronous-heavy bodies, consider adding explicit cancellation checkpoints.
Reference
See ADR-019 for the design rationale
and the follow-on task to expose timeout as a first-class execution_profile HTTP field.
5. Adapter eager validation¶
How it works¶
AGENTIC_DEFAULT_ADAPTER selects which execution engine the server validates at startup.
The default is langchain.
During the FastAPI lifespan startup sequence, AdapterRegistry.validate_selected(name) is
called. For the langchain adapter, this immediately attempts to import langchain and
langgraph. If either import fails (the optional extras are not installed), a
ConfigurationError is raised:
ConfigurationError: LangChain engine selected but extras not installed.
Install with: pip install -e '.[langchain]'
uvicorn logs the error and exits with a non-zero code. The server does not start, and no requests are served. This fail-fast behavior is intentional — a misconfigured deployment fails at boot rather than mid-workflow.
For AGENTIC_DEFAULT_ADAPTER=native (and any other adapter value that is not langchain),
validate_selected() is a no-op.
Recipe: deploy with native engine only (no LangChain extras)¶
Set this when:
- You have not installed the
[langchain]extras. - You want to minimize the dependency footprint.
- All workflows in your deployment explicitly specify
adapter: native.
Warning
Setting AGENTIC_DEFAULT_ADAPTER=native bypasses the LangChain check but does not
prevent workflows from requesting the langchain adapter at run time. If a workflow
request specifies adapter: langchain and the extras are absent, the error surfaces at
request time rather than boot time.
Reference
See ADR-020 for the rationale and the CLI pre-flight check planned as a follow-on.
6. Filesystem sandboxing¶
AGENTIC_FILE_BASE_DIR¶
The file_ops built-in tool uses this variable to establish a sandbox root. All file
operations are resolved relative to this directory, and any attempt to traverse outside it
(e.g., via ../) is rejected at the validator boundary.
Fail-closed
When AGENTIC_FILE_BASE_DIR is not set, all file tool operations are refused. File
tools will return an error on every call until this variable is configured.
Operators who need file operations must set this variable. Leaving it unset is the safe default — no accidental file access can occur.
Recommended value: a dedicated, isolated directory that the server process can write to and that does not contain sensitive system files or application secrets.
# Container deployments
AGENTIC_FILE_BASE_DIR=/app/data
# Bare-metal / VM deployments
AGENTIC_FILE_BASE_DIR=/var/lib/agentic/data
Ensure the directory:
- Exists and is writable by the server process user.
- Does not overlap with application code, secrets, or system directories.
- Has appropriate filesystem-level permissions (mode
700or750for the server user).
7. Shell tool allowlist¶
AGENTIC_SHELL_ALLOWED_COMMANDS¶
The shell_ops built-in tool executes shell commands inside the server process. By default
(when the variable is not set or is empty), all shell commands are disabled.
Fail-closed
An empty or missing AGENTIC_SHELL_ALLOWED_COMMANDS means no shell commands can run.
This is the safe default.
When set, the value is a comma-separated list of command basenames (the executable name without path). Only commands whose basename appears in this list are allowed to execute.
# Disable entirely (default — omit the variable or set it to empty)
AGENTIC_SHELL_ALLOWED_COMMANDS=
# Allow only specific commands
AGENTIC_SHELL_ALLOWED_COMMANDS=ls,cat,python,git
Minimum set for code-execution workflows
The following covers most code-execution workflow patterns while keeping the attack surface narrow:
Add additional commands only when a specific workflow requires them. Never include interpreters (bash, sh, zsh) unless the workflow explicitly requires a shell
runner and you have reviewed the associated risk.
8. Sanitization middleware¶
How it works¶
SanitizationASGIMiddleware (layer 2 in the middleware stack) inspects the JSON body of
every inbound request. It applies the SanitizationMiddleware instance stored in
app.state.sanitization (initialized in the FastAPI lifespan with dry_run=False).
Classification outcomes:
| Classification | Action |
|---|---|
clean |
Request passes through unmodified. |
requires_approval |
Request passes through unmodified (advisory only in current release). |
redacted |
Request body replaced with sanitized text before passing downstream. |
blocked |
422 Unprocessable Entity returned immediately. |
Fail-closed behavior¶
Fail-closed by default
When the sanitization layer fails to initialize (or is explicitly None), the middleware
returns 503 Service Unavailable unless AGENTIC_SANITIZER_FAIL_OPEN=1 is set.
On any unexpected exception during sanitization processing, the middleware returns
500 Internal Server Error rather than passing the request through unsanitized.
Only JSON bodies (Content-Type: application/json) are inspected. All other content
types pass through unchanged.
AGENTIC_SANITIZER_FAIL_OPEN¶
Only the literal 1 enables fail-open
Since 2026-05-09, only the string "1" is accepted as enabling fail-open. Earlier
versions accepted "true", "yes", and other truthy strings. If your deployment
scripts set AGENTIC_SANITIZER_FAIL_OPEN=true, update them to
AGENTIC_SANITIZER_FAIL_OPEN=1.
Setting AGENTIC_SANITIZER_FAIL_OPEN=1 causes the middleware to pass requests through
when the sanitization layer is unavailable or encounters an error, rather than returning
503/500.
Not for production
AGENTIC_SANITIZER_FAIL_OPEN=1 disables the prompt-injection and secret-redaction
safety net when the sanitization layer is unavailable. Use only for debugging in
isolated environments. Do not set this in production.
Recipe: legacy debugging mode¶
# Temporarily enable fail-open to diagnose sanitization startup failures
AGENTIC_SANITIZER_FAIL_OPEN=1 python -m uvicorn agentic_v2.server.app:app ...
# After diagnosing, remove the variable and fix the root cause.
9. CORS¶
AGENTIC_CORS_ORIGINS¶
Controls which browser origins the server includes in Access-Control-Allow-Origin response
headers (via FastAPI's CORSMiddleware, the innermost middleware layer).
Default origins (when AGENTIC_CORS_ORIGINS is not set):
http://localhost:5173
http://127.0.0.1:5173
http://localhost:8000
http://127.0.0.1:8000
http://localhost:8010
http://127.0.0.1:8010
These defaults cover the local Vite dev server and common local API ports. They are not appropriate for production.
Production deployments must restrict CORS origins
Set AGENTIC_CORS_ORIGINS to the actual frontend origin(s) in every production
deployment. Leaving the default allows any localhost browser session to make
credentialed cross-origin requests.
# Single frontend origin
AGENTIC_CORS_ORIGINS=https://app.example.com
# Multiple origins (comma-separated, no spaces)
AGENTIC_CORS_ORIGINS=https://app.example.com,https://admin.example.com
WebSocket origin validation¶
WebSocket origin validation reuses the same AGENTIC_CORS_ORIGINS allowlist via
get_allowed_origins(). Additionally, any localhost or 127.0.0.1 origin (any port) is
always allowed for WebSocket connections — the Vite dev server starts on dynamic ports
(5173, 5174, …) so they cannot be enumerated statically.
Non-browser clients that omit the Origin header are always allowed for WebSocket
connections.
10. HTTP tool SSRF protection¶
AGENTIC_BLOCK_PRIVATE_IPS¶
The http_ops built-in tool (and langchain/tools.py) make outbound HTTP requests
during workflow execution. As of P1 #13, SSRF protection is enabled by default and
covers:
- Private/loopback/link-local/reserved IP literals (RFC 1918, RFC 4193, etc.)
- DNS names: all returned addresses are resolved and checked — a hostname that resolves to a private address is blocked even if it looks public.
- Cloud metadata endpoints:
169.254.169.254,fd00:ec2::254,100.100.100.200,metadata.google.internalare always blocked regardless of the flag. - Redirect re-validation: each redirect hop is validated before following (max 5 hops).
Per RFC 7538/9110,
307/308hops preserve the original method and body;301/302/303degrade to a bodylessGET. Caller headers (e.g.Authorization) are sent on the first hop only, so credentials are never replayed to a redirect target. - IPv4-mapped IPv6 (
::ffff:127.0.0.1) is normalised before checking. - Legacy IPv4 literal forms the OS resolver accepts (decimal
2130706433, hex0x7f000001, octal0177.0.0.1, short127.1) are parsed and blocked — they cannot bypass the string-based check.
DNS-rebinding residual risk¶
The guard validates the hostname's resolved addresses and pins the connection to a validated address so the HTTP client cannot independently re-resolve to a different (private/metadata) IP between the check and the connect:
- aiohttp path (
http_ops): aGuardedResolverre-validates every address aiohttp is about to dial at connect time. - httpx path (
langchain/tools):validate_url_pinnedrewrites the request URL to the validated IP, carrying the real hostname in theHostheader (andsni_hostnameextension for TLS), so the connection goes to the address that was checked.
Residual: both mitigations rely on getaddrinfo/the resolver returning the same answer the
guard validated. A sufficiently hostile authoritative DNS server with a near-zero TTL could, in
principle, return a different address to the connect-time lookup than to the validation lookup on
a path not covered by pinning, and OS-level resolver caching is not under the application's
control. For threat models that include attacker-controlled DNS, treat the application guard as
defense-in-depth and add a network-layer egress control (egress firewall / service-mesh
authorization / network policy) restricting which addresses the server process may reach. See
docs/KNOWN_LIMITATIONS.md.
SSRF defense
In environments where the server process has network access to internal services (databases, metadata APIs, internal dashboards), an unauthenticated or malicious workflow could exploit the HTTP tool to make requests to those services. The SSRF guard is on by default — this is a security-critical default. Do not disable it without applying compensating controls.
# On by default — opt out only with explicit justification
# AGENTIC_BLOCK_PRIVATE_IPS=0 # disables private-IP blocking (NOT recommended)
Set AGENTIC_BLOCK_PRIVATE_IPS=0 only when workflows legitimately need to reach internal
network endpoints and you have applied other controls (network policy, service mesh
authorization) to restrict which internal addresses the server can reach. Note that
cloud metadata endpoints (169.254.169.254 etc.) remain blocked even when the flag
is set to 0.
11. Human approval gates¶
As of P1 #12, high-impact tools no longer execute the instant the LLM emits a call: the
tool-execution hot path consults an injectable approval gate first, at both dispatch
points (the engine tool loop and the agent loop). The gate is implemented in
agentic_v2/governance/approval.py.
Contract¶
A tool requires approval when any of these is true (the triggers are OR'd):
- The tool opts in via
requires_approval = Trueon its class. - The global override
AGENTIC_REQUIRE_TOOL_APPROVAL=1is set (gates every tool). - The tool's name appears in
AGENTIC_APPROVAL_REQUIRED_TOOLS(comma-separated).
When a tool requires approval, the registered ApprovalProvider decides. The decision is
returned to the model as a normal (failed) tool result, so a denied call does not crash the
run — the model sees the denial and can adapt.
Fail-closed rule¶
If a tool requires approval and no provider is registered, the call is DENIED and never executes. There is no implicit "allow when unconfigured" path. To run gated tools you must either register a provider or remove the requirement. The denial error tells the operator exactly that.
Default posture change¶
The global override defaults OFF so existing flows are not broken. The real default posture change is the per-tool flag: the following high-impact builtins are gated by default, so a deployment that registers no provider will have these tools fail closed until one is wired:
| Tool name | Why gated |
|---|---|
shell, shell_exec |
Arbitrary command execution |
execute_python |
Arbitrary code execution |
file_write, file_delete, file_move, file_copy, directory_create |
Filesystem mutation |
http, http_post |
State-changing network requests |
Read-only tools (file_read, http_get, search/transform/context ops) are not gated.
Provider examples¶
from agentic_v2.governance import (
AutoApproveProvider,
AutoDenyProvider,
CallbackApprovalProvider,
PolicyApprovalProvider,
set_approval_provider,
)
# Trusted, non-interactive environment: approve everything (use with care).
set_approval_provider(AutoApproveProvider())
# Hard kill-switch: deny every gated call.
set_approval_provider(AutoDenyProvider())
# Allowlist by tool name.
set_approval_provider(PolicyApprovalProvider(frozenset({"file_write"})))
# Interactive / custom: a sync-or-async callable decides per request.
async def decide(request):
from agentic_v2.governance import ApprovalDecision
# request.tool_name, request.call_id, request.agent_or_step are available;
# request.tool_args values longer than ~200 chars are redacted in its repr.
return ApprovalDecision.APPROVED if request.tool_name == "file_write" else ApprovalDecision.DENIED
set_approval_provider(CallbackApprovalProvider(decide))
The provider is a process-wide module global — set it once at process start. Approval policy is an application-level posture decision; it is not bound per-task.
UI-driven pause/resume is a follow-on, not built yet
This release ships the programmatic gate: an injectable provider consulted
synchronously on the hot path. A full UI-driven pause-and-resume flow — where a run
suspends, surfaces an approval prompt to a human operator in the web UI, and resumes on
their click — is an explicit follow-on. The wire-format events
(approval_required, approval_decision in contracts/events.py) already exist so the
server/UI can build on them, but the engine tool loop currently surfaces approval activity
via the logger and the serialized result metadata rather than streaming those events. See
KNOWN_LIMITATIONS.md §4.3.
Reference deployment posture¶
The following block represents the minimum recommended configuration for a hardened, internet-exposed production instance. Copy and adapt it to your environment.
# ── Authentication ────────────────────────────────────────────────────────────
# Generate with: openssl rand -hex 32
AGENTIC_API_KEY=<32-byte-random-hex>
# ── CORS ─────────────────────────────────────────────────────────────────────
AGENTIC_CORS_ORIGINS=https://app.example.com
# ── Filesystem sandbox ────────────────────────────────────────────────────────
AGENTIC_FILE_BASE_DIR=/app/data
# ── Shell tool ────────────────────────────────────────────────────────────────
# Empty = all shell commands disabled (recommended unless workflows require shell)
AGENTIC_SHELL_ALLOWED_COMMANDS=
# ── Engine ───────────────────────────────────────────────────────────────────
# Use native engine to avoid LangChain extras requirement.
# Remove or set to langchain if LangChain extras are installed.
AGENTIC_DEFAULT_ADAPTER=native
# ── SSRF defense (default ON — no action required for production) ────────────
# AGENTIC_BLOCK_PRIVATE_IPS is True by default. Unset or set to 1 to keep the
# default. Set to 0 only if workflows must reach internal services and
# compensating network controls are in place.
# AGENTIC_BLOCK_PRIVATE_IPS=0 # opt-out — not recommended
# ── Human approval gates (§11) ───────────────────────────────────────────────
# High-impact builtins (shell/exec, file writes/deletes, http/post) are gated by
# default and FAIL CLOSED with no provider registered. Register an
# ApprovalProvider at process start (see §11) before relying on those tools.
# Optionally gate EVERYTHING, or extra tools by name:
# AGENTIC_REQUIRE_TOOL_APPROVAL=1 # gate every tool call
# AGENTIC_APPROVAL_REQUIRED_TOOLS=git_commit,deploy # extra tools by name
# ── Rate limiting ─────────────────────────────────────────────────────────────
# Tune based on expected traffic. Lower for higher-security, lower-traffic
# environments. Raise with caution — in-process only, see §2.
AGENTIC_RATE_LIMIT_DEFAULT=30/minute
# ── Auth brute-force throttle ─────────────────────────────────────────────────
# Window (seconds), threshold (failures), lockout (seconds)
# Defaults are 60 / 5 / 300; values below are more aggressive for production.
AGENTIC_AUTH_LOCKOUT_WINDOW_SECONDS=60
AGENTIC_AUTH_LOCKOUT_THRESHOLD=5
AGENTIC_AUTH_LOCKOUT_DURATION_SECONDS=600
# ── Sanitization ──────────────────────────────────────────────────────────────
# Leave AGENTIC_SANITIZER_FAIL_OPEN unset (fail-closed is the safe default).
# AGENTIC_SANITIZER_FAIL_OPEN= # do not set
# ── Observability ─────────────────────────────────────────────────────────────
AGENTIC_TRACING=1
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
Secret generation
Use a cryptographically strong random value for AGENTIC_API_KEY:
See also¶
- Configuration Reference — full environment variable index with types, defaults, and accepted values
- ADR-018 — rate limiting and auth throttle design rationale, Sprint 2 Redis migration plan
- ADR-019 — DAG executor top-level timeout watchdog design
- ADR-020 — LangChain adapter eager validation at startup
- KNOWN_LIMITATIONS.md — in-process caveats, multi-replica behavior (§4.1, §4.2), and deferred Sprint 2 work
- Troubleshooting — common failure modes and diagnostic recipes
- Deployment Guide — CI/CD pipeline, Docker, environment setup, and production observability checklist