SPOT v2 Roadmap¶
Approved on 2 October 2026. Status: v2.0 is in pre-release; 2.0.0-beta.8, of 14 October 2026, is the latest.
Summary¶
SPOT v2 first becomes a plugin platform that can be trusted, then a product people run on real mail. v2.0 Foundation carries every breaking change; three additive minor releases follow (Ecosystem, Context, Adoption). There's no deadline: work follows the execution order below, at its own pace.
- Direction: grow the plugin ecosystem first, then production adoption. The core idea stays spear-phishing detection using the organisation's own context, through the Knowledge Store and MCP.
- Users, in priority order: mail/sys admin, plugin developer, detection engineer, SOC analyst, researcher.
- First production installation: Sonn's own Postfix mail, in observe mode.
- Releases: v2.0 Foundation, v2.1 Ecosystem, v2.2 Context, v2.3 Adoption.
Every decision below comes from the review session of 1-2 October 2026 (Q1-Q48) and the ruling of 12 October 2026 (Q49), all listed in the decision log at the end.
Starting point (SPOT 1.3.2)¶
SPOT 1.3.2 doesn't protect a mailbox end to end: the only mail path is broken, verdicts trigger no action, and plugins run with more privileges than they need. Each finding from the 1 October 2026 review is tracked as a GitLab issue under the v2.0 milestone; security findings are confidential issues until fixed.
Strategy¶
SPOT grows its plugin ecosystem first and goes after production adoption second, and it keeps organisation context as its core bet.
- Goal (Q1): plugin ecosystem now, production adoption next.
- Users (Q2), in priority order: mail/sys admin, plugin developer, detection engineer, SOC analyst, researcher.
- Target mail stack (Q3, Q45): mostly Microsoft 365, plus a real on-prem Postfix minority. Sonn's own mail runs on Postfix.
- Tenancy (Q4): one organisation per installation, with no tenant concept in the code.
- Data leaving SPOT (Q5, Q19): only through opt-in plugins, each labelled
none,indicatorsorcontent. Acontentplugin needs explicit admin consent. - Change appetite (Q6): a few people, and no known production installation. Breaking changes are fine if versioning and migration guides follow best practice.
- Thesis (Q7): make organisation-context detection real by building the first analyzers that read the Knowledge Store, and add MCP as a second, live context channel.
Plugin platform¶
Each plugin gets its own scoped identity and sits on a network that can't reach SPOT's data stores. Core calls plugins over HTTP, and plugins call back only through the gateway and the context service.
flowchart TB
subgraph core["Core network"]
stores[("Postgres, Redis, RabbitMQ<br/>core network only")]
gw["api-gateway<br/>public API, mints plugin tokens"]
orch["orchestrator<br/>runs workflows, dispatches verdicts"]
ctx["context service<br/>Knowledge Store, brokers MCP calls"]
pm["plugin-manager<br/>internal only, via socket proxy"]
end
subgraph plugins["Plugin network: each plugin holds one scoped token"]
retr["Retrievers<br/>milter, M365 / scope: ingest"]
resp["Responders<br/>act on verdicts / scope: results"]
anl["Analyzers<br/>knowledge:read, content:read"]
prov["Providers<br/>knowledge:write, own source only"]
mcp["MCP servers<br/>read-only tools, or external"]
end
retr -- ingest --> gw
orch -- verdicts --> resp
orch -- analyze --> anl
anl -- context --> ctx
prov -- write --> ctx
ctx -- MCP calls --> mcp
pm -- runs --> plugins The plugin-manager is the only service that touches Docker, and it can't be reached from the internet.
- Identity and isolation (Q9, v2.0):
- At install, each plugin receives its own token, with scopes set by its kind:
ingestfor retrievers,knowledge:readandcontent:readfor analyzers,knowledge:writeon their own source for providers,resultsfor responders. - Plugins run on a plugin network that reaches only the gateway and the context service, never Postgres, Redis or RabbitMQ.
- A new internal
plugin-managerservice starts and stops plugin containers through a Docker socket proxy limited to container and image calls. The gateway no longer mounts the socket.
- At install, each plugin receives its own token, with scopes set by its kind:
- Plugin kinds (Q10, Q41, Q49):
analyzer,context_provider,mail_retriever, plus two new ones,responderandmcp_servers. Every plugin has exactly one kind (Q49): the milter is a retriever that applies the verdict while the sender waits, and the M365 connector is two plugins, a retriever and a responder. - Action layer (Q10, v2.0):
- Workflows declare
on_verdict:, which maps verdict classes to responders. - A dispatcher in core consumes
spot.resultsand calls each responder over HTTP, so plugins never touch RabbitMQ.
- Workflows declare
- MCP (Q11, Q41, v2.2):
- MCP servers are plugins of kind
mcp_servers. An external server is a manual entry, like a manually added plugin. - The knowledge service grows into a context service that holds every MCP connection. The SDK offers
ctx.fetch()for the Knowledge Store andctx.call_tool()for MCP. - Each workflow stage lists the tools it may call and their limits. Every call is audited, and only read-only tools are allowed, because the email body is written by the attacker and can try to steer an LLM into calling tools.
- SPOT as an MCP server comes in v2.3.
- MCP servers are plugins of kind
- Distribution (Q12, v2.1):
- A curated git index,
spot-plugin-index. Each entry gives the id, kind, image pinned by digest, publisher, signing key, compatible SPOT versions and a tier (official, verified, community). - Signatures are checked at install. Operators can pick a version and roll back.
- Registry scanning stays for private registries.
- A curated git index,
- Egress labels (Q19, v2.0):
spot.plugin.egress: none | indicators | content, shown at install and as a dashboard badge.contentneeds admin consent. - Compatibility (Q33):
- The contract version equals the SDK major version, and plugins declare the one they target (
spot.plugin.contract). - The previous contract stays accepted for at least two minor releases, with a deprecation badge.
- Each major release comes with a migration guide. v2.0 drops v1, since no one is using it.
- The contract version equals the SDK major version, and plugins declare the one they target (
- Secrets (Q46, v2.0):
- Fields marked secret are stored in
spot.yamlencrypted withENCRYPTION_KEYand reach the plugin as files under/run/secrets, never as environment variables. - The SDK settings loader reads them without the plugin author doing anything, and
spot-cli backupincludes the key.
- Fields marked secret are stored in
- One plugin at a time (Q35, Q44): every plugin has its own repo, version and release, and can be installed or removed on its own.
Contract v2 (v2.0)¶
Core parses each email once and stores the raw copy; analyzers return verdict classes with the signals behind them; LLM access goes through one client.
- Email content (Q20):
- Core stores the raw
.emlonce, encrypted, in Postgres. Queue messages carry the email id, not the content. - Core parses the email once, for every analyzer: headers in their original order, the MIME tree, decoded text and HTML, the URLs, and attachment metadata and hashes.
Authentication-Resultsheaders are trusted only when your own mail servers added them.- Analyzers receive that parsed view. They fetch raw bytes or attachment content on demand through the gateway, using the
content:readscope. - The retention purge deletes the stored copies too. Inbound
X-SPOT-*headers are stripped when mail arrives.
- Core stores the raw
- Verdict (Q21):
- Classes:
malicious,suspicious,spam,benign,unknown. - A score from 0 to 100, and signals, each with a name, weight and evidence. Signal names are open strings, so third parties can add their own.
- A failed analyzer returns
unknown, neverbenign. - Each workflow sets a verdict policy: a weighted score with thresholds, plus decisive signals that set the class directly (for example, a MISP hit means
malicious).
- Classes:
- LLM client (Q22):
- One SDK client for the OpenAI-compatible API, which Ollama, vLLM and llama.cpp all serve. It supports tool calling and JSON-schema output.
- The email always goes into the prompt as clearly delimited data, never as instructions.
- GPU support comes through a compose profile. Cloud APIs are possible only as a plugin labelled
content.
- Specs (Q34): OpenAPI specs are generated from the code in CI, for the gateway API and for the plugin contracts (analyze, sync, ingest, respond). The TypeScript SDK is dropped.
Detection¶
The order: v2.0 fixes the analyzers that exist, v2.2 builds the context analyzers and the URL and attachment analyzers, and v2.3 adds multilingual support and detonation (Q23, Q24).
| Work | Release | What it does |
|---|---|---|
| Fix existing analyzers | v2.0 | Failures return unknown. The rules analyzer stops flagging all Cyrillic mail as homoglyph attacks. ML and NLP are retrained on data that doesn't overlap the benchmark sets. All analyzers move to SDK v2 and create_analyzer_app. |
analyzer-identity | v2.2 | Rule-based checks for: display-name impersonation of employees or VIPs (from the Knowledge Store); lookalikes of your own or your vendors' domains; a first-time sender asking for payment or credentials; a Reply-To that points elsewhere; thread hijacking (In-Reply-To/References compared with past mail). |
| LLM analyzer with context | v2.2 | Makes the final judgement from the identity signals plus Knowledge Store documents and MCP tools. |
sender-history plugins | v2.2 | A responder that learns who mails whom from verdicts, and a provider that writes it to the Knowledge Store; or one responder that writes to the Knowledge Store itself, which needs knowledge:write, a scope only providers get today (Q49). The split is decided when it is designed. |
vendor-list provider | v2.2 | Vendor master data from a CSV file or an ERP's MCP server. |
analyzer-url | v2.2 | Expands shortened links; flags links to raw IP addresses and punycode domains; checks domain age via RDAP (labelled indicators); uses the URLhaus and OpenPhish feeds, cached locally. |
analyzer-attachment | v2.2 | Static analysis only: QR codes in images, PDFs and Office files; phishing hidden in SVG or HTML; Office macros (oletools); archive listings; local YARA rules. |
| Multilingual NLP | v2.3 | Language detection, then French, German and Luxembourgish models. |
| Detonation plugins | v2.3 | URLs through CIRCL Lookyloo, files through CIRCL Pandora. Before adopting Pandora, check a reported path-traversal advisory against the release to deploy. |
Mail integration¶
Postfix gets a milter in v2.0, and Microsoft 365 gets a connector in v2.3. Every new connection starts in observe mode.
- Postfix milter (Q26, v2.0):
- Replaces
retriever-smtp. - Runs the whole workflow while the sender waits. Each route has its own time limit; on timeout, it either accepts and tags the mail or asks the sender to retry later.
- Actions: reject, quarantine (Postfix hold queue), add headers, tag the subject.
- In observe mode, use
milter_default_action = accept, so mail is never lost if SPOT fails. - Also works with Sendmail.
- Replaces
- Observe, then enforce (Q40, v2.0):
- Each connection and route has
mode: observe | enforce, and new connections always start inobserve. - The dashboard shows what would have happened, and switching to
enforcetakes one click.
- Each connection and route has
- Connection wizards (Q40):
- Postfix: generated
main.cfmilter lines plus a connection test, shipped with the milter. - Microsoft 365: the Entra app permissions, the PowerShell that limits access through RBAC for Applications, and a connection test, shipped with the connector's retriever.
- Postfix: generated
- Microsoft 365 connector (Q25, Q49, v2.3): two plugins, a retriever that picks up new mail and a responder that applies the verdict.
- An Entra app with the
Mail.ReadWriteapplication permission, limited to a mail-enabled group through RBAC for Applications. - New mail is picked up by Graph delta queries every 15-30 s. Webhooks come later, only if that delay proves too slow.
- Defaults: a "SPOT: suspicious" category on suspicious mail; malicious mail moved into a hidden quarantine folder, recoverable and never hard-deleted; analysts release mail from the dashboard. Graph can't rewrite a delivered message, so warning banners aren't possible.
- An Entra app with the
- User-reported mail (v2.3): the M365 retriever watches the mailbox that Outlook's built-in Report button sends to, and SPOT triages each reported email.
- First installation (Q42, Q45): Sonn's own Postfix, in observe mode, as soon as the milter works.
Workflows, playground and evaluation¶
v2.0 makes workflows versioned and routable. v2.1 adds one testing engine that serves plugin authors, benchmarks and the playground.
- History (Q27, v2.0): the gateway writes
spot.yamlatomically, then commits it to a local git repo inside the config volume, with the user who made the change as author. That gives diff, rollback and an audit trail for every config change. - Routing (Q27, v2.0):
- An ordered
routing:list matches on source, recipient group or sender domain and picks a workflow. - A
shadow:list names candidate workflows that run beside production and only record the differences. - The milter's fast path is just another route.
- An ordered
- Repairs (v2.0): workflow test/clone/history implemented; per-analyzer results saved; timeouts enforced; cancel works; a condition on a skipped stage no longer fails the job.
- Testing engine (Q13, v2.1):
- Ships with the SDK as
spot-sdk[testing]. spot-cli plugin newcreates a plugin from the copier template.spot plugin testruns a plugin's container againsttests/emls/{phish,benign}/, checks the contract and reports precision and recall.- The same engine runs whole-workflow benchmarks. Each run is stored in Postgres with the workflow, plugin and model versions, the seed and a dataset hash, plus per-analyzer metrics and latency, and is shown in the dashboard.
- Benchmark runs stay under the platform's rate limit: they wait when it answers 429, and never switch rate limiting off (decided on 9 October 2026).
- Ships with the SDK as
- Playground (Q14, v2.1): a dry run on one email, a backtest over stored mail, and shadow mode with its differences.
- Feedback (Q28, v2.1):
- Analysts can relabel any analysis with a verdict class and a note.
- A relabelled email can be promoted into a test dataset. That copy is exempt from the retention purge until removed, and the exemption is documented.
- Labels also feed
sender-historyand a per-organisation allow/block list.
- Dashboard (Q18): build on
design/chocolatier-overhaul, merged in v2.0 together with the core commits it still waits on. Keep the HTMX app and form-based workflow builder, and spend the effort on testing, backtests, shadow mode, history and diffs.
Operations and deployment¶
Docker Compose stays the only deployment target (Q16). The goal is a first verdict in under 15 minutes on a fresh host.
spot-cli setup(Q16, Q38, v2.0):- Generates every secret and checks prerequisites first: DNS, ports, RAM and disk, Docker version.
- Installs a profile through the plugin-manager, sets a matching default workflow and sends a test email.
- Profiles:
minimal, the default (rules, identity and URL analyzers; CPU only, about 4 GB of RAM, no LLM);standard(adds NLP and a 3-4B-parameter LLM via Ollama on CPU);full(adds GPU, a larger model and detonation plugins). - Until v2.2 ships the identity and URL analyzers,
minimalis rules plus NLP.
spot-cli backup / restore / upgrade(v2.0): a backup is taken before each migration, and it covers the database, the config volume andENCRYPTION_KEY.- Deploy compose (v2.0):
- Resource limits and log rotation.
- Database ports not published; a Redis password required.
- Every service waits for migrations to finish.
- Offline bundle (v2.3): images and models in one archive, for networks with no internet access.
- Model weights (Q39):
- Built into each analyzer image, so the image digest identifies the model. Retraining means releasing a new image.
- Runtime images drop gcc, git and Poetry. Ollama models stay in their own volume.
- Observability (Q29, v2.0):
- Logs that are valid JSON, and correlation IDs carried through to plugins.
- An audit log: git history for config, the database for logins and installs.
- A Prometheus
/metricsendpoint on each core service. No bundled Prometheus or Grafana, and no tracing.
- Single sign-on (Q30, v2.3): standard OIDC tested with Entra ID and Keycloak; roles come from the identity provider's groups; local accounts stay for bootstrap and emergency access.
- CLI instead of a TUI (Q17, v2.1): a global
--output table|json|yamloption,.emlsubmission,spot-cli watchfor live jobs and health, and an interactive setup wizard.
Repositories, tooling and CI¶
The platform moves into one spot monorepo, every plugin keeps its own repo, and GitLab stays the main forge and CI/CD (Q15, Q35, Q37, Q44).
| Repo | Contains | Versioning |
|---|---|---|
spot (today's core) | core, web-dashboard, deploy, cli, and docs as one MkDocs site. The root audits and plans move to docs/decisions/. | One version for all of it |
sdk | Python SDK and the testing extra. analyzer-fake-llm becomes a test fixture here. | Own semver; major = contract version |
| One repo per plugin | analyzer-nlp, -ml, -llm, -misp, -rules, provider-employee-dir, retriever-milter, future plugins | Own version and release each |
spot-plugin-template | The copier template every plugin is created from. copier update pushes template changes into existing plugins. | Own version |
spot/ci-templates | Shared GitLab CI templates, pulled in with include: | n/a |
spot-models | ml-training-pipeline and nlp-pipeline merged: reproducible training, a record of the data each model was trained on, model cards with honest metrics | Own version |
spot-plugin-index | The curated plugin index | n/a |
datasets | Unchanged, stays private | n/a |
Archived: documentation, discussions, retriever-smtp (once the milter ships), retriever-postfix, email_scanner, analyzer-fake-llm, ml-training-pipeline, nlp-pipeline.
- Tooling (Q36): uv workspaces.
sharedbecomes a proper package, there's one lockfile per repo, and thePYTHONPATH=/app/sharedworkaround goes away. The plugin template uses uv too. - CI and forge (Q15, Q37): GitLab is the main forge and CI/CD, with Renovate for dependency updates. Codeberg stays a mirror and keeps its Forgejo CI unchanged. A move from Codeberg to GitHub is planned later.
- Contributions (Q31): Codeberg is the public place for issues and pull requests until the GitHub move. Maintainers merge outside pull requests through GitLab.
- Releases: each versioned unit keeps its own CHANGELOG, and every tag annotation carries that version's changelog entries.
Release plan¶
Every breaking change ships in 2.0.0, and everything that only adds something ships in a 2.x minor release (Q32). Ecosystem tooling comes before the context analyzers, so they can be measured from their first day.
flowchart TB
v20["<b>v2.0 Foundation</b> (every breaking change)<br/>Security: scoped plugin tokens, plugin network, socket proxy, encrypted plugin secrets<br/>Contract v2: parsed email and raw copy, verdict classes and signals, responders, egress labels<br/>Repairs: ingest path, saved per-analyzer results, timeouts, retention purge, dead-letter queue<br/>Postfix milter, observe/enforce, git-backed config with routing, LLM client, analyzer fixes<br/>Dashboard redesign, setup/backup/upgrade, metrics, audit log, repo consolidation, uv"]
g1{{"Gate: 2.0.0 released, and Sonn's Postfix mail runs through SPOT in observe mode"}}
v21["<b>v2.1 Ecosystem</b><br/>Signed plugin index, plugin template, testing engine and spot plugin test<br/>Benchmarks in the dashboard, playground (dry run, backtest, shadow), analyst feedback, CLI"]
g2{{"Gate: testing engine live, so the context analyzers are measured from day one"}}
v22["<b>v2.2 Context</b><br/>Context service brokering MCP, analyzer-identity, LLM analyzer with context<br/>Sender-history and vendor-list providers, analyzer-url, analyzer-attachment"]
v23["<b>v2.3 Adoption</b><br/>Microsoft 365 connector and wizards, OIDC sign-on, triage of user-reported mail<br/>Multilingual NLP, detonation plugins, offline bundle, SPOT as an MCP server"]
v20 --> g1 --> v21 --> g2 --> v22 --> v23 Execution order¶
There's no deadline: v2.0 work proceeds in this order, at its own pace. This was decided on 2 October 2026 and replaces the hack.lu target from Q47.
- Repair the ingest path, build
retriever-milter, and run it on Sonn's Postfix in observe mode withmilter_default_action = accept. - The security fixes (plugin tokens, plugin network, socket proxy, refresh-token check, header stripping) and the dashboard redesign merge.
- Contract v2: the parsed email view and the verdict model (classes, score, signals).
- Benchmarks re-run on held-out sets that share no data with any training set (
nahmiasd_spear_phishing,miltchev,pashakhin), and those numbers published. - The rest of v2.0. Repo consolidation and the uv move come last, because they change no behaviour.
Work is tracked in GitLab: one group milestone per release, one issue per item, and one merge request per issue.
Not planned¶
The following aren't planned unless decided otherwise: a TUI, Helm charts or a virtual appliance, native multi-tenancy, a TypeScript SDK, a translated dashboard, campaign clustering and trend reports, phishing-simulation tooling, OpenTelemetry tracing, and Graph webhooks for M365.
Decision log¶
Forty-nine decisions: Q1-Q48 taken on 1-2 October 2026, and Q49 on 12 October 2026.
| Q | Topic | Decision |
|---|---|---|
| Q1 | Goal | Plugin ecosystem now, production adoption next |
| Q2 | Users | Admin > plugin developer > detection engineer > SOC analyst > researcher |
| Q3 | Target mail stack | Mostly M365, plus a real on-prem Postfix minority |
| Q4 | Tenancy | One organisation per installation |
| Q5 | Data leaving SPOT | Only through opt-in plugins, clearly labelled |
| Q6 | Change appetite | A few people, no production installation; breaking changes with semver and migration guides |
| Q7 | Thesis | Make organisation context real; plan MCP alongside RAG |
| Q8 | Sequencing | v2.0 Foundation first |
| Q9 | Plugin isolation | A scoped token per plugin, a plugin network, a plugin-manager behind a socket proxy |
| Q10 | Actions | New responder kind, on_verdict; several kinds per plugin, superseded on 12 October 2026 by Q49 |
| Q11 | MCP | Core brokers calls through the context service; SPOT as an MCP server later |
| Q12 | Distribution | Curated git index with signatures and digests |
| Q13 | Development loop | One testing engine in spot-sdk[testing], a template, spot plugin test, stored benchmarks |
| Q14 | Sandbox | Both: playground first, detonation plugins later |
| Q15 | Repos and forge | Consolidate; GitLab main, Codeberg mirror, GitHub later |
| Q16 | Deployment | Docker Compose only, made easy |
| Q17 | TUI | None; a stronger CLI instead |
| Q18 | Dashboard | Build on the redesign; HTMX form builder; invest in the test loop |
| Q19 | Egress labels | none / indicators / content, consent required for content |
| Q20 | Email content | Parsed once in core, raw copy in Postgres, bytes fetched on demand |
| Q21 | Verdict | Classes, score and signals; failures return unknown; a verdict policy per workflow |
| Q22 | LLM | OpenAI-compatible client with tool calling and JSON schema |
| Q23 | Context analyzers | analyzer-identity, LLM with context, sender-history, vendor-list |
| Q24 | Detection order | Fix existing, then URL, attachment, multilingual, detonation |
| Q25 | M365 | Delta polling first; category, hidden quarantine folder, release |
| Q26 | Postfix | A milter replaces retriever-smtp |
| Q27 | Workflows | Git-backed config history; routing: and shadow: lists |
| Q28 | Feedback | Minimal analyst feedback in v2.1, promotion into datasets |
| Q29 | Observability | Fixed logs, correlation IDs, audit log, Prometheus endpoints |
| Q30 | Single sign-on | Standard OIDC, in the adoption phase |
| Q31 | Contributions | Codeberg as the public place until the GitHub move |
| Q32 | Releases | v2.0 Foundation, v2.1 Ecosystem, v2.2 Context, v2.3 Adoption |
| Q33 | Compatibility | Contract = SDK major; previous contract kept for two minor releases |
| Q34 | TypeScript SDK | Dropped; OpenAPI generated from code |
| Q35 | Repo mapping | spot monorepo, sdk, spot-models, the index, datasets; plugins installable one at a time |
| Q36 | Tooling | uv workspaces |
| Q37 | Forgejo CI | Kept as is; GitLab is the main CI/CD |
| Q38 | Install profiles | minimal (default), standard, full; first verdict in under 15 minutes |
| Q39 | Model weights | Built into the images |
| Q40 | Rollout | Observe then enforce, plus connection wizards |
| Q41 | MCP config | mcp_servers as a plugin kind |
| Q42 | First installation | Own mail, in observe mode |
| Q43 | Deliverable | This document, then GitLab milestones and issues |
| Q44 | Plugin repos | One repo per plugin, a copier template, shared CI templates |
| Q45 | Facts | Sonn runs Postfix |
| Q46 | Plugin secrets | Encrypted in spot.yaml, passed as files |
| Q47 | hack.lu | Superseded on 2 October 2026: no deadline, work at its own pace |
| Q48 | 1.x security release | None; don't slow the refactor |
| Q49 | Plugin kinds | One kind per plugin; contract v2's kinds list removed in 2.0. The M365 connector is two plugins, a retriever and a responder. sender-history is a responder and a provider, or one responder that writes to the Knowledge Store itself (it would need knowledge:write), decided when it is designed |