Skip to content

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, indicators or content. A content plugin 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: ingest for retrievers, knowledge:read and content:read for analyzers, knowledge:write on their own source for providers, results for 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-manager service starts and stops plugin containers through a Docker socket proxy limited to container and image calls. The gateway no longer mounts the socket.
  • Plugin kinds (Q10, Q41, Q49): analyzer, context_provider, mail_retriever, plus two new ones, responder and mcp_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.results and calls each responder over HTTP, so plugins never touch RabbitMQ.
  • 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 and ctx.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.
  • 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.
  • Egress labels (Q19, v2.0): spot.plugin.egress: none | indicators | content, shown at install and as a dashboard badge. content needs 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.
  • Secrets (Q46, v2.0):
    • Fields marked secret are stored in spot.yaml encrypted with ENCRYPTION_KEY and 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 backup includes the key.
  • 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 .eml once, 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-Results headers 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:read scope.
    • The retention purge deletes the stored copies too. Inbound X-SPOT-* headers are stripped when mail arrives.
  • 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, never benign.
    • 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).
  • 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.
  • Observe, then enforce (Q40, v2.0):
    • Each connection and route has mode: observe | enforce, and new connections always start in observe.
    • The dashboard shows what would have happened, and switching to enforce takes one click.
  • Connection wizards (Q40):
    • Postfix: generated main.cf milter 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.
  • 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.ReadWrite application 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.
  • 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.yaml atomically, 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.
  • 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 new creates a plugin from the copier template.
    • spot plugin test runs a plugin's container against tests/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).
  • 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-history and 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, minimal is 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 and ENCRYPTION_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 /metrics endpoint 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|yaml option, .eml submission, spot-cli watch for 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. shared becomes a proper package, there's one lockfile per repo, and the PYTHONPATH=/app/shared workaround 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.

  1. Repair the ingest path, build retriever-milter, and run it on Sonn's Postfix in observe mode with milter_default_action = accept.
  2. The security fixes (plugin tokens, plugin network, socket proxy, refresh-token check, header stripping) and the dashboard redesign merge.
  3. Contract v2: the parsed email view and the verdict model (classes, score, signals).
  4. Benchmarks re-run on held-out sets that share no data with any training set (nahmiasd_spear_phishing, miltchev, pashakhin), and those numbers published.
  5. 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