Skip to content
kazma.
Search Kazma Documentation & Site
ع

Security & Safety

Kazma’s safety model: three HITL execution paths + one gate registry, default-deny tool tiers, fail-closed bus, CSRF / trusted-proxy, SSRF pin-IP, HMAC skills, and hardening. Invariants: AGENTS.md §7, §26, §30–§32.

Also see: Document security — hostile intake, the two worker isolation profiles (process, which has no filesystem or network jail, and the attested per-job container profile), fencing, malware scan, remote-parse egress, redaction model for Document Intelligence.


1. Three HITL execution paths + one registry

Section titled “1. Three HITL execution paths + one registry”

Kazma has three independent HITL execution paths. Each covers a different tool-running path. Breaking any one creates an unattended-danger-tool gap. Decision truth is a fourth piece: the HITL Gate Registry (kazma-data/hitl_gates.db). Execution truth remains the LangGraph checkpoint (or pipeline SQLite). Surfaces render; they never infer Approved from a missing row.

flowchart TB
    subgraph "Path A: Graph interrupt (single-agent chat)"
        A1[graph_tool_worker.tool_worker_node] -->|danger + hitl_config| A2[interrupt]
        A2 --> REG[hitl_gates.db CAS]
        REG -->|resume Command| A3[execute with _hitl_approved]
    end
    subgraph "Path B: Swarm bus (/swarm + IDE execute)"
        B1[LocalToolRegistry.execute] --> B2[SafetyMiddleware.check]
        B2 -->|register_gate| REG
        B2 --> B3[bus.request_approval]
        B3 -->|FanOut tri-state| B4[claim + settle]
    end
    subgraph "Path C: Pipeline checkpoints"
        C1[pipeline HITL step] --> C2[CheckpointManager]
        C2 --> REG
        C2 -->|approve_checkpoint| C4[resume_pipeline]
    end
PathWhere it livesApproval surface
Aagent/graph_tool_worker.py (wired from graph_builder.py)POST /api/approve/{thread_id} (routes_direct/misc.py) or gateway /hitl approve|deny
Bswarm/safety.py + tool_registry.pyBus adapter buttons (Telegram / Discord / Slack)
Cswarm/checkpoint_manager.pyPOST /api/swarm/tasks/{id}/approve

Gateway /hitl decisions require a gateway administrator who owns the target thread. Ownership is stored durably in the session directory, independently of the five-minute delivery cache, so authorized approvals survive cache expiry and restarts. Missing or mismatched identity is refused before reading the graph. An approval command cannot create its own thread ownership. KAZMA_GATEWAY_ADMINS, when set, is authoritative; otherwise the platform’s configured user allowlist grants admin status. An empty allowlist allows chatting and grants no administrator privileges.

Enabling /yolo, /unrestricted or /long yolo also requires a gateway administrator. These handlers check authorization before activation; adding their names to a later command gate would leave them unprotected. Checking status or disabling YOLO remains available without granting new privileges. Production YOLO restrictions still apply to administrators.

Not extra HITL gates:

  • Tool hooks (agent/tool_hooks.py) — cannot auto-approve danger, skip commitment, or replace the YAML list. Broken hook fail-opens. KAZMA_TOOL_HOOKS=0.
  • Plan mode — /plan on removes write/exec tools. /plan go returns them; danger still needs approval.
  • Commitment clarify/confirm — same interrupt bus, different kind (below).

Critical: hitl_config at every resumable graph

Section titled “Critical: hitl_config at every resumable graph”

The gate is active only when hitl_config is passed to build_supervisor_graph(). Live reader: get_hitl_config(). YAML timeout default is 300s.

Build siteHITL?Notes
KazmaAgent._ensure_graph()✅Run path; checkpointer so interrupt() can resume
app.py startup recompile into _graph_holder✅SSE SoT — checkpointer + HITL. Omitting it = dormant Web gate
KazmaAgent.get_streaming_graph()⚠️ auto_denyCached without a checkpointer (sync). Voice/boot-window consumers cannot resume interrupt(), so danger tools deny instead of minting an unresumable pause

There is no create_supervisor_graph() factory. Do not pin app.py line numbers.

H-8: LocalToolRegistry.execute on the web/chat path must not mint a second gate. Graph ContextVars (_graph_hitl_gate_ctx / _hitl_approved_ctx) skip the bus re-prompt. Clarify/confirm from execute() fail closed (“run from chat”).


2. Path A — the graph gate (single-agent chat)

Section titled “2. Path A — the graph gate (single-agent chat)”

tool_worker_node lives in graph_tool_worker.py, not graph_builder.py. When hitl_config is supplied:

  1. A call the host will refuse anyway gets no card: a shell_exec the host-shell policy refuses, or an empty command or shell metacharacter; a browser tool or computer_use this host cannot launch (safety/call_preflight.call_refusal). The model gets an ordinary error and the turn goes on.
  2. Commitment authorize_effect runs first (may rewrite args or raise a semantic card).
  3. Each tool is tested with requires_approval(name, hitl_config) (safety/hitl.py). Unclassified tools are gated (TOOL_TIERS; default-deny F-04).
  4. Danger tools call LangGraph interrupt() — the graph suspends. A registry row is registered (pending).
  5. On approval, _hitl_approved is injected so execute() does not re-prompt the bus. The registry claims then settles.
  6. On denial, a ToolResult with is_error=True is returned. An approval nobody answers within approval_timeout_seconds expires: the tool is not run, the model is told the card is closed and nobody decided, and the turn ends with an answer saying so.

If hitl_config is falsy, all tools skip this gate. That is why every resumable build site must pass it.

Web paints from _serverGates / TurnDocument (chat.js). close_turn keeps the turn open while a pending row exists. Absence of a row is an unregistered pending gate (backfill), never an inferred Approved stamp.

First-run output does not print the generated secret. A local operator can open the loopback UI; configure a strong KAZMA_SECRET in the installation’s .env for a stable machine credential.

Never rely on cookie-based KAZMA_SECRET for multi-user safety. A kazma-secret=<KAZMA_SECRET> cookie is the raw shared secret: theft = admin until rotation; no revocation table; no per-user identity.

Use opaque server-side sessions (kazma-session random ID → hashed row + expiry). Keep KAZMA_SECRET for machine-to-machine header auth. If unset, get_kazma_secret() may return "" and approval endpoints become unauthenticated — always set it off localhost.

Behind a reverse proxy, peer address is not a credential. Set KAZMA_TRUSTED_PROXIES. Honour X-Forwarded-* only from those addresses. An undeclared proxy latches undeclared_proxy_detected() and revokes peer trust for the process (AGENTS.md §26A). Rate limit / login throttle must use auth.client_address(request), never request.client.host.

POST /api/approve/{thread_id} (kazma_ui/routes_direct/misc.py approve_tool) — not app.py. Claims the registry row, then graph.ainvoke(Command(resume=…), config). Ownership mismatch → 403. The WebSocket takes no approvals (it refuses approve_tool).

Paused turns persist in the checkpointer. At exclusive-owner startup, registry boot_sweep() holds interrupted claimed/resuming rows as execution errors while preserving their decision and actor. It never touches pending (the card must survive restart). A failed resume cannot display as Approved or reopen the same approval card. Graph tool receipts hold unknown effects for inspection; they do not authorize automatic retries.


3. Path B — the swarm bus gate (/swarm + IDE)

Section titled “3. Path B — the swarm bus gate (/swarm + IDE)”

LocalToolRegistry.execute():

  1. Pops _hitl_approved.
  2. If not already approved, get_safety().
  3. is_danger_tool(name) must call requires_approval() (H-9 — tier floor, not a name list that can un-gate by omission).
  4. await safety.check(...). Denied → is_error=True.
  5. Fail-closed: any exception in the safety check → blocked.

swarm/safety.py — class SafetyMiddleware (not SafetyGate / SafetyChecker).

MethodBehavior
check() (async)Danger tools: NullBus + allow_headless_danger=False → reject; else bus.request_approval
check_sync()Fail-closed. Sync path cannot wait on a bus; blocks unless headless escape
is_danger_toolClassification via requires_approval / TOOL_TIERS / ALWAYS_HITL_TOOLS

allow_headless_danger=False is the default. Tests/dev set it true. Do not in production.

Danger list: _EXTENDED_DANGER = list(CANONICAL_DANGER_TOOLS) — not a second SoT. Spawn tools only if they are on CANONICAL.

TelegramBusAdapter / DiscordBusAdapter / SlackBusAdapter. Process-local singleton get_message_bus().

App wiring (app.py): collect every configured platform. One → that adapter. Two or more → FanOutBusAdapter. None → NullBusAdapter (fail-closed danger).

FanOut request_approval is tri-state (Wave 6 H-12): True settles; False is a vote until expected_voters or the deadline. A Discord Deny must not kill a Telegram Approve. This is not web claim_gate (first claim 200 / second 409).

Pytest skips real adapters (_skip_real_adapters).

Each adapter handle_callback() resolves the platform button onto the shared approval. Telegram swarm_approve_<task_id> / swarm_reject_<task_id>; Discord components; Slack interactive.


See Swarm Orchestration → Pipeline checkpoints.

  • CheckpointManager.handle_pipeline_checkpoint also _gate_register_pipeline.
  • Auto-reject timeout if checkpoint_timeout > 0.
  • Approve / reject settle the registry row. T-2: timeout must finalize the task and settle_gate.
  • restore_paused_tasks() re-arms timeouts after restart.
  • Endpoints: POST /api/swarm/tasks/{id}/approve / /reject.

Semantic clarify / confirm interrupts (the Commitment Layer)

Section titled “Semantic clarify / confirm interrupts (the Commitment Layer)”

The same unified HITL bus that carries danger-tool approvals (Gates A/B/C above) also carries a second kind of interrupt: semantic clarify/confirm cards produced by the Commitment Layer.

These are not danger-tool approvals — they fire when the agent’s intent is ambiguous or critical before a durable mutation, not because the tool is dangerous. Example: the user says “remind me about the meeting” and the model calls the schedule tool with an ambiguous time. The Commitment gate (authorize_effect) returns clarify, and the graph suspends via interrupt() with a payload carrying a question and a list of options (each with a slots_patch).

Interrupt kindWhen it firesPayload kind
Danger-tool approvalA tool is on the danger list (Gates A/B/C)hitl_approval
Semantic clarifyIntent is ambiguous (e.g. relative time + nearby event)semantic_clarify
Semantic confirmA critical act needs explicit OKsemantic_confirm

Renders on every platform — Web (chat + sidebar), Telegram, Discord, and Slack. Each semantic card renders one button per option; the existing Approve/Deny buttons map to best-option / cancel (build_resume_value in commitment/resume.py). On resume, the chosen option’s slots_patch is applied to the tool arguments and the turn continues; cancel is terminal.

The full decision mapping (allow / clarify / confirm / deny + rewrite-on-allow), the act resolvers (remind, exec denylist, config protected keys, outbound allowlist), scope tokens, the soul-confirm gate, modes, and kill-switches are documented in the dedicated guide: Commitment Layer (resolve-before-act).


Canonical + YAML + Settings must stay one set. Swarm _EXTENDED_DANGER is a copy of CANONICAL. MCP is pattern-based (unknown → danger). TOOL_TIERS is the floor for anything registered.

5.1 Path A (graph) — kazma.yaml safety.hitl.require_approval_for

Section titled “5.1 Path A (graph) — kazma.yaml safety.hitl.require_approval_for”

Default (kazma.yaml safety.hitl.require_approval_for, must match CANONICAL_DANGER_TOOLS): includes file_write, file_apply_patch, file_delete, shell_exec, code_exec, python_exec, computer_use, git/GitHub mutators, vault, installers, email send/delete, request_path_access, x_post, x_delete_post, x_schedule_post, x_cancel_scheduled_post. Parity-tested.

Always-HITL (X ToU): ALWAYS_HITL_TOOLS (x_post, x_delete_post, x_schedule_post, x_cancel_scheduled_post) require approval even when YOLO, standing grants, or HITL-disabled would skip other danger tools. Their card offers a one-time approval only (Approve once / Deny; on chat apps no “Approve for task”) and names the public action (“Publish to X — public, can’t be undone”, Post now). The approve route refuses a tool or yolo answer for one with 409 one_time_only, and the card stays pending. That is the chat/agent path. On the Web, X Studio (/x) and /api/scheduled/x treat the operator click as the approval (they call publish_x_post / book_x_post / delete_x_post directly — they do not invoke the chat tools). Official API only — see X publisher.

Git-write always-gate (2026-08): git commit, push, merge, rebase, reset, checkout --, restore, clean, rm and friends require an approval card on every execution path — YOLO cannot auto-approve repo mutations (a misread intent must cost a dialog, never a commit; 2026-08-27 incident). Read-only git (status/log/diff) is exempt. YOLO windows also default to 1 hour (was 4) — KAZMA_YOLO_TTL_SECONDS overrides. See Task Ledger. An ended window is removed within 15 minutes whether or not its chat is opened again, and a flag from before windows had an end time counts as ended: until 2026-09-28 such a flag was honoured for good, and one chat of the live install had skipped approvals under it since July.

Code fallback if unset: DEFAULT_DANGER_TOOLS = ["file_write", "file_delete", "shell_exec", "vault_retrieve", "vault_delete"] (safety/hitl.py:41). The vault tools protect secret retrieval/deletion.

Narrowing guard: the canonical danger tools are always part of the effective list — the floor has been on by default since audit 2026-09-16 F-5 — so Settings/YAML cannot narrow below them. KAZMA_HITL_CANONICAL_FLOOR=0 opts out, with a warning, and a drift warning then repeats every 15 minutes naming the tools left out.

5.2 Path B (swarm bus) — _EXTENDED_DANGER

Section titled “5.2 Path B (swarm bus) — _EXTENDED_DANGER”

_EXTENDED_DANGER = list(CANONICAL_DANGER_TOOLS) — a materialized copy because CANONICAL is a tuple. Same contents. Adding a danger tool means CANONICAL and kazma.yaml (parity tests compare sets). Spawn tools only if they are on CANONICAL.

Plus _SENSITIVE_READS = ["sqlite_query", "file_search"] — allowed but logged.

mcp/manager.py:71-88 — dynamic name-pattern matching (MCP tools are runtime-discovered):

  • danger if any of: write, delete, remove, exec, run, shell, bash, command, kill, terminate, install, deploy, upload, download, fetch, request, post, put, patch.
  • safe if any of: read, list, search, get, info, status, check, describe, query, count, exists, help.
  • unknown otherwise.

Unknown defaults to danger. UnifiedToolExecutor.execute() requires approval for both danger and unknown. Never weaken this. Local tools: requires_approval() ends on TOOL_TIERS — an unclassified registered tool is gated (F-04). Add a tier (read / write / danger) when you register a tool.


Verified in Skills, MCP & Tools → Cryptographic signing. Summary:

  • kazma hub sign writes checksum (SHA-256) + signature (HMAC-SHA256 over checksum, keyed by KAZMA_SECRET) into skill_manifest.yaml.
  • SkillLoader._load_module_from_file verifies both with hmac.compare_digest (constant-time) and refuses to load tampered/unsigned-by-required skills.

Nothing in Kazma signs or encrypts work handed from one agent to another. An inter-agent delegation package (Ed25519 signing, X25519 key agreement + AES-256-GCM) was never wired into the product: it sat in archive/ and was deleted on 2026-07-28. Work between swarm workers is dispatched by SwarmEngine (kazma_core/swarm/); what protects it is on this page — the swarm bus gate, the Commitment Layer’s scope token and the danger-tool lists — not message signing. The roadmap lists signed delegation as not in the code.

POST /api/approve/{thread_id} is protected by session / KAZMA_SECRET (secrets.compare_digest). get_kazma_secret() resolves env → KAZMA_AUTH_DISABLED → pytest skip → DB security.secret → auto-generate. Off localhost, always set KAZMA_SECRET. KAZMA_AUTH_DISABLED is for one machine: the boot guard refuses it on a non-loopback bind or behind a declared proxy (KAZMA_TRUSTED_PROXIES: a tunnel on the same machine makes a loopback bind public), and while it is set a request that came through a proxy is answered 503.


ComponentFail-closed behavior
SafetyMiddleware.check_syncBlocks danger tools when no real bus + allow_headless_danger=False.
ToolRegistry.executeAny safety-check exception → “blocked — SafetyMiddleware unavailable”.
classify_mcp_toolunknown → requires approval.
SkillLoaderTampered/missing signature → SkillLoadError, no load.
Hub write APINo KAZMA_SECRET → all writes rejected.
Delegation receiveMissing/invalid signature → reject.
HITL approval endpointCross-user approval → 403; no KAZMA_SECRET → see warning below.

⚠ Production warning: if KAZMA_SECRET is unset, get_kazma_secret() may return "" and approval endpoints become unauthenticated. Always set KAZMA_SECRET for any non-localhost deployment. (kazma serve only binds 0.0.0.0 when KAZMA_SECRET is set — this is by design.)


kazma-security.yaml declares posture across scanning, disclosure, bug_bounty (disabled — no paid program), and hardening. No Kazma code reads that file (checked 2026-09-27): it records the security program for people, and nothing below runs because of it. Its hardening section lists 8 checks ( secrets_in_logs, input_validation, rbac_enforcement, tls_required, dependency_audit, least_privilege, audit_trail, config_integrity). See Configuration → security config and root SECURITY.md. kazma-permissions.yaml defines division-based MCP allow/deny lists (the ALMuhalab divisions) with cross-division rules (require_explicit_approval, max_approval_duration_hours: 24, audit_all_access).

8.1 Hardening report (security/hardening.py)

Section titled “8.1 Hardening report (security/hardening.py)”

SecurityHardeningRunner is an on-demand operator report, not a gate. GET /api/security/hardening runs it over the install (the folder Kazma runs from, never the working directory) as a read-only diagnostic, and returns the findings. Each check runs in a worker thread, so the server keeps answering. A whole run takes about 10 seconds, most of it the OSV query. It does not run at startup, and no page calls it yet. The run_on_startup / fail_on_critical keys in kazma-security.yaml are not read.

Each check measures the property it names, using the same answer the rest of Kazma uses:

CheckWhat it readsSeverity
check_no_hardcoded_secretsThe install’s product files, as its git repository lists them. Tests, docs, generated folders and .env are left out. The values are judged by security/secret_scan.py: issuer formats (private key blocks, AWS, GitHub, Slack, Google, Stripe, OpenAI, Anthropic, Telegram, JWT), random-looking values assigned to credential names, and URL passwords. Placeholders, templates, vault pointers and environment-variable names are not counted. Findings name the file and line, never the value.critical
check_mcp_sandboxingMCP servers that keep a secret as typed rather than as a vault pointer (mcp_servers_store.servers_with_plaintext_secrets), and the per-task workspace guard (KAZMA_MCP_SCOPE_GUARD).high
check_rbac_enforcementWho reaches the API: the boot guard’s answer (bind, declared proxy, KAZMA_AUTH_DISABLED / KAZMA_DEV_WS_BYPASS / KAZMA_DEMO_MODE), plus a hand-set KAZMA_SECRET shorter than 20 characters.high
check_dependency_vulnerabilitiesOSV advisories for the versions actually installed (8.2). An OSV failure fails the check; it is never reported as clean.critical
check_skill_manifest_validityBuilt-in skills, checked the way the loader loads them: the manifest parses and every tool it names exists. Agent skills, checked the way activation does: a usable SKILL.md, and the integrity check activation applies (a tampered skill fails; an unsigned one is counted).medium
check_encrypted_communicationsWhether traffic from other machines arrives over TLS. Reachable only locally passes. If Kazma is exposed (a non-loopback bind, or a declared proxy), KAZMA_PUBLIC_URL must be an https:// address.high
check_audit_loggingThe approval registry (hitl_gates.db): it is on (KAZMA_GATE_REGISTRY) and readable. The message gives the number of decisions it holds.high
check_permission_escalationCalls in product code that run a shell or evaluate code: subprocess with shell= true or decided at run time, os.system, os.popen, subprocess.getoutput, create_subprocess_shell, eval, exec. Import aliases are resolved. Each call is checked against _REVIEWED_SITES, which records why the site is safe. Today it has one entry: operator tool hooks.critical

A check that cannot run is reported as failed, with its reason; the other checks still run. Until 2026-10-02, four checks passed on a keyword found anywhere, and four failed on findings that were not true (a package cache inside the install, requirement minimums sent to OSV, the wrong manifest file name). fix_issues, which wrote a .env into the install and was never called, was removed.

8.2 Dependency scanning (security/dependency_scanner.py)

Section titled “8.2 Dependency scanning (security/dependency_scanner.py)”

audit_installed() reads the environment’s installed distributions (PEP 503 names, local version labels dropped). It asks OSV’s batch API about each exact (name, version), following page tokens, then reads each advisory’s summary and fixed version. OSV lists most PyPI advisories under two ids, a GHSA id and a PYSEC id. They name each other as aliases, and the report counts each advisory once. A failure to reach OSV raises DependencyQueryError. Nothing is cached.

Some advisories have no fixed release and cannot be reached through Kazma’s use of the package. REVIEWED_ADVISORIES lists them, each with the reason and the code that would make it reachable: four chromadb advisories against the Chroma HTTP server (Kazma embeds Chroma in its own process and runs no Chroma server), and one accelerate advisory in load_checkpoint_in_model (Kazma never calls it). The report names them as reviewed, never as clean, and a test fails if product code starts using a Chroma server or that loader.

Minimums are security floors. pyproject.toml raises a package’s minimum to the release that fixes an advisory, including packages Kazma runs on without importing them by name (urllib3, anyio, pyasn1, and per extra aiohttp, oauthlib, torch, transformers, setuptools, virtualenv). kazma update installs additively (uv pip install -e), so a minimum is what makes it upgrade an install that holds an affected version. The lock alone changes only fresh installs. A deploy by git pull installs nothing, so at boot the server compares the installed packages with what this build declares (kazma_core/install_requirements.py: a checkout’s own pyproject.toml, because the installed metadata goes stale with every pull). When the install is behind, it logs a WARNING naming each package and raises the ops alert install.requirements_unmet. kazma update --reinstall -y installs the missing minimums; on a guarded install it stops Kazma through the guard once no chat turn is running, and starts it again after (Kazma update → when the server says its packages are behind). The report’s dependency check shows the same list, and it works without OSV.

GET /api/security/deps runs scan_skill_manifests() over the skills installed from the hub. For each skill it reports:

  • a credential written into the manifest (by the same secret_scan);
  • a command: that runs through a shell with -c, /c or -Command;
  • --privileged, a host network, sudo, chmod 777 or set-ID bits;
  • OSV advisories for the installed versions of the skill’s requirements.txt. A requirement that is not installed is named.

An OSV failure answers {"status": "error"}, never an empty list.

Neither scan is scheduled. The scanning section of kazma-security.yaml (interval 24 h, auto_create_issues) is not read. What does run on a schedule is GitHub’s: Dependabot’s weekly uv lockfile update PRs (.github/dependabot.yml; until 2026-10-02 it watched the pip ecosystem, which never touched uv.lock), and bandit’s HIGH gate on every CI run. CI’s uv export --locked and the release’s SBOM step fail when uv.lock no longer matches pyproject.toml.

8.3 Disclosure workflow (security/disclosure.py)

Section titled “8.3 Disclosure workflow (security/disclosure.py)”

SQLite kazma-data/disclosure.db enforces the transition chain submitted → acknowledged → investigating → confirmed → patched → closed. publish_advisory() mints an internal KAZMA-ADV-YYYY-… id (not a MITRE CVE) plus a markdown template, only from patched/closed states. encrypt_report() HMAC-SHA256-signs the JSON payload with KAZMA_DISCLOSURE_KEY.

kazma-security.yaml: response window 48 h, assessment 7 d, security_txt_url (RFC 9116 contact file — not a PGP key), channels email + GitHub private reporting. bug_bounty.enabled: false (no paid bounty). Canonical policy: root SECURITY.md and Vulnerability reporting.

What enforces security at run time is in the code, not in kazma-security.yaml: the approval gates (§7 of AGENTS.md), the commitment layer, the vault, SSRF/CSRF checks and the fence — each with its gate in tests/. The YAML is a declaration.


  1. Always set KAZMA_SECRET for non-localhost deployments. Generate with openssl rand -hex 32.
  2. Run stdio MCP servers in a sandbox. The stdio transport has no auth; a server gets the allowlisted basics plus its own env (audit H-4), never Kazma’s secrets, and runs with the Kazma process’s rights.
  3. Prefer SSE MCP with bearer auth for any remote MCP server.
  4. Sign all skills (kazma hub sign) and keep KAZMA_SECRET consistent across load — signature verification fails otherwise.
  5. Keep all three HITL execution paths + the registry active. Do not pass hitl_config=None on a resumable production graph. Do not mint a second web gate from execute().
  6. Do not set allow_headless_danger=True in production. It’s the test/dev escape hatch.
  7. Run as the non-root kazma user in Docker (the Dockerfile already does this).
  8. Bind 127.0.0.1 unless you have a reverse proxy + KAZMA_SECRET in place.
  9. Multi-operator: set platform allowlists + KAZMA_GATEWAY_STRICT_ALLOWLIST=1 (2026-08-19) — by default the Telegram/Discord/Slack adapters run allow-all for backward compatibility with single-operator installs; strict mode fails closed on an empty allowlist.
  10. Leave KAZMA_HITL_CANONICAL_FLOOR unset — the floor is on by default, and 0 would let the danger-tool approval list be narrowed below the canonical set.
  11. Set KAZMA_TRUSTED_PROXIES when behind nginx/Caddy/Docker. Peer 127.0.0.1 is not a credential.
  12. Do not pin scraping through proxy=. Direct hops use PinHostAsyncTransport; peer-private abort always.
  13. If you connect MCP servers, approve each call or name the tool in KAZMA_MCP_SAFE_ALLOWLIST. The server supplies the tool name. get_file, read_env, get_ssh_key and list_env_vars still classify safe, and that label does not skip the gate. read_env is the one to think about: env is deliberately absent from the shell_exec allowlist precisely so a single approval cannot become a credential dump. KAZMA_PRODUCTION=1 does not allow these names.

Default-deny, CSRF, SSRF (2026-08-29 / Wave 8)

Section titled “Default-deny, CSRF, SSRF (2026-08-29 / Wave 8)”

These sit beside HITL, not inside it.

RuleWhere
Unclassified tool is gatedrequires_approval() → TOOL_TIERS (deny wins)
Allowlisting a binary is not allowlisting what it runsshell_exec _EXEC_CAPABLE_ARGS (find -exec, git -c, …)
Secret masking recursessettings.mask_deep() — lists and JSON strings too
A password inside a URL is a secretmasked by value on every way out (security/url_credentials.py), whatever the setting is called; memory-backend URLs with a password go to the vault
CSRFcsrf.py: non-GET /api/* mismatched Origin/Referer host → 403. Use request.url.hostname (Starlette has no .host)
/health/details is sensitiveL-1; /health/live and /health/ready stay public
SSRF pin-IPvalidate_url returns public IPs; PinHostAsyncTransport when no proxy; assert_peer_public after each hop
ErrorsAPI safe_error / validation_error; no internals in 4xx/5xx bodies
Fenced tool outputFetched pages / search / MCP resources go through fence_untrusted — see section 10

CI: tests/test_audit_2026_08_29_regressions.py, tests/test_audit_wave8.py, test_every_registered_tool_has_a_tier.


10. The prompt fence — what it stops, measured

Section titled “10. The prompt fence — what it stops, measured”

Untrusted text — fetched web pages, search snippets, MCP tool results and resource bodies — is wrapped before a model sees it. The production entry point is fence_untrusted(content, *, source, is_error=False) in kazma_core/safety/prompt_fence.py; format_untrusted_block is the inner function it calls.

Trust comes from the caller, never from the content. is_error=True is how a caller says “this string is my own failure message, not third-party text”, and it is the only way to skip the fence. This is not a style preference: the flag replaced a check that sniffed content.startswith("Error:"), and because every caller forwards attacker-controlled text, an attacker who began a payload with those six characters had it delivered to the model unfenced. A trust decision read out of the content is a trust decision the attacker writes.

A fencing failure also fails closed — if the fence raises, the caller gets a placeholder naming the source and the number of characters withheld, not the raw text.

Document text is fenced by the documents platform itself, with format_untrusted_block (sources document, document_attachment and document_knowledge); see Document security.

attack successacted on the payload
undefended18.1%24.1%
spotlighting (4-character delimiter, from the literature)11.6%18.6%
Kazma’s fence10.4%14.8%

996 runs per condition on AgentDojo, a public benchmark Kazma did not write, every condition measured four times. The fence beats no-defense decisively (p < 0.001) and cannot be told apart from a four-character delimiter (p = 0.39).

Both halves of that matter. Fencing untrusted tool output works. It is also not where Kazma’s advantage lies, and the page below says so rather than leaving you to discover it.

  • Prompt injection: the numbers — the full measurement, the payloads that still land, and the reproduction you can run for free with no API key.
  • Threat model — what each boundary stops and, stated plainly, what it does not. Approval is consent, not containment.
  • Known gaps — open weaknesses, dated. Including one that affects this page’s defaults: an MCP server’s own tool name still classifies read_env as safe, and that label is not an approval. The call runs when you approve it, or when that name is on KAZMA_MCP_SAFE_ALLOWLIST. Production mode does not close it.

The fence reduces attack success. It does not eliminate it, and no honest number here will ever be 0%.


  • Class name: swarm safety is SafetyMiddleware, not SafetyGate/SafetyChecker.
  • Approve endpoint: routes_direct/misc.py approve_tool, not app.py.
  • Do not pin line numbers. tool_worker_node is graph_tool_worker.py; SSE SoT is the app.py recompile, not get_streaming_graph() (that path is checkpointer-less auto_deny).
  • FanOut, not Telegram-only. Multiple platforms → FanOutBusAdapter tri-state.
  • _EXTENDED_DANGER is CANONICAL, not a longer private list. MCP classify_mcp_tool remains pattern-based (unknown → danger).
  • “Trust tiers” do not exist as a product feature — the hub’s boolean certified flag, and an MCP server’s trust: trusted, which skips the approval card for its tools (refused in production unless KAZMA_MCP_TRUSTED_IN_PROD=1).
  • MCP stdio has no auth. Sandbox accordingly.
  • Binding audit: AUDIT_DEEP_2026-09-01_EXEC.md.