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
| Path | Where it lives | Approval surface |
|---|---|---|
| A | agent/graph_tool_worker.py (wired from graph_builder.py) | POST /api/approve/{thread_id} (routes_direct/misc.py) or gateway /hitl approve|deny |
| B | swarm/safety.py + tool_registry.py | Bus adapter buttons (Telegram / Discord / Slack) |
| C | swarm/checkpoint_manager.py | POST /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 onremoves write/exec tools./plan goreturns 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 site | HITL? | 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_deny | Cached 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)”2.1 How it works
Section titled “2.1 How it works”tool_worker_node lives in graph_tool_worker.py, not graph_builder.py. When hitl_config is supplied:
- A call the host will refuse anyway gets no card: a
shell_execthe host-shell policy refuses, or an empty command or shell metacharacter; a browser tool orcomputer_usethis host cannot launch (safety/call_preflight.call_refusal). The model gets an ordinary error and the turn goes on. - Commitment
authorize_effectruns first (may rewrite args or raise a semantic card). - Each tool is tested with
requires_approval(name, hitl_config)(safety/hitl.py). Unclassified tools are gated (TOOL_TIERS; default-deny F-04). - Danger tools call LangGraph
interrupt()— the graph suspends. A registry row is registered (pending). - On approval,
_hitl_approvedis injected soexecute()does not re-prompt the bus. The registry claims then settles. - On denial, a
ToolResultwithis_error=Trueis returned. An approval nobody answers withinapproval_timeout_secondsexpires: 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.
Auth: KAZMA_SECRET vs opaque sessions
Section titled “Auth: KAZMA_SECRET vs opaque sessions”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.
2.2 Resume
Section titled “2.2 Resume”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)”3.1 Execution path
Section titled “3.1 Execution path”LocalToolRegistry.execute():
- Pops
_hitl_approved. - If not already approved,
get_safety(). is_danger_tool(name)must callrequires_approval()(H-9 — tier floor, not a name list that can un-gate by omission).await safety.check(...). Denied →is_error=True.- Fail-closed: any exception in the safety check → blocked.
3.2 SafetyMiddleware
Section titled “3.2 SafetyMiddleware”swarm/safety.py — class SafetyMiddleware (not SafetyGate / SafetyChecker).
| Method | Behavior |
|---|---|
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_tool | Classification 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.
3.3 Bus adapters
Section titled “3.3 Bus adapters”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).
3.4 Callbacks
Section titled “3.4 Callbacks”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.
4. Path C — pipeline checkpoints
Section titled “4. Path C — pipeline checkpoints”See Swarm Orchestration → Pipeline checkpoints.
CheckpointManager.handle_pipeline_checkpointalso_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 kind | When it fires | Payload kind |
|---|---|---|
| Danger-tool approval | A tool is on the danger list (Gates A/B/C) | hitl_approval |
| Semantic clarify | Intent is ambiguous (e.g. relative time + nearby event) | semantic_clarify |
| Semantic confirm | A critical act needs explicit OK | semantic_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).
5. Danger-tool lists
Section titled “5. Danger-tool lists”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=0opts 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.
5.3 MCP — classify_mcp_tool()
Section titled “5.3 MCP — classify_mcp_tool()”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 bothdangerandunknown. Never weaken this. Local tools:requires_approval()ends onTOOL_TIERS— an unclassified registered tool is gated (F-04). Add a tier (read/write/danger) when you register a tool.
6. Cryptographic integrity (beyond HITL)
Section titled “6. Cryptographic integrity (beyond HITL)”6.1 Skill HMAC signing (Hub)
Section titled “6.1 Skill HMAC signing (Hub)”Verified in Skills, MCP & Tools → Cryptographic signing. Summary:
kazma hub signwriteschecksum(SHA-256) +signature(HMAC-SHA256 over checksum, keyed byKAZMA_SECRET) intoskill_manifest.yaml.SkillLoader._load_module_from_fileverifies both withhmac.compare_digest(constant-time) and refuses to load tampered/unsigned-by-required skills.
6.2 No signed delegation between agents
Section titled “6.2 No signed delegation between agents”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.
6.3 HITL endpoint secret
Section titled “6.3 HITL endpoint secret”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.
7. Fail-closed behaviors (inventory)
Section titled “7. Fail-closed behaviors (inventory)”| Component | Fail-closed behavior |
|---|---|
SafetyMiddleware.check_sync | Blocks danger tools when no real bus + allow_headless_danger=False. |
ToolRegistry.execute | Any safety-check exception → “blocked — SafetyMiddleware unavailable”. |
classify_mcp_tool | unknown → requires approval. |
SkillLoader | Tampered/missing signature → SkillLoadError, no load. |
| Hub write API | No KAZMA_SECRET → all writes rejected. |
| Delegation receive | Missing/invalid signature → reject. |
| HITL approval endpoint | Cross-user approval → 403; no KAZMA_SECRET → see warning below. |
⚠ Production warning: if
KAZMA_SECRETis unset,get_kazma_secret()may return""and approval endpoints become unauthenticated. Always setKAZMA_SECRETfor any non-localhost deployment. (kazma serveonly binds0.0.0.0whenKAZMA_SECRETis set — this is by design.)
8. Security config files
Section titled “8. Security config files”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:
| Check | What it reads | Severity |
|---|---|---|
check_no_hardcoded_secrets | The 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_sandboxing | MCP 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_enforcement | Who 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_vulnerabilities | OSV advisories for the versions actually installed (8.2). An OSV failure fails the check; it is never reported as clean. | critical |
check_skill_manifest_validity | Built-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_communications | Whether 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_logging | The 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_escalation | Calls 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,/cor-Command; --privileged, a host network,sudo,chmod 777or 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 intests/. The YAML is a declaration.
9. Hardening recommendations
Section titled “9. Hardening recommendations”- Always set
KAZMA_SECRETfor non-localhost deployments. Generate withopenssl rand -hex 32. - 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. - Prefer SSE MCP with bearer auth for any remote MCP server.
- Sign all skills (
kazma hub sign) and keepKAZMA_SECRETconsistent across load — signature verification fails otherwise. - Keep all three HITL execution paths + the registry active. Do not pass
hitl_config=Noneon a resumable production graph. Do not mint a second web gate fromexecute(). - Do not set
allow_headless_danger=Truein production. It’s the test/dev escape hatch. - Run as the non-root
kazmauser in Docker (the Dockerfile already does this). - Bind
127.0.0.1unless you have a reverse proxy +KAZMA_SECRETin place. - 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. - Leave
KAZMA_HITL_CANONICAL_FLOORunset — the floor is on by default, and0would let the danger-tool approval list be narrowed below the canonical set. - Set
KAZMA_TRUSTED_PROXIESwhen behind nginx/Caddy/Docker. Peer127.0.0.1is not a credential. - Do not pin scraping through
proxy=. Direct hops usePinHostAsyncTransport; peer-private abort always. - 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_keyandlist_env_varsstill classifysafe, and that label does not skip the gate.read_envis the one to think about:envis deliberately absent from theshell_execallowlist precisely so a single approval cannot become a credential dump.KAZMA_PRODUCTION=1does 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.
| Rule | Where |
|---|---|
| Unclassified tool is gated | requires_approval() → TOOL_TIERS (deny wins) |
| Allowlisting a binary is not allowlisting what it runs | shell_exec _EXEC_CAPABLE_ARGS (find -exec, git -c, …) |
| Secret masking recurses | settings.mask_deep() — lists and JSON strings too |
| A password inside a URL is a secret | masked 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 |
| CSRF | csrf.py: non-GET /api/* mismatched Origin/Referer host → 403. Use request.url.hostname (Starlette has no .host) |
/health/details is sensitive | L-1; /health/live and /health/ready stay public |
| SSRF pin-IP | validate_url returns public IPs; PinHostAsyncTransport when no proxy; assert_peer_public after each hop |
| Errors | API safe_error / validation_error; no internals in 4xx/5xx bodies |
| Fenced tool output | Fetched 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.
What it is measured to do
Section titled “What it is measured to do”| attack success | acted on the payload | |
|---|---|---|
| undefended | 18.1% | 24.1% |
| spotlighting (4-character delimiter, from the literature) | 11.6% | 18.6% |
| Kazma’s fence | 10.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.
Before you rely on it
Section titled “Before you rely on 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_envas safe, and that label is not an approval. The call runs when you approve it, or when that name is onKAZMA_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%.
Documentation Audit Notes
Section titled “Documentation Audit Notes”- Class name: swarm safety is
SafetyMiddleware, notSafetyGate/SafetyChecker. - Approve endpoint:
routes_direct/misc.pyapprove_tool, notapp.py. - Do not pin line numbers.
tool_worker_nodeisgraph_tool_worker.py; SSE SoT is the app.py recompile, notget_streaming_graph()(that path is checkpointer-lessauto_deny). - FanOut, not Telegram-only. Multiple platforms →
FanOutBusAdaptertri-state. _EXTENDED_DANGERis CANONICAL, not a longer private list. MCPclassify_mcp_toolremains pattern-based (unknown → danger).- “Trust tiers” do not exist as a product feature — the hub’s boolean
certifiedflag, and an MCP server’strust: trusted, which skips the approval card for its tools (refused in production unlessKAZMA_MCP_TRUSTED_IN_PROD=1). - MCP stdio has no auth. Sandbox accordingly.
- Binding audit:
AUDIT_DEEP_2026-09-01_EXEC.md.

