{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "OpenLore MCP server — security capability declaration",
  "description": "Machine-readable declaration of the server's security-relevant capabilities (filesystem, subprocess, network, credentials, local daemon) plus an accepted-risk register of benign source->sink patterns a generic scanner may flag. Kept in sync with the code by security-capabilities.test.ts. See openspec/specs/mcp-security/spec.md.",
  "tool": "openlore",
  "specDomain": "mcp-security",
  "capabilities": {
    "filesystem": {
      "reads": [
        { "tree": "<project root>", "what": "source files for static analysis (tree-sitter parse, call graph, signatures)" },
        { "tree": ".openlore/", "what": "analysis artifacts (llm-context.json, mapping.json), SQLite edge store, config.json, decisions, memory, serve.json — treated as untrusted, size-bounded, shape-validated, fail-closed" },
        { "tree": "openspec/", "what": "spec.md domains, change proposals, decision ADRs" },
        { "tree": ".git/", "what": "read indirectly via the git subprocess (history, diffs, change coupling)" }
      ],
      "writes": [
        { "tree": ".openlore/", "what": "analysis cache, edge store, decisions store, memory notes, telemetry JSONL, serve.json discovery file" },
        { "tree": "openspec/", "what": "synced spec decisions (append-only to pre-existing specs), change proposals (sanitized slug), decision ADRs (kebab-cased filenames)" },
        { "tree": ".git/hooks/", "what": "post-commit / pre-commit hook install — opt-in via explicit --install-hook only" }
      ],
      "confinement": "Every tool path argument is confined to the validated project root via validateDirectory + safeJoin, which enforce canonical (symlink-resolved) containment. Mutators write only within .openlore/ and openspec/."
    },
    "subprocess": {
      "spawns": [
        { "bin": "git", "how": "execFile/execFileSync/spawnSync with an argv array (no shell); refs validated against argument injection (leading-dash + shell-meta)", "why": "history, diffs, change coupling, drift detection" },
        { "bin": "openlore (self)", "how": "spawn(process.execPath | resolved bin, ['decisions','--consolidate'|'analyze']) with fixed argv", "why": "background decision consolidation / re-analyze" },
        { "bin": "npm | npx", "how": "direct argv; on Windows, the absolute running node.exe launches npm-cli.js or npx-cli.js by absolute path", "why": "installation evidence, updates, and generated agent launch commands" },
        { "bin": "open | xdg-open | cmd start", "how": "spawn with a fixed browser-launch verb and URL argument", "why": "open the local viewer in the platform browser" },
        { "bin": "claude | gemini | vibe (LLM CLIs)", "how": "execFileSync(bin, argv) with fixed argv; bin overridable by env (CLAUDE/GEMINI_CLI/MISTRAL_VIBE_CLI)", "why": "optional CLI-based LLM provider transport (alternative to HTTP)" }
      ],
      "method": "argv arrays only; no shell:true. Windows npm/npx commands launch their CLI modules through the absolute running Node executable; no repository path or user-authored command is interpolated.",
      "exception": "The local-viewer launcher and Pi configuration wizard invoke cmd.exe /c only with fixed executables and validated argv on Windows; repository paths remain in spawn cwd options and are never interpolated into commands."
    },
    "network": {
      "egress": [
        { "to": "configured LLM provider endpoint", "source": "baseUrl from config / provider env vars", "data": "prompts derived from analysis + (for LLM-backed tools) repo-derived content" },
        { "to": "configured embedding provider endpoint", "source": "EMBED_BASE_URL / config.embedding", "data": "text to embed for semantic search" },
        { "to": "loopback (127.0.0.1)", "source": "local serve daemon transport", "data": "in-process tool dispatch only" }
      ],
      "allowedHosts": [
        "api.anthropic.com",
        "api.openai.com",
        "generativelanguage.googleapis.com",
        "127.0.0.1",
        "localhost",
        "::1"
      ],
      "prohibited": "No telemetry, analytics, update-check, or any other ad-hoc network sink. Repo content is sent only to the configured provider on LLM-backed tool calls."
    },
    "credentials": {
      "source": "environment variables / config (provider API keys, embedding key, optional serve token)",
      "use": "authenticate the configured provider request (and gate the local daemon) only",
      "confinement": "redacted from every output channel — tool results, errors, telemetry, logs — via secret-redaction (shared with sanitizeMcpError)."
    },
    "localDaemon": {
      "bind": "127.0.0.1 by default",
      "auth": "optional x-openlore-token compared in constant time; REQUIRED to bind any non-loopback host (refuses to start otherwise)",
      "defense": "DNS-rebinding guard (Host header must be a loopback name or the bound host) + cross-site Origin rejection, enforced before any dispatch"
    }
  },
  "acceptedRisks": [
    {
      "id": "read-source-invoke-git",
      "pattern": "fs.readFile(repo source) -> execFile(git, argv)",
      "why": "Reading the analyzed repository and invoking git over it is the tool's core purpose. git is spawned with an argv array (never a shell) and refs are validated, so no source or repo content can be reinterpreted as a command."
    },
    {
      "id": "read-config-call-llm",
      "pattern": "read config/env (baseUrl, apiKey) -> fetch(configured LLM/embedding endpoint)",
      "why": "LLM-backed tools must reach the operator's configured provider with the operator's own credentials. Egress is restricted to the configured endpoint (enumerated + asserted); no other destination receives repo-derived data."
    },
    {
      "id": "read-artifact-deserialize",
      "pattern": "fs.readFile(.openlore/*.json) -> JSON.parse",
      "why": "On-disk artifacts are treated as untrusted: reads are size-bounded (ARTIFACT_MAX_BYTES), the top-level shape is validated, and parsing fails closed to a 're-run analyze' result. A poisoned artifact cannot crash the server or be emitted as authoritative."
    },
    {
      "id": "serve-output-redirect-fds",
      "pattern": "git stdout/stderr -> openSync temp file fds (was: /bin/sh -c redirect)",
      "why": "Inside the MCP server, libuv pipe() fails EBADF because FD 0/1 are the JSON-RPC sockets. git output is redirected to temp-file descriptors instead of pipes — without a shell — so the workaround introduces no command-string interpolation."
    }
  ]
}
