diff --git a/LICENSE b/LICENSE
index 49559f0..be3f7b2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,3 @@
-Swarm Map
-Copyright (C) 2025-2026 Juniper Bevensee and contributors
-
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..0c4e4e4
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,19 @@
+Swarm Map
+Copyright (C) 2025-2026 Juniper Bevensee and contributors
+
+A commons, public goods project of NimbleCo (https://www.nimbleco.ai/).
+Formerly Hermes Swarm Map / HSM.
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version. See the LICENSE file for the full license text.
+
+Why this file exists
+--------------------
+The project name and copyright line used to sit at the top of LICENSE. GitHub
+classifies a repository's license by matching LICENSE against known license
+texts, and the prepended header was enough to make it report "Other /
+NOASSERTION" — which contradicted the AGPL v3 claim in the README and on the
+website. LICENSE is now the verbatim GNU AGPL v3 text so the classifier can
+recognise it, and the project's own attribution lives here instead.
diff --git a/README.md b/README.md
index 33f35df..5376aa3 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,14 @@
A commons, public goods project of [NimbleCo](https://www.nimbleco.ai/).
-**Multiplayer admin and orchestrator platform for Hermes.** Deploy, manage, and monitor multiple [Hermes Agent](https://github.com/NimbleCoAI/hermes-agent) instances from one dashboard — with built-in multi-tenant security, model cascades, and platform connections.
+**Multiplayer admin and orchestrator platform for heterogeneous agent runtimes.** One dashboard for two shapes of agent: [Hermes](https://github.com/NimbleCoOrg/hermes-agent-mt) agents, where each agent is its own container, and [Letta](https://github.com/letta-ai/letta) agents, where memory-first agents live as rows on one shared server. Multi-tenant security, model cascades, and platform connections come from the same control plane either way.
-*First of its kind, a point and click GUI for not just managing Hermes runtimes, but also who can do what and where. Solves the multi-tenant Hermes problem. View the godhead of complexity without derealizing. Share compute.*
+*First of its kind, a point and click GUI for not just managing agent runtimes, but also who can do what and where. Solves the multi-tenant agent problem. View the godhead of complexity without derealizing. Share compute.*
+
+**Runtime support is not symmetrical yet.** Hermes is the mature path; Letta support is read-only plus send-a-message. See [Runtimes](docs/runtimes.md) for the exact shipped/not-shipped line — we would rather you read a boring matrix than discover the gap after deploying.
-*Calm UX showing a variety of config settings for different hermes harness runtimes*
+*Calm UX showing a variety of config settings across agent harness runtimes*
---
@@ -17,7 +19,9 @@ A commons, public goods project of [NimbleCo](https://www.nimbleco.ai/).
AI agents are most useful when they're always on — running on a server, reachable from your phone, remembering context across conversations. But running *multiple* agents across *multiple* platforms for *multiple* users? That's where it gets hard.
-Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet of Hermes agents — each with its own personality, memory, platform connections, and budget. Everything a single agent can do, but multiplied and multiplayer.
+Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet of agents — each with its own personality, memory, platform connections, and budget. Everything a single agent can do, but multiplied and multiplayer.
+
+It also gets harder when your agents aren't all the same *shape*. A Hermes agent is a container you can start, stop, rebuild, and attach messaging surfaces to. A Letta agent is a row in a shared server's database with a memory-first architecture — no container of its own, nothing to `docker restart`. Swarm Map models both behind one runtime seam (`ContainerRuntimeAdapter` for container runtimes, a separate REST provider for Letta) instead of pretending everything is a container.
## What People Build With It
@@ -41,6 +45,31 @@ Swarm Map is the control plane. One UI to deploy, configure, and manage a fleet
💰 **Budget Enforcement** — Set monthly spend limits per API key. Agents self-throttle when budget is exceeded.
+🧩 **Two Runtimes, One Seam** — Hermes (container-per-agent) and Letta (memory-first agents as rows on one shared server) in the same fleet view. Feature parity is partial and documented in [Runtimes](docs/runtimes.md); the six bullets above describe the Hermes path.
+
+---
+
+## Runtimes
+
+Swarm Map manages two agent runtimes. They are **not** at parity, and the docs say so on purpose.
+
+| | **Hermes** | **Letta** |
+|---|---|---|
+| Agent is | a container | a row on a shared server |
+| Deploy from the wizard | yes | yes (brings the shared server up, creates the agent) |
+| Start / stop / restart / rebuild | yes | n/a — no container to control (the *server* is a container and does have these) |
+| Read agent state | yes | yes — fleet list, core-memory blocks, model handle |
+| Send a message from the UI | yes | yes |
+| Edit memory, reconfigure, delete agent | yes | **not shipped** — the Letta REST surface in Swarm Map is read-only plus send-message |
+| Messaging surfaces (Signal / Telegram / Mattermost / Discord) | yes, direct | only via a Hermes front that proxies a group conversation to the agent |
+| Git-backed memory files (memfs) | n/a | **not enabled** — we never request `git_enabled`, so agents take the server default (`false`) |
+| Group approval + audit | yes | inherited when reached through a Hermes front |
+| Budget enforcement | yes | **does not cover proxied Letta turns** — see below |
+
+**Budget caveat, stated plainly.** Group approval and the audit trail inherit cleanly through a Hermes front, because they gate the turn before it runs. Budget does **not** inherit — on any path. Swarm Map's monthly-spend check sums `input_tokens` / `output_tokens` from each agent's `state.db` `sessions` table, and the Letta bridge never writes those columns: it records only `last_prompt_tokens` (context size). So proxied Letta spend is not counted and a soft cap is never approached. Do not deploy a Letta agent behind a Hermes front expecting the front's budget cap to bound it.
+
+Full detail, including the route inventory that backs these claims: [docs/runtimes.md](docs/runtimes.md).
+
---
## Quick Start
@@ -53,7 +82,7 @@ pnpm seed # first run: writes settings + tier config
pnpm dev # http://localhost:3000
```
-On first launch, the setup wizard detects your Hermes compose directories automatically. Point it at your agent directory and go.
+On first launch, the setup wizard detects your Hermes compose directories automatically. Point it at your agent directory and go. Choosing the Letta runtime in the wizard instead brings up the shared Letta server and creates the agent on it.
**New here?** Read the [Getting Started guide](docs/getting-started.md) for a full walkthrough.
@@ -61,13 +90,15 @@ On first launch, the setup wizard detects your Hermes compose directories automa
- **Node.js 18+**
- **Docker** running locally (used for container management)
-- **Hermes Agent** instances — see [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
+- For the Hermes runtime: **Hermes Agent** instances — Swarm Map deploys the multi-tenant fork [NimbleCoOrg/hermes-agent-mt](https://github.com/NimbleCoOrg/hermes-agent-mt), a fork of [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
+- For the Letta runtime: a **Letta server** — the wizard brings one up for you via `docker/letta-compose.yml`
---
## Features
- **Discovery** — auto-detect Hermes agent containers and compose files
+- **Letta fleet view** — list agents on a configured Letta server, read their core memory, send a message (read-only otherwise)
- **Per-agent configuration** — edit env vars, SOUL.md personas, surface connections
- **Model cascade** — ordered fallback chains across providers (Anthropic, OpenAI, Bedrock, etc.)
- **Surface management** — connect agents to Telegram, Signal, Mattermost
@@ -155,6 +186,18 @@ Any AI agent (Claude Code, Hermes, etc.) can orchestrate your fleet via the REST
| `GET` | `/api/audit` | Audit log (`?who=&what=&since=`) |
| `GET/PUT` | `/api/settings` | App settings |
+Letta runtime — **read-only plus send-message.** This is the complete surface; there is no `PATCH`, `PUT`, or `DELETE` for Letta agents:
+
+| Method | Path | Description |
+|--------|------|-------------|
+| `GET` | `/api/letta/harnesses` | Letta server + its hosted agents, as harness rows |
+| `GET` | `/api/letta/agents` | List agents on the Letta server |
+| `GET` | `/api/letta/agents/:id/blocks` | Core-memory blocks (read-only) |
+| `GET` | `/api/letta/agents/:id/files` | Context-file view, when the server exposes one |
+| `POST` | `/api/letta/agents/:id/messages` | Send a message, get the turn back |
+
+Creating a Letta agent goes through `POST /api/setup/deploy` with the Letta runtime selected. Editing memory, reconfiguring, and deleting Letta agents are not shipped.
+
```bash
# Examples
curl http://localhost:3000/api/harnesses
@@ -183,6 +226,7 @@ Settings are stored at `~/.hermes-swarm-map/settings.json`. API keys are encrypt
## Documentation
- [Getting Started](docs/getting-started.md) — deploy your first agent in 5 minutes
+- [Runtimes](docs/runtimes.md) — Hermes vs Letta: what's shipped, what isn't, and the evidence
- [Migrating Existing Agents](docs/migrating.md) — upgrade path for existing Hermes users
- [Platform Setup](docs/platforms.md) — Signal, Telegram, Mattermost, Google Workspace guides
- [Image vs HSM Boundary](docs/architecture/image-vs-hsm-boundary.md) — what belongs in the Docker image vs HSM scaffolding
@@ -194,4 +238,6 @@ Settings are stored at `~/.hermes-swarm-map/settings.json`. API keys are encrypt
## License
-[AGPL v3](LICENSE). You can use, modify, and deploy this software freely. If you modify it and expose it over a network (e.g., as a hosted service), you must make your modified source code available under the same license. Self-hosting for your own agents requires no source disclosure.
+Swarm Map — Copyright (C) 2025-2026 Juniper Bevensee and contributors. See [NOTICE](NOTICE).
+
+Licensed under [AGPL v3](LICENSE). You can use, modify, and deploy this software freely. If you modify it and expose it over a network (e.g., as a hosted service), you must make your modified source code available under the same license. Self-hosting for your own agents requires no source disclosure.
diff --git a/app/(setup)/setup/wizard/page.tsx b/app/(setup)/setup/wizard/page.tsx
index f63f3d4..de09044 100644
--- a/app/(setup)/setup/wizard/page.tsx
+++ b/app/(setup)/setup/wizard/page.tsx
@@ -449,7 +449,7 @@ export default function WizardPage() {
/>
Letta swarm
- A stateful agent on the shared Letta server (memfs memory). No container of its own; messaging comes later via a Hermes front.
+ A memory-first agent on the shared Letta server — a row, not a container of its own. Read-only in Swarm Map after deploy, plus sending a message; messaging surfaces come later via a Hermes front.
@@ -587,7 +587,7 @@ export default function WizardPage() {
- Letta agents use memfs memory (git-backed context files) and have no per-agent fallback model or bundled Ollama — those are Hermes-only. + Letta agents keep their memory in core-memory blocks on the shared server. Letta's git-backed memory files (memfs) are not enabled on this deploy path yet, so the context-files view will be empty. Letta agents also have no per-agent fallback model and no bundled Ollama — those are Hermes-only.
A Letta agent is a stateful brain on the shared server — it's reached over REST, not Signal/Telegram/Slack directly. Multiplayer messaging arrives via a separate Hermes front that proxies a group conversation to this agent (v1 bridge). Nothing to configure here.
++ Through that front, group approval and the audit trail carry over — they gate the turn before it runs. Budget enforcement does not carry over at all. The spend check sums the token columns an agent writes to its own state DB, and the Letta bridge never writes them, so proxied turns count as zero spend on every path. Do not rely on the front's budget cap to bound a Letta agent. +
)} diff --git a/components/harness/letta-agent-detail.tsx b/components/harness/letta-agent-detail.tsx index 1ef943c..df4c638 100644 --- a/components/harness/letta-agent-detail.tsx +++ b/components/harness/letta-agent-detail.tsx @@ -11,14 +11,20 @@ * Lifecycle buttons → Send message (Reset/Delete deferred to a later phase) * Overview CPU/mem → model handle + agent identity * Logs tab → message / turn viewer (send + rendered reply) - * Memory rows → core-memory blocks + memfs context-file view (A3) + * Memory rows → core-memory blocks (+ a context-file view, if enabled) * - * MEMFS (2026-07-21 research, memory/specs/2026-07-21-letta-memfs-api-and-a3.md): - * a modern Letta agent's memory is a git-backed file tree; `system/` files are - * pinned into the prompt, the rest open on demand. We surface BOTH the - * core-memory blocks (`/core-memory/blocks`) and the live file view (`/files`). - * Everything here is read-only except "send a message". Block/file WRITES and - * the memfs↔REST live-sync semantics need a live server (Phase 4). + * MEMFS IS NOT ENABLED — DO NOT WRITE COPY THAT PROMISES IT. + * The 2026-07-21 research (memory/specs/2026-07-21-letta-memfs-api-and-a3.md) + * describes memfs as a git-backed memory file tree with `system/` files pinned + * into the prompt. That is Letta's capability, not ours yet: we never send + * `git_enabled` (zero occurrences in executable code — only doc comments like + * this one), so agents created by our deploy path take the server default, + * observed as `git_enabled: false`. So the `/files` view below is + * normally EMPTY, and its empty state must say "not enabled" rather than imply + * the feature is there and the agent simply has no files. + * + * Everything here is read-only except "send a message". Block/file WRITES, + * enabling memfs, and the memfs↔REST live-sync semantics are all unshipped. */ import { useState } from 'react' @@ -77,8 +83,8 @@ export function LettaAgentDetail({ harness }: { harness: Harness }) {{isServer - ? 'Letta server (a real container). Hosts N agents as Postgres rows — start/stop/logs via Docker are available on the server, agents are managed over REST.' - : 'Letta agent — a row in the Letta server’s Postgres, not a container. No CPU/memory, no lifecycle buttons; managed over REST.'} + ? 'Letta server (a real container). Hosts N agents as Postgres rows — start/stop/logs via Docker are available on the server; the agents themselves are reached over REST.' + : 'Letta agent — a row in the Letta server’s Postgres, not a container. No CPU/memory, no lifecycle buttons. Read-only over REST, plus sending a message: editing memory, reconfiguring and deleting are not shipped.'}
{/* Overview: model handle + identity, not CPU/mem (design §4a) */} @@ -131,14 +137,18 @@ function ServerAgentsList() { ) } -/** Core-memory block viewer — the seed layer that projects into memfs system/ files (design §4a). */ +/** + * Core-memory block viewer. This is the agent's actual live memory surface on + * our deploy path, because memfs is not enabled — the "blocks project into + * system/ files" story only applies once git-backed memory is turned on. + */ function MemoryBlocks({ agentId }: { agentId: string }) { const { data: blocks, loading, error } = useApi
- Read-only. Under memfs these blocks project into system/ files (e.g. persona.md). Editing lands with the live server (Phase 4).
+ Read-only — editing memory blocks from Swarm Map is not shipped. These blocks are this agent's memory: git-backed memory files (memfs) are not enabled, so nothing is projected into system/ files yet.
Loading…
} {error &&Couldn't read memory: {error}
} @@ -170,25 +180,44 @@ function MemoryBlocks({ agentId }: { agentId: string }) { } /** - * memfs context-file view — the "what's in this agent's memory right now" - * surface (GET /v1/agents/{id}/files). `system/` files are pinned into the - * prompt every turn; others open on demand. The open/pinned marker is a - * CONTEXT-LOADING indicator, NOT an access-control boundary (memfs has no - * per-file ACL — see the airlock note in the spec). + * Context-file view (GET /v1/agents/{id}/files) — Letta's memfs surface. + * + * HONESTY: memfs is NOT enabled on our deploy path (`git_enabled: false`), so + * this list is empty for every agent we create. The panel therefore leads with + * an explicit "not enabled" state instead of an empty box under a heading that + * promises git-backed memory. The file list still renders if a server does + * expose one, so this stays useful for externally-created agents. + * + * When files ARE present: `system/` files are pinned into the prompt every + * turn; others open on demand. The open/pinned marker is a CONTEXT-LOADING + * indicator, NOT an access-control boundary (memfs has no per-file ACL — see + * the airlock note in the spec). */ function ContextFiles({ agentId }: { agentId: string }) { const { data: files, loading, error } = useApi
- The agent's git-backed memory file tree. Pinned = a system/ file loaded into the prompt every turn; open = currently in context. This is a loading indicator, not a permission boundary.
-
+ The agent's git-backed memory file tree. Pinned = a system/ file loaded into the prompt every turn; open = currently in context. This is a loading indicator, not a permission boundary.
+
Loading…
} {error &&Couldn't read context files: {error}
} {files && files.length === 0 && ( -No context files (agent may predate memfs, or files aren't exposed on this server).
++ Git-backed memory files are not enabled for agents Swarm Map deploys, so there is nothing to show here. Core memory (blocks) above is this agent's live memory. Enabling memfs is not shipped yet. +
)} {files && files.length > 0 && (