Pulse
README.md
# Pulse
  
Pulse is a network behavior simulation environment. It creates simulated networks where devices run real Linux networking stacks and routing protocols, enabling investigation of runtime behavior through device interaction and AI agents.
> Pulse answers: **"What is happening inside the network right now?"**
```bash
# Terminal 1 # Terminal 2
make lab make mcp
```
`make lab` builds and holds a live network simulation open; `make mcp` starts the MCP server your IDE connects to. See [Quick Start](#quick-start).
## How It Works
An AI agent (via an MCP-capable IDE like Windsurf) talks to Pulse over MCP. Pulse's backend reads and mutates a live Mininet/FRR simulation — there is no separate model of the network to fall out of sync with; the running simulation *is* the state.
```mermaid
flowchart LR
Agent["AI Agent (IDE / Windsurf Cascade)"] -- MCP over HTTP --> MCP["Pulse MCP server (FastMCP)"]
MCP --> State["state/ (read-only queries)"]
MCP --> Safety["safety/ (mutations, snapshot, rollback)"]
State --> Sim[("Live simulation\nMininet namespaces + FRR daemons")]
Safety --> Sim
Sim -.->|OSPF, ARP, interfaces,\nrunning-config| State
```
## What Pulse Is
- A simulated network environment using Mininet (Linux namespaces) and FRRouting (OSPF, BGP)
- Protocol-level investigation (OSPF adjacency, routing tables, VLAN behavior)
- Runtime fault injection (latency, loss, interface failures)
- AI agent integration via MCP (Model Context Protocol)
- Terminal-like device access for human and automated investigation
## What Pulse Is Not
- Not a production network monitoring tool
- Not a vendor CLI simulator (no Cisco IOS / Juniper JunOS)
- Not a hardware ASIC emulator
- Not a large-scale topology modeler
- Not a telemetry or time-series analysis platform (v1)
- Not a multi-user collaboration platform (MVP)
## Relationship with Graphite
Pulse is a sibling project to Graphite. They are complementary:
- **Graphite**: "What does the network look like?" (topology structure, blast-radius analysis)
- **Pulse**: "What is happening inside the network right now?" (runtime behavior, protocol debugging)
## Current State
**Project maturity**: Architecture and specification complete, execution planned. Milestones 0–5 (simulation runtime, FRR config, state reads, read-only MCP tools, mutation/safety subsystem) are implemented; Milestone 6 (hardening and expansion) has completed its entire MCP tool-catalog roadmap — every proposed resource/tool is implemented — plus the seven MVP-scope agent Skills (`docs/skills/`) and four foundational Rules (`docs/rules/`). Marketplace/IDE connectivity is verified end-to-end. Formal Observability, the Custom Agent Flow, and the frontend are explicitly deferred (ADR 0003) — not open MVP work.
Pulse has completed:
- Architecture phase (cross-cutting system concerns)
- Specification phase (implementation contracts for MVP backend)
- Execution planning phase (build order, module boundaries, process)
- MCP surface: 9 resources + 16 tools (see `docs/specs/mcp-tool-catalog.md`)
- 7 agent Skills, verified connected to Windsurf's Marketplace
See `docs/execution/EXECUTION-PHASES.md` for current Milestone status and `docs/investigations/` for evidence gathered so far.
## Quick Start
**Prerequisites**: Python 3.10+, Mininet + FRR installed as system packages (not pip-installed — see `docs/investigations/milestone-0-environment-constraints.md`).
```bash
make install # venv --system-site-packages + pip install -e ".[dev]"
make test # pytest tests/unit
```
> **The standard development workflow is two terminals:**
>
> | Terminal 1 | Terminal 2 |
> |---|---|
> | `make lab` — build the MVP topology, hold it `READY` | `make mcp` — start the MCP server, then connect your IDE |
>
> `make lab` needs `sudo` (Mininet). Leave it running; `Ctrl+C` tears the simulation down cleanly. Without a `make lab` running, MCP tools/resources will report `Simulation state: UNINITIALIZED` — there's nothing yet for them to read or mutate.
Run `make help` for the full command list. Without `make`, the equivalent raw commands are:
```bash
python3 -m venv --system-site-packages .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest tests/unit
sudo .venv/bin/pulse-lab # terminal 1
pulse-mcp # terminal 2
```
### Server startup options
```bash
pulse-mcp # http on 127.0.0.1:8000/mcp (default, validated with Windsurf Marketplace)
pulse-mcp --host 0.0.0.0 --port 9000 # override host/port
pulse-mcp --transport stdio # for an IDE that spawns Pulse as a subprocess instead
pulse-lab --topology hub-and-spoke-mvp # override topology (currently the only one)
```
### Mutation approval mode
Mutation tools (fault injection, interface state changes, etc.) require approval before execution. The approval mechanism is controlled by the `PULSE_APPROVAL_MODE` environment variable:
```bash
export PULSE_APPROVAL_MODE=chat # default: agent asks user in chat before calling tool
export PULSE_APPROVAL_MODE=mcp # uses FastMCP's elicitation callback (may not work with all clients)
export PULSE_APPROVAL_MODE=none # no confirmation (automation/testing only)
```
**CHAT (default)**: The agent asks you for confirmation in the chat interface before calling the mutation tool. The MCP server approves immediately, trusting the agent's chat confirmation. This works across all MCP clients (Windsurf, Cursor, Claude Code, VS Code) and is the recommended default for local development.
**MCP**: Uses FastMCP's `Context.elicit()` callback, which sends a blocking request to the connected MCP client. This was confirmed working with FastMCP's in-memory client but may not be supported by all MCP clients. Windsurf Marketplace, for example, does not currently support elicitation.
**NONE**: No confirmation. Mutations execute immediately. Useful for automation and testing only. Should not be used in production.
## Developer Experience
| Task | Command |
|---|---|
| Install | `make install` |
| Run unit tests | `make test` |
| **Build + hold the simulation `READY`** | **`make lab`** *(sudo)* |
| Start the MCP server (HTTP) | `make mcp` |
| Start the MCP server (stdio) | `make mcp-stdio` |
| Simulation smoke test (Milestone 0 — build topology, confirm OSPF, teardown) | `make smoke-build` *(sudo)* |
| State/MCP smoke test (Milestone 3/4 — reads + tool registration against a live sim) | `make smoke-mcp` *(sudo)* |
| Mutation smoke test (Milestone 5 — apply + roll back a mutation against a live sim) | `make smoke-mutation` *(sudo)* |
| Clean up leaked Mininet state | `make clean-mininet` *(sudo)* |
> **Note**: the `smoke-*` targets are throwaway prototype scripts (`prototypes/`) — build, assert, teardown once, then exit. They validate a specific question and are not part of `make test`/CI. `make lab` is the supported, long-running entrypoint for interactive development; see each smoke script's own module docstring for exactly what it validates.
## Connecting an IDE/Marketplace Client (e.g. Windsurf)
Point the client at Pulse's canonical HTTP endpoint. For Windsurf, add this
to `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"pulse": {
"disabled": false,
"serverUrl": "http://127.0.0.1:8000/mcp"
}
}
}
```
(Windsurf accepts either `serverUrl` or `url` for remote HTTP MCP servers —
both are equivalent; see [Windsurf's MCP docs](https://docs.windsurf.com/windsurf/cascade/mcp#remote-http-mcps).)
This has been verified end-to-end against a real Windsurf Marketplace
connection: `initialize` → `notifications/initialized` → `tools/list`, with
Marketplace successfully enumerating Pulse's tools. **Port `8000` is the
validated default** — connecting on a different local port was observed to
fail Marketplace's connection for reasons not fully understood (see
`docs/investigations/windsurf-marketplace-connectivity-root-cause.md` for
the full investigation trail); stick to the default unless you have a
specific reason to change it.
**Validation checklist** — run this in order if the IDE reports it cannot
connect, to localize the problem before changing any configuration:
1. **Confirm the server is actually running and bound where expected**:
```bash
pulse-mcp &
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/mcp
```
Any HTTP response code (even `406`) confirms the server is listening —
`curl: (7) Failed to connect` means the server isn't running or is bound
to a different host/port than the client is configured with.
2. **Confirm the full MCP handshake succeeds** (this is what a real client
does):
```bash
curl -s -i http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```
Expect `200 OK` with `content-type: application/json` and a `serverInfo`/
`capabilities` JSON-RPC response body. If this fails, the problem is
server-side (check `pulse-mcp`'s own output for a traceback) — this step
isolates server bugs from client/config issues.
3. **Confirm the URL in `mcp_config.json` matches exactly** — scheme, host,
port, and path (`/mcp`, no trailing slash). A trailing-slash mismatch
(`/mcp/` instead of `/mcp`) will 307-redirect (standard Starlette
behavior for a path that doesn't exactly match a registered route) —
harmless for `curl -L`, but some HTTP client libraries do not resend a
streaming POST body across a redirect. Use the exact canonical URL to
avoid this entirely rather than relying on redirect-following.
4. **Restart the IDE's MCP connection** after editing `mcp_config.json` —
most MCP-capable IDEs (including Windsurf) only re-read this file on an
explicit reconnect/reload, not automatically on save.
5. **If still failing**, capture the IDE's own MCP error message/log (not
just "failed to connect") — steps 1–2 above confirm the server is
spec-compliant and reachable, so a remaining failure at this point is in
the client's own request construction or config schema for that
specific IDE, not in Pulse.
## Activating Windsurf Skills and Rules
The `.windsurf/` directory is IDE-specific and is not committed to this repo. To use the canonical skills and rules after cloning, copy or symlink them into Windsurf's expected locations:
```bash
# Rules are copied as-is
mkdir -p .windsurf/rules
cp docs/rules/*.md .windsurf/rules/
# Skills need a subdirectory per skill and a SKILL.md filename
for skill in docs/skills/*.md; do
name=$(basename "$skill" .md)
mkdir -p ".windsurf/skills/$name"
cp "$skill" ".windsurf/skills/$name/SKILL.md"
done
```
Windsurf `SKILL.md` files require a YAML frontmatter block with `name` and `description` at the top. If your copy of `docs/skills/<name>.md` does not include one, prepend it before using it in Windsurf:
```yaml
---
name: <skill-name>
description: <short description>
---
```
The `docs/skills/` and `docs/rules/` files are the source-of-truth authoring versions; `.windsurf/` is a local activation copy.
## Repository Structure
```
pulse/
├── docs/
│ ├── architecture/ # Cross-cutting system concerns
│ ├── specs/ # Implementation specifications
│ ├── execution/ # Build order, module boundaries, process
│ ├── decisions/ # ADRs (Architecture Decision Records)
│ ├── topologies/ # Network topology specifications
│ ├── skills/ # Agent Skills — authoring/spec layer
│ ├── rules/ # Foundational agent rules — authoring/spec layer
│ ├── investigations/ # Evidence-gathering writeups (real bugs found, real findings)
│ └── meta/ # Documentation system, glossary, templates
├── src/backend/ # Implementation — see docs/execution/PROJECT-STRUCTURE.md
│ ├── simulation/ # Mininet/FRR runtime, topology loading, run_lab.py (dev entrypoint)
│ ├── frr/ # FRR config templates and rendering
│ ├── state/ # Read-only live-state queries
│ ├── safety/ # Mutations, snapshot/rollback (sole writer of live state)
│ └── mcp/ # FastMCP server, resources, tools
├── tests/unit/ # pytest suite (mirrors src/backend/ layout)
├── prototypes/ # Throwaway feasibility/smoke-test scripts (never imported by src/)
├── Makefile # `make help` for all common commands
├── pyproject.toml # Dependencies, `pulse-mcp` + `pulse-lab` console scripts
└── README.md # This file
```
### Module relationships
```mermaid
flowchart TD
mcp["mcp/ — FastMCP server, resources, tools, approval gate"]
state["state/ — read-only live-state queries"]
safety["safety/ — mutations, snapshot, rollback (sole writer of live state)"]
frr["frr/ — FRR config templates and rendering"]
simulation["simulation/ — Mininet/FRR runtime, topology loading"]
mcp --> state
mcp --> safety
safety --> simulation
state --> simulation
safety --> frr
frr --> simulation
```
See `docs/execution/MODULE-BOUNDARIES.md` for the authoritative description of what each module owns and may not reach across.
## Where to Start
**For understanding Pulse:**
- Start with [docs/architecture/vision-and-positioning.md](docs/architecture/vision-and-positioning.md)
**For implementing:**
- Start with [docs/execution/EXECUTION-WORKSPACE.md](docs/execution/EXECUTION-WORKSPACE.md)
- Then [docs/execution/PROJECT-STRUCTURE.md](docs/execution/PROJECT-STRUCTURE.md)
- Then [docs/execution/EXECUTION-PHASES.md](docs/execution/EXECUTION-PHASES.md)
**For autonomous coding agents:**
- See [AGENTS.md](AGENTS.md) (repository onboarding for agents)
**For navigating documentation:**
- See [docs/OVERVIEW.md](docs/OVERVIEW.md) (engineering handbook index)
## Technology Stack
- **Simulation**: Mininet (Linux network namespaces)
- **Routing**: FRRouting (OSPF, BGP)
- **Backend**: Python, FastMCP (FastAPI was considered and not adopted — FastMCP's own native HTTP transport covers Pulse's needs; see `docs/architecture/technology-decisions.md`)
- **Agent Orchestration**: IDE Flow only for MVP — the IDE's own agent runtime, no Pulse-side orchestrator (ADR 0003). LangChain is a Working Assumption for the *Custom Agent Flow only*, which is deferred; see `docs/architecture/technology-decisions.md`.
- **Frontend**: Deferred for MVP (see ADR 0003)
## Documentation System
Pulse uses a structured documentation system defined in [docs/meta/DOCUMENTATION-SYSTEM.md](docs/meta/DOCUMENTATION-SYSTEM.md). All documents follow maturity labeling (Established, Working Assumption, Exploration, Deferred, Superseded, Rejected) and carry explicit metadata.
## Decision Records
Significant architectural decisions are recorded as ADRs in [docs/decisions/](docs/decisions/):
- 0001: Pulse owns the simulation lifecycle; one active simulation for MVP
- 0002: The live simulation is the source of truth; config is seed
- 0003: Defer the Custom Agent Flow; MVP is IDE-Flow-only
- 0004: Two approval levels for MVP (read-only vs. mutation)
- 0005: MVP OSPF scope: single-area, adjacency + propagation only
- 0006: Local Unix-socket RPC boundary between the Runtime (`make lab`) and MCP (`make mcp`) processes
## MVP Scope
**Topology**: Hub-and-spoke with 3 switches (Access-SW1, Access-SW2, Core-L3-SW) and 3 endpoints (Host-A, Host-B, Server)
**Protocols**: VLAN tagging, trunking, SVI, basic routing (static and simple OSPF), ARP, ICMP
**Agent Flow**: IDE Flow only (Windsurf IDE agent via MCP). Custom Agent Flow deferred.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues