Simple, modular, stateful AI agent orchestration with sandboxed execution.
ModuStash is a self-hosted agent framework and workspace built with FastAPI, React, LangGraph, SQLite, FastMCP, and Temporal. It provides a dual-engine architecture: LangGraph for fast, micro-step reasoning and Temporal for long-running, durable macro-orchestration.
ModuStash was built around three primary principles:
- Simplicity First: Defining an agent should be as simple as writing a clear YAML manifest and a system prompt. Adding custom tools requires nothing more than placing standard Python functions with a single
@register_tooldecorator. - Strict Modularity: Tools, model providers, memory limits, and project workspaces are completely decoupled.
- Workspaces are execution boundaries, not capability providers. An agent only gets access to tools explicitly declared in its manifest.
- State & Isolation by Default: Every execution is strictly stateful and checkpointed using SQLite. Concurrent access to the same thread is protected with single-flight file-backed locking (
ThreadBusyError). Filesystem operations are strictly sandboxed through descriptor-relative syscalls (dir_fd,O_NOFOLLOW).
- FastAPI REST and SSE backend.
- React browser workspace served by FastAPI.
- Strict, manifest-driven agent configuration.
- Explicit per-agent tool capabilities.
- Prompt-driven and native provider tool calling.
- Descriptor-relative filesystem sandboxing.
- Isolated project workspaces and artifact browsing.
- SQLite-backed LangGraph checkpoints.
- Composite checkpoint identities scoped by project, agent, and user thread.
- Single-flight protection for active checkpoint threads.
- Temporal workflows for durable long-running execution.
- FastMCP gateway with explicit project and thread scoping.
- Local CLI installed as
modustash. - Loopback-only systemd services suitable for a private reverse proxy or Tailscale Serve.
This section tracks upcoming features and areas for community contribution:
- Custom Topology Plugins: Support configurable agent topologies (such as ReAct, RSI wrappers, and Map-Reduce) declared directly inside YAML manifests.
- Multi-Agent Swarm Orchestration: Direct agent-to-agent delegation protocols within the same project workspace.
- Front-End polishing and wiring: General completion of frontend in the UI. Wrap up dead ends, etc.
┌─────────────────────────────────────────┐
│ React Web UI / FastMCP / CLI │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────┐
│ FastAPI Control Plane │
└────────────┬────────────┘
│
┌──────────────────────┴──────────────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ LangGraph Micro-Loop │ │ Temporal Macro-Workflow │
│ (Step Reasoning & Tools)│ │ (Durable Timers/Loops) │
└────────────┬────────────┘ └────────────┬────────────┘
│ │
└──────────────────────┬──────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Core Sandbox & Memory │
│ • WorkspaceSandbox (dir_fd / O_NOFOLLOW file bounds) │
│ • SQLite Checkpointer (project:manifest:thread) │
│ • Token & Cost Budget Accumulator │
└────────────────────────────────────────────────────────┘
Tools are granted by an agent manifest:
tools:
- name: read_workspace_filedescription: Read a UTF-8 workspace file.A project workspace is only an execution boundary. Selecting a workspace does not grant filesystem, network, or other tools.
At graph compilation time, ModuStash intersects the manifest's declared tool names with the registered tool implementations. Only that intersection is bound to the model and graph nodes. A manifest that requests an unknown tool fails compilation.
The built-in tool modules are:
stash/tools/workspace_tools.py: project-bound filesystem tools.stash/tools/http_tools.py: network HTTP tools.stash/tools/registry.py: modular registry construction.
Prompt-driven agents must emit the entire tool request as one clean fenced JSON block:
```json
{"tool": "read_workspace_file", "args": {"path": "notes.md"}}
```Prose around the block, unfenced JSON, multiple objects, missing args, and
extra object fields are rejected by the strict parser.
User-facing thread IDs are never used directly as LangGraph storage keys. Every checkpoint uses:
project:manifest_name:user_thread_id
For example:
default:workspace_assistant:session_123
The REST API, MCP gateway, CLI, and Temporal activities all use this composite
identity. Concurrent executions against the same composite thread are rejected
as Thread Busy; the REST API returns HTTP 429.
WorkspaceSandbox performs file access relative to open directory
descriptors. It uses dir_fd, O_NOFOLLOW, regular-file checks, and atomic
descriptor-relative replacement to prevent path traversal, symbolic-link
escapes, and symlink retargeting races.
Uploads are limited to 26,214,400 bytes. Only UTF-8 text, Markdown, structured data, configuration files, and standard programming source extensions are accepted. Executable binaries, NUL-containing files, invalid UTF-8, and unknown file extensions are rejected.
- Python 3.11 or newer.
- Node.js 18 or newer.
- Git.
- SQLite.
- At least one API key for a model provider used by an enabled manifest.
- Temporal CLI or an external Temporal server when durable workflows are used.
Supported model providers include OpenAI-compatible endpoints, Google Gemini, and Groq.
Clone the repository and create a virtual environment:
git clone https://github.com/RY004/modustash.git
cd modustash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .Create local configuration:
cp .env.example .envAdd the provider key required by your chosen manifest:
OPENAI_API_KEY=GOOGLE_API_KEY=GROQ_API_KEY=Install and build the browser application:
npm --prefix frontend install
npm --prefix frontend run buildStart FastAPI:
uvicorn core.api:app --host 127.0.0.1 --port 8501 --reloadOpen:
http://127.0.0.1:8501
For frontend development, run Vite separately:
npm --prefix frontend run devVite listens on 127.0.0.1:5173 and proxies /api to FastAPI on port 8501.
Installing the project registers the modustash command.
Show help:
modustash --helpList and validate manifests:
modustash list
modustash list --jsonInspect compilation properties:
modustash inspect manifests/workspace_assistant.yamlRun an agent:
modustash run \
manifests/workspace_assistant.yaml \
"List the workspace files and summarize them." \
--project default \
--thread-id local-session-1 \
--workspace ./projects/default \
--database ./data/database.sqliteReuse the same project, manifest, and thread ID to continue checkpointed state. Change any of those values to create an isolated checkpoint namespace.
Use --no-memory for an invocation without SQLite checkpoint persistence:
modustash run \
manifests/workspace_assistant.yaml \
"Create a short README draft." \
--no-memoryStart a local Temporal development server on loopback:
temporal server start-dev \
--ip 127.0.0.1 \
--port 7233 \
--ui-port 8233 \
--db-filename ./data/temporal.dbStart the worker in another terminal:
source .venv/bin/activate
python -m temporal.workerRelevant environment variables:
TEMPORAL_ADDRESS=127.0.0.1:7233TEMPORAL_NAMESPACE=defaultTEMPORAL_TASK_QUEUE=modustash-queueMODUSTASH_DATABASE_PATH=./data/database.sqliteMODUSTASH_PROJECTS_ROOT=./projectsMODUSTASH_MANIFESTS_ROOT=./manifestsTemporal workflow input supports an explicit project and thread_id.
Activities convert those values and the validated manifest name into the same
composite checkpoint identity used by the API and MCP gateway.
Start the MCP server:
python -m core.mcp_serverIt listens on:
http://127.0.0.1:8000/sse
Every agent MCP tool requires:
user_inputthread_idproject
There is no shared default MCP thread. State is scoped by project, manifest, and caller-supplied thread ID.
The React interface provides:
- Project creation and selection.
- Conversation sessions.
- Agent selection from the active manifest registry.
- Streaming model output over SSE.
- Tool execution traces.
- Token, cost, and step telemetry.
- Workspace artifact listing and preview.
- Validated text/source uploads.
- Runtime and systemd service status.
Production builds are written to frontend/dist and served by FastAPI.
The included units run:
| Unit | Purpose | Listener |
|---|---|---|
temporal-dev.service | Local Temporal server and UI | 127.0.0.1:7233, 127.0.0.1:8233 |
modustash-worker.service | Temporal worker | No public listener |
modustash-mcp.service | FastMCP SSE gateway | 127.0.0.1:8000 |
modustash-ui.service | FastAPI and React application | 127.0.0.1:8501 |
Install the deployment on a supported Debian or Ubuntu system:
bash deploy/install.shReview the installer and units before using them on a production or shared host.
Check service status:
sudo systemctl status \
temporal-dev.service \
modustash-worker.service \
modustash-mcp.service \
modustash-ui.serviceFollow logs:
sudo journalctl \
-u temporal-dev.service \
-u modustash-worker.service \
-u modustash-mcp.service \
-u modustash-ui.service \
-fStart from .env.example.
Important variables include:
MODUSTASH_DATABASE_PATH=./data/database.sqliteMODUSTASH_PROJECTS_ROOT=./projectsMODUSTASH_MANIFESTS_ROOT=./manifestsTEMPORAL_ADDRESS=127.0.0.1:7233TEMPORAL_NAMESPACE=defaultTEMPORAL_TASK_QUEUE=modustash-queueOPENAI_API_KEY=GOOGLE_API_KEY=GROQ_API_KEY=Never commit .env or provider credentials.
- Keep all application listeners on loopback.
- Place authentication and TLS in a trusted reverse proxy or Tailscale.
- Review every manifest before enabling it.
- Grant only the tools an agent requires.
- Treat network tools as sensitive capabilities.
- Keep project directories owned by the service account.
- Do not replace project directories with symbolic links.
- Restrict
.envto mode0600. - Encrypt backups containing checkpoints, prompts, uploads, and artifacts.
- Back up SQLite databases using SQLite-aware tooling or stop writers first.
- Keep Python, Node.js, Temporal, and operating-system dependencies updated.
ModuStash is distributed under the Apache License 2.0.
Agent manifests are strictly validated YAML files in manifests/.
- Explicit Tool Declaration: Manifests specify only the tools the agent is permitted to use. Selecting a project workspace does NOT grant tools by default.
- Explicit Budgets: Always set conservative step and USD budget ceilings to prevent infinite reasoning loops or runaway model bills.
- Choice of Tool Mode:
prompt_driven: Recommended when using open-weights models or providers without native function-calling support. Requires strict single fenced JSON responses.native_api: Recommended for OpenAI, Gemini, or Groq function-calling capabilities.
schema_version: "3.2"name: "my_custom_agent"description: "Brief summary of what this agent does."tool_execution_mode: "prompt_driven"# or "native_api"execution_engine: "langgraph"durability_engine: "temporal"# or "none"# Define inline or reference an external file via system_prompt_filesystem_prompt: | You are an expert file analyst. Always inspect files before drawing conclusions.models:
default:
provider: "openai"# openai, google, or groqmodel: "gpt-4o-mini"temperature: 0.1pricing:
input_cost_per_1m: 0.15output_cost_per_1m: 0.60tools:
- name: "read_workspace_file"description: "Read a UTF-8 file using a workspace-relative path."
- name: "search_workspace_file"description: "Search for a query string in a file."execution:
max_steps: 12max_token_budget: 50000max_usd_budget: 0.25recursion_limit: 32tool_error_policy: "return_to_model"memory:
checkpoint_backend: "sqlite"checkpoint_database: "database.sqlite"workspace_root: "projects/default"When using prompt_driven execution mode, include explicit instructions in your system prompt detailing how tool calls should be formatted.
- Relative Paths Only: Instruct the model never to pass absolute paths (e.g.,
/etc/passwd,C:\...) or parent traversal sequences (..). - One Tool at a Time: In
prompt_drivenmode, ModuStash parses one fenced JSON block per turn. Tell the model not to provide surrounding text or multiple JSON blocks when making a tool call. - Honesty on Tool Output: Ensure the prompt instructs the model to rely only on data returned by executed tools and never to fake tool responses.
- Composite Key Structure: All checkpoints are keyed as
project:manifest_name:user_thread_id. If you want an agent to retain memory across requests, maintain the exact samethread_idandproject. - Single-Flight Lock: ModuStash locks threads while execution is underway. If a user or API client sends concurrent requests for the same composite key, HTTP 429 (
Thread Busy) will be returned. - Trimming Messages: Built-in context trimming uses standard LangChain strategies (
strategy="last") starting on human messages to respectmax_context_tokens.
Included in the repo are the files needed for a simple, lightweight demonstration chatbot agent: manifests/demo_chatbot.yaml
modustash inspect manifests/demo_chatbot.yamlmodustash run manifests/demo_chatbot.yaml "Hello! Explain what ModuStash is in two sentences."Run the first message:
modustash run manifests/demo_chatbot.yaml "Hi, my favorite color is teal." --thread-id demo-chat-1Follow up in the same thread:
modustash run manifests/demo_chatbot.yaml "What is my favorite color?" --thread-id demo-chat-1When you start FastAPI (uvicorn core.api:app --port 8501), demo_chatbot will automatically appear in the agent selector dropdown in the browser UI.