Security & Deployment Posture

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.

How to Read This Page

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:

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.

1. Which LLM Providers See What Data

Supported providers

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:

ProviderBackendAuthenticationData leaves your network?
vertex_anthropicClaude on Google Vertex AIApplication Default Credentials (no API key)Yes — to your GCP project's Vertex endpoint
vertex_geminiGemini on Google Vertex AIApplication Default CredentialsYes — to your GCP project's Vertex endpoint
anthropicAnthropic API (direct)ANTHROPIC_API_KEYYes — to Anthropic
openaiOpenAI API (direct)OPENAI_API_KEYYes — to OpenAI
google_genaiGemini Developer API (direct)GOOGLE_API_KEYYes — to Google
lmstudioAny local model via LM Studio's OpenAI-compatible serverNone (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.

What is sent to the LLM provider

What does not leave

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).

⚠️ The precise nuance

"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.

✓ The fully-local option

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.

2. Code-Execution Sandboxing

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.

TierWhat it isIsolation 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).

⚠️ Honest caveats the design docs state themselves

  • Environment scrubbing does not stop cloud-credential theft on a cloud VM. On GCP, Application Default Credentials are obtainable from the metadata server (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.
  • Python path guards are not the kernel's security boundary. 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).
  • macOS development containers run with open egress. Rootless podman on macOS cannot combine port-forwarding with an internal network, so the 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.
  • Stopping a compiled sampler means killing the kernel. A NumPyro fit compiled to one XLA program ignores SIGINT; cancellation escalates to killing the container/kernel and respawning, which loses the in-flight run (agent-session-kernels.md §3.4).

3. The Hosted Multi-User Posture

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:

⚠️ Without this flag, the default is single-user development

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).

4. Authentication & Authorization

Stated plainly, because this is the area where current reality is weakest relative to enterprise expectations:

Mitigation path

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.

5. Audit Logging

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:

EventMeaningEmitted from
kernel_spawn / kernel_respawnKernel started (container spawns include transport + egress posture)agents/kernels.py, agents/container_kernel.py
kernel_evict_lruIdle kernel evicted at the live-kernel capagents/kernels.py
kernel_diedKernel died mid-execute (OOM/crash); error synthesized, respawn followsagents/kernels.py
kernel_timeout_interrupt / kernel_timeout_killPer-cell timeout: SIGINT, then kill after a grace periodagents/kernels.py
kernel_fit_start / kernel_fit_doneModel fit lifecycle inside the kernelagents/kernels.py
kernel_egressPer-spawn egress posture; logged at WARNING if open (dev-macOS)agents/container_kernel.py
spawn_refusedFail-closed gate rejected an incomplete sandbox, with reasonsagents/container_kernel.py
overlay_wipedContainer teardown wiped the ephemeral state + control diragents/container_kernel.py
plot_rejectedOversized or schema-invalid figure dropped at captureagents/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).

6. Data Residency & Tenancy

7. Outbound Network Calls — Complete Inventory

Everything a running deployment may initiate toward the outside world, with the originating process:

CallFromWhenControl
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.

8. Control Status Table

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.

ControlDev defaultHosted profile (MMM_AGENT_HOSTED=1)Verified in
Kernel isolationinprocess — nonecontainer forced; fail-closed at startup and at spawnsrc/mmm_framework/agents/profile.py, container_kernel.py
Kernel secret/env scrubOn for subprocess/container kernels (n/a in-process); opt-out MMM_KERNEL_SCRUB_ENV=0On (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 egressUnrestricted (in-process/subprocess)Denied; metadata endpoints blocked (--network none / internal network)src/mmm_framework/agents/container_kernel.py (PR-F.4)
Kernel resource capsCell timeout 600 s + kernel LRU cap (subprocess); none in-processcgroup memory (2 GB default) / pids / CPU + ulimits + timeoutkernels.py, container_kernel.py (PR-F.3)
Plot/table channel hardeningThread-salted IDs, schema validation, size caps (all tiers)Samesrc/mmm_framework/agents/tools.py, workspace.py (PR-E.3)
Session IDsClient-supplied, auto-createdServer-minted UUID4 only; /chat rejects otherssrc/mmm_framework/api/main.py (chat endpoint)
File-serving path guardsis_within allow-roots incl. Path.cwd(); O_NOFOLLOW TOCTOU guardPath.cwd() dropped from allow-rootssrc/mmm_framework/agents/workspace.py:allowed_roots (PR-E.2)
Report output locationShared working directory (legacy fixed names)Per-session under the workspaceprofile.py, workspace.py (PR-F.6)
Brand-extraction fetchEnabled, SSRF-guardedDisabled unless MMM_BRAND_FETCH_ALLOW=1src/mmm_framework/agents/brand_extract.py
API authenticationAgent API: none. Models API: shared key, off by defaultUnchanged — bring your own perimeter (SSO/reverse proxy); session ID is a bearer capabilityapi/auth.py, api/config.py; agent-session-kernels-phase3.md §1.1
User roles (owner/analyst/viewer)Attribution and sign-off only — not access controlUnchanged; real RBAC is an explicitly deferred workstreamdocs/platform-overview.html; agent-session-kernels-phase3.md §1.1
Audit loggingmmm_audit logger → stdoutSame; off-host tamper-evident sink planned (Phase 4d)kernels.py, container_kernel.py, tools.py
Data residencyAll 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.

Questions From Your Security Review?

Every control on this page traces to a file you can read. For anything not covered, open an issue or audit the source directly.