This page answers the questions an information-security reviewer asks about a platform that ships an AI agent which executes code and calls large language models. Every claim below is tied to a specific file in the repository, and where the current state is weaker than a buyer would want, that is stated plainly along with the configuration that closes the gap.
The platform is self-hosted software — there is no vendor-operated SaaS. Security posture is therefore a function of how you deploy it, and the platform exposes two named postures:
technical-docs/agent-knowledge-workspace.md §11: "Single-user / low-concurrency local tool… the workspace dir is an organizational boundary, not a security one").MMM_AGENT_HOSTED=1). A single switch (src/mmm_framework/agents/profile.py) that fail-closes onto container-sandboxed kernels, denied egress, per-session report paths, and server-minted session IDs. It refuses to start if the sandbox is incomplete.
Between the two sits an intermediate hardening tier (MMM_AGENT_KERNEL=subprocess or container, set explicitly) that you can adopt piecewise. The status table at the bottom of this page lists every control with its state in each posture and the file where it is implemented.
This page is the AI-governance and LLM-data-flow reference. For the buyer-facing platform-trust view — multi-tenant isolation, IDOR-safe 404 authorization, password/JWT authentication, encryption posture, data retention/deletion, and the shared-responsibility split — see the companion Trust & Security page. Where the two overlap they agree.
The agent's chat model is selected by a configuration file plus environment overrides — never hard-coded. The full provider list, verified in src/mmm_framework/agents/llm.py:
| Provider | Backend | Authentication | Data leaves your network? |
|---|---|---|---|
vertex_anthropic | Claude on Google Vertex AI | Application Default Credentials (no API key) | Yes — to your GCP project's Vertex endpoint |
vertex_gemini | Gemini on Google Vertex AI | Application Default Credentials | Yes — to your GCP project's Vertex endpoint |
anthropic | Anthropic API (direct) | ANTHROPIC_API_KEY | Yes — to Anthropic |
openai | OpenAI API (direct) | OPENAI_API_KEY | Yes — to OpenAI |
google_genai | Gemini Developer API (direct) | GOOGLE_API_KEY | Yes — to Google |
lmstudio | Any local model via LM Studio's OpenAI-compatible server | None (defaults to http://localhost:1234/v1) | No |
Embeddings (used by the project knowledge base) are resolved separately from the chat model in src/mmm_framework/agents/embeddings.py, because Anthropic offers no embedding model: Vertex text-embedding-005, Google text-embedding-004, OpenAI text-embedding-3-small, or a local LM Studio embedding model (default text-embedding-nomic-embed-text-v1.5), overridable via MMM_EMBED_PROVIDER / MMM_EMBED_MODEL.
stdout of executed Python, knowledge-base snippets retrieved by search_knowledge_base (top-k chunks of documents you uploaded), and file excerpts the agent reads via read_workspace_file (truncated, 20 KB default — src/mmm_framework/agents/tools.py).src/mmm_framework/agents/knowledge_base.py, technical-docs/agent-knowledge-workspace.md §5–6).
Raw datasets are not uploaded to any LLM provider as part of normal operation. Uploaded data files live on local disk (uploads/, the per-session workspace), and analysis runs against them inside the execution kernel on your machine. Captured Plotly figures and tabular tool output are content-addressed and streamed to the browser via GET /plots/{id} and GET /tables/{id} — they do not pass through the model context (technical-docs/agent-knowledge-workspace.md §11b; src/mmm_framework/agents/workspace.py).
"Raw data stays local" holds by architecture, not by a hard filter: whatever the agent prints or reads into context — a df.head(), summary statistics, a 20 KB file excerpt — does go to the LLM provider as a tool result, because the model must see output to reason about it. The system prompt forbids printing full DataFrames and tool output is byte-capped, but if individual rows contain sensitive values and the agent surfaces them, those rows reach the provider. If that is unacceptable, use the fully-local option below or pre-anonymize data before upload.
Setting provider: lmstudio with a locally loaded chat model and a local embedding model means no chat, tool-result, or embedding traffic leaves the machine — both the LLM and KB-embedding endpoints are localhost (src/mmm_framework/agents/llm.py, embeddings.py). No API key is needed or sent.
The agent executes LLM-authored Python through a KernelManager abstraction (src/mmm_framework/agents/kernels.py) with three implementations, selected by MMM_AGENT_KERNEL. Model fits also run inside the kernel (Phase 2 of technical-docs/agent-session-kernels.md), so the isolation tier covers all agent-driven computation, not just ad-hoc cells.
| Tier | What it is | Isolation actually provided |
|---|---|---|
inprocess(dev default) |
A warm, per-session namespace executed inside the API process. | None. Code shares the API's memory, environment (including any API keys), and filesystem. Appropriate only for the single-user posture, where the operator and the user are the same person. |
subprocess |
One isolated ipykernel process per session (via jupyter_client; no extra service needed). |
Process + secret isolation. Spawned with a scrubbed environment: a fail-closed allowlist (anything not explicitly allowed is dropped) plus a secret-pattern denylist applied on top — *_API_KEY, *_TOKEN, *_SECRET, *_CREDENTIAL*, *_PASSWORD, *_PRIVATE_KEY, GOOGLE_APPLICATION_CREDENTIALS, AWS_*, AZURE_* are removed even if allowlisted. The kernel never calls the LLM or embedder, so it needs no credentials. Per-cell wall-clock timeout (MMM_CELL_TIMEOUT, default 600 s: SIGINT, then kill) and a live-kernel LRU cap (MMM_MAX_KERNELS, default 8). Verified in kernels.py and technical-docs/agent-session-kernels-phase3.md PR-E.1. No filesystem or network isolation. |
container |
Each session's kernel runs inside podman run on a pinned image (deploy/kernel/Containerfile; Docker-compatible via MMM_KERNEL_RUNTIME). |
OS-level sandbox (src/mmm_framework/agents/container_kernel.py): read-only rootfs, --cap-drop ALL + no-new-privileges, default-deny seccomp, masked /proc//sys, non-root UID, cgroup memory cap (default 2 GB) + pids + CPU limits, size-quota'd tmpfs scratch, only the session's own workspace directory bind-mounted, egress denied by default (MMM_KERNEL_EGRESS=deny; the production ipc transport uses --network none, validated to make the internet, 169.254.169.254, and metadata.google.internal unreachable — the chat-model API included), ephemeral writable overlay discarded on teardown, and the kernel's control directory (ZMQ HMAC key) wiped. With MMM_KERNEL_REQUIRE_SANDBOX on, _verify_isolation refuses to spawn unless every control is present. |
Independent of tier, the plot channel is hardened at the single capture funnel: figure IDs are salted with the session's thread_id (not guessable from content across sessions), payloads are schema-validated and size-capped (MMM_PLOT_MAX_BYTES, default 5 MiB), and rejected plots are dropped with an audit line rather than inlined (technical-docs/agent-session-kernels-phase3.md PR-E.3; src/mmm_framework/agents/tools.py).
169.254.169.254), reachable from any same-host subprocess regardless of its environment. Blocking that is an egress control, which only the container tier provides (agent-session-kernels-phase3.md §1.2, PR-F.4). The subprocess tier is defense-in-depth, not a hosting green-light.safe_join/is_within run in the API process and gate downloads; what in-kernel code can read or write is constrained only by the container mount namespace (agent-session-kernels.md §4).tcp dev posture is recorded as open:unenforced-macos-dev and audited at WARNING level so it cannot be mistaken for production (container_kernel.py; PR-F.0/F.4 findings). Production egress-deny is the Linux ipc posture.agent-session-kernels.md §3.4).
MMM_AGENT_HOSTED=1 (src/mmm_framework/agents/profile.py) flips the deployment from single-user development to the hosted multi-user posture. When set:
container, and an explicitly configured non-sandboxed kernel is force-upgraded to container; assert_hosted_sandbox is wired into app startup so a hosted server on a non-sandboxed kernel refuses to boot (profile.py; src/mmm_framework/api/main.py)._verify_isolation fail-closes any spawn missing read-only rootfs, dropped capabilities, a memory cap, or denied egress (container_kernel.py).Path.cwd() is dropped from the download allow-roots, so file serving is confined to the workspace, uploads, and model/config directories (src/mmm_framework/agents/workspace.py:allowed_roots)./chat refuses guessable or client-invented session IDs — only server-minted UUID4 sessions from POST /sessions are accepted (main.py, chat endpoint).
A default install runs agent code in-process, auto-creates any session ID a client supplies, and serves one trusted operator. Do not place a default install in front of multiple users or untrusted networks. Conversely, do not set MMM_AGENT_HOSTED=1 without building the kernel image and having a container runtime present — the profile is deliberately fail-closed and the server will refuse to start rather than run half-sandboxed (profile.py docstring; design rule "a half-applied sandbox is more dangerous than an honest single-user posture", agent-session-kernels.md §4).
Stated plainly, because this is the area where current reality is weakest relative to enterprise expectations:
X-API-Key header on the agent app (src/mmm_framework/api/main.py) is a pass-through credential for direct LLM providers, not a server login; no route carries an auth dependency. In the hosted profile, the unguessable server-minted session ID acts as a bearer capability scoping plots, files, and kernels to a session — a deliberate, documented interim decision, with full multi-tenant auth named as a separate, explicitly deferred workstream (agent-session-kernels-phase3.md §1.1).api/auth.py validates X-API-Key against a configured list only when api_keys_enabled is true; the default is False (api/config.py). When enabled it is a shared key, not per-user identity.docs/platform-overview.html). A viewer role does not technically prevent any API action.Today: deploy behind your own perimeter — VPN or private network, plus a reverse proxy with SSO/OIDC in front of both APIs — and enable the Models-API key. The hosted profile's session-capability model plus per-session container mounts then provides session-to-session separation behind that perimeter. An identity layer with real per-user authorization is the named follow-on workstream; until it lands, the platform should be treated as trusting everyone who can reach its ports.
Kernel lifecycle and security events are emitted as structured key=value lines on the mmm_audit standard-library logger (stdout / container logs). Event names verified in code:
| Event | Meaning | Emitted from |
|---|---|---|
kernel_spawn / kernel_respawn | Kernel started (container spawns include transport + egress posture) | agents/kernels.py, agents/container_kernel.py |
kernel_evict_lru | Idle kernel evicted at the live-kernel cap | agents/kernels.py |
kernel_died | Kernel died mid-execute (OOM/crash); error synthesized, respawn follows | agents/kernels.py |
kernel_timeout_interrupt / kernel_timeout_kill | Per-cell timeout: SIGINT, then kill after a grace period | agents/kernels.py |
kernel_fit_start / kernel_fit_done | Model fit lifecycle inside the kernel | agents/kernels.py |
kernel_egress | Per-spawn egress posture; logged at WARNING if open (dev-macOS) | agents/container_kernel.py |
spawn_refused | Fail-closed gate rejected an incomplete sandbox, with reasons | agents/container_kernel.py |
overlay_wiped | Container teardown wiped the ephemeral state + control dir | agents/container_kernel.py |
plot_rejected | Oversized or schema-invalid figure dropped at capture | agents/tools.py, agents/eda_tools.py |
Known limits, per the design docs: the sink is a single stdout stream — there is no tamper-evident or off-host audit sink yet (planned as Phase 4d, agent-session-kernels.md §6). Under deny-all egress there are no per-packet drop logs to emit (nothing escapes to be dropped); per-drop logging belongs to a future allowlist-proxy model (agent-session-kernels-phase3.md PR-F.4).
$MMM_AGENT_WORKSPACE/threads/<thread_id>/, per-project knowledge-base sources under $MMM_AGENT_WORKSPACE/projects/<project_id>/kb/, dataset uploads under uploads/ (default workspace root: ./agent_workspace) — technical-docs/agent-knowledge-workspace.md §2.kb_documents / kb_chunks tables, embeddings as binary blobs) — not in any external vector database (agent-knowledge-workspace.md §4/§6).agent-session-kernels-phase3.md PR-E.3/F.6). Cross-user tenancy ultimately rests on the session capability until the deferred auth workstream lands (section 4).Everything a running deployment may initiate toward the outside world, with the originating process:
| Call | From | When | Control |
|---|---|---|---|
| Chat LLM API (Anthropic / OpenAI / Google / Vertex) | API process only — never the kernel | Every agent turn | Provider chosen in config/model_config.yaml; lmstudio keeps it on localhost. The kernel has no LLM credentials (env scrub) and, in the container tier, no route to the model API (egress deny). |
| Embedding API | API process | KB document ingest and each KB search | Same provider controls; local LM Studio embedder available (agents/embeddings.py). |
Vertex model discovery (GET /vertex-models backend call) |
API process | On request, cached ~5 minutes | Only when a Vertex provider is configured (docs/model-configuration.md). |
Brand-extraction website fetch (extract_brand_from_website) |
API process (server-side, never the kernel) | Only when a user asks to extract branding from a URL | SSRF-guarded in agents/brand_extract.py: resolves and blocks loopback, RFC 1918, link-local 169.254.x (the cloud-metadata vector), and ULA targets; at most 3 manually re-vetted redirects. Disabled in the hosted profile unless MMM_BRAND_FETCH_ALLOW=1. |
| Agent kernel egress | Kernel | — | inprocess/subprocess: unrestricted (single-user posture). container: denied by default; production ipc posture is --network none (agents/container_kernel.py). |
Nothing else phones home: no telemetry, no license checks, no auto-update calls exist in the codebase.
One row per control: its state in the development default, its state under the hosted profile, and the file where the behavior is implemented and can be verified.
| Control | Dev default | Hosted profile (MMM_AGENT_HOSTED=1) | Verified in |
|---|---|---|---|
| Kernel isolation | inprocess — none | container forced; fail-closed at startup and at spawn | src/mmm_framework/agents/profile.py, container_kernel.py |
| Kernel secret/env scrub | On for subprocess/container kernels (n/a in-process); opt-out MMM_KERNEL_SCRUB_ENV=0 | On (container gets a config-only env subset) | src/mmm_framework/agents/kernels.py; technical-docs/agent-session-kernels-phase3.md PR-E.1 |
| Kernel network egress | Unrestricted (in-process/subprocess) | Denied; metadata endpoints blocked (--network none / internal network) | src/mmm_framework/agents/container_kernel.py (PR-F.4) |
| Kernel resource caps | Cell timeout 600 s + kernel LRU cap (subprocess); none in-process | cgroup memory (2 GB default) / pids / CPU + ulimits + timeout | kernels.py, container_kernel.py (PR-F.3) |
| Plot/table channel hardening | Thread-salted IDs, schema validation, size caps (all tiers) | Same | src/mmm_framework/agents/tools.py, workspace.py (PR-E.3) |
| Session IDs | Client-supplied, auto-created | Server-minted UUID4 only; /chat rejects others | src/mmm_framework/api/main.py (chat endpoint) |
| File-serving path guards | is_within allow-roots incl. Path.cwd(); O_NOFOLLOW TOCTOU guard | Path.cwd() dropped from allow-roots | src/mmm_framework/agents/workspace.py:allowed_roots (PR-E.2) |
| Report output location | Shared working directory (legacy fixed names) | Per-session under the workspace | profile.py, workspace.py (PR-F.6) |
| Brand-extraction fetch | Enabled, SSRF-guarded | Disabled unless MMM_BRAND_FETCH_ALLOW=1 | src/mmm_framework/agents/brand_extract.py |
| API authentication | Agent API: none. Models API: shared key, off by default | Unchanged — bring your own perimeter (SSO/reverse proxy); session ID is a bearer capability | api/auth.py, api/config.py; agent-session-kernels-phase3.md §1.1 |
| User roles (owner/analyst/viewer) | Attribution and sign-off only — not access control | Unchanged; real RBAC is an explicitly deferred workstream | docs/platform-overview.html; agent-session-kernels-phase3.md §1.1 |
| Audit logging | mmm_audit logger → stdout | Same; off-host tamper-evident sink planned (Phase 4d) | kernels.py, container_kernel.py, tools.py |
| Data residency | All state local in both postures; only configured LLM/embedding traffic leaves (none with LM Studio) | agents/workspace.py; technical-docs/agent-knowledge-workspace.md | |
Design history and validation evidence for the kernel controls — including the adversarial review that shaped them and the joint exit tests (a hostile cell cannot read host secrets, a sibling session's workspace, or the network; OOM is contained; the plot channel cannot cross sessions) — live in technical-docs/agent-session-kernels.md and its phase documents in the repository.
Every control on this page traces to a file you can read. For anything not covered, open an issue or audit the source directly.