{
  "checker": {
    "claims": [
      {
        "claim": "Only http and https initial URLs are accepted; an ftp URL is refused",
        "command": "web-retrieval-mcp tools/call {\"tool\": \"web_fetch\", \"arguments\": {\"url\": \"ftp://example.com/file\", \"render\": \"never\"}}",
        "evidence": "refused non-http(s) URL scheme: ftp\", \"type\": \"text\"}], \"isError\": false, \"structuredContent\": {\"result\": \"RETRIEVAL_FAILED: ftp://example.com/file \u2014 refused non-http(s) URL scheme: ftp\"}}",
        "expected": "refused non-http(s) URL scheme",
        "input": "{\"tool\": \"web_fetch\", \"arguments\": {\"url\": \"ftp://example.com/file\", \"render\": \"never\"}}",
        "kind": "claim",
        "outcome": "Real input produced the expected result: refused non-http(s) URL scheme: ftp",
        "source": "Only `http` and `https` initial URLs are accepted.",
        "source_checked": true,
        "source_file": "README.md",
        "verdict": "yes"
      },
      {
        "claim": "Initial hosts must resolve to globally routable addresses; localhost is refused",
        "command": "web-retrieval-mcp tools/call {\"tool\": \"web_fetch\", \"arguments\": {\"url\": \"http://localhost:8/\", \"render\": \"never\"}}",
        "evidence": "refused non-public address for 'localhost': ::1\", \"type\": \"text\"}], \"isError\": false, \"structuredContent\": {\"result\": \"RETRIEVAL_FAILED: http://localhost:8/ \u2014 refused non-public address for 'localhost': ::1\"}}",
        "expected": "refused non-public address",
        "input": "{\"tool\": \"web_fetch\", \"arguments\": {\"url\": \"http://localhost:8/\", \"render\": \"never\"}}",
        "kind": "claim",
        "outcome": "Real input produced the expected result: refused non-public address for 'localhost'",
        "source": "Initial hosts must resolve exclusively to globally routable addresses.",
        "source_checked": true,
        "source_file": "README.md",
        "verdict": "yes"
      },
      {
        "claim": "Tools resolve credentials lazily and report what a selected route is missing",
        "command": "web-retrieval-mcp tools/call {\"tool\": \"web_search\", \"arguments\": {\"query\": \"python package metadata\"}}",
        "evidence": "no usable credential for EXA_API_KEY; set EXA_API_KEY, add it to <guest-path>, or configure the optional keyring/OS secret store | firecrawl: no usable credential for FIRECRAWL_API_KEY; set FIRECRAWL_API_KEY, add it to <guest-path>, or configure the optional keyring/OS secret store\", \"type\": \"text\"}], \"isError\": false, \"structuredContent\": {\"result\": \"SEARCH_FAILED: python package metadata \u2014 exa: no usable credential for EXA_API_KEY; set EXA_API_KEY, add it to <guest-path>, or configure ",
        "expected": "search reports the missing EXA_API_KEY credential",
        "input": "{\"tool\": \"web_search\", \"arguments\": {\"query\": \"python package metadata\"}}",
        "kind": "claim",
        "outcome": "Real input produced the expected result: no usable credential for EXA_API_KEY",
        "source": "Tools resolve credentials lazily",
        "source_checked": true,
        "source_file": "README.md",
        "verdict": "yes"
      }
    ],
    "cycle_checks": [
      {
        "claim": "Scheduled or looping main behavior",
        "command": "",
        "evidence": "Entry points inspected (src/web_retrieval_mcp/server.py, __main__.py, install.py). The server runs as a request/response MCP stdio or Streamable HTTP service with no cron, scheduler, --once/--now flag, loop, or webhook; the only while True loop is a transport/serve loop. No product cycle to trigger.",
        "expected": "identify the product trigger",
        "input": "source inspection",
        "kind": "cycle",
        "outcome": "Entry points inspected (src/web_retrieval_mcp/server.py, __main__.py, install.py). The server runs as a request/response MCP stdio or Streamable HTTP service with no cron, scheduler, --once/--now flag, loop, or webhook; the only while True loop is a transport/serve loop. No product cycle to trigger.",
        "source": "repository entry points",
        "verdict": "yes"
      }
    ],
    "details": {
      "argv": [
        "web-retrieval-mcp"
      ],
      "kind": "mcp",
      "scope": "product operation",
      "serverInfo": {
        "name": "web-retrieval",
        "version": "0.2.0"
      },
      "tool_result": {
        "content": [
          {
            "text": "RETRIEVAL_FAILED: http://127.0.0.1:9/ \u2014 refused non-public address for '127.0.0.1': 127.0.0.1",
            "type": "text"
          }
        ],
        "isError": false,
        "structuredContent": {
          "result": "RETRIEVAL_FAILED: http://127.0.0.1:9/ \u2014 refused non-public address for '127.0.0.1': 127.0.0.1"
        }
      },
      "tool_result_checked": true,
      "tool_schema_bytes": 18936,
      "tools": [
        "web_search",
        "web_fetch",
        "research_papers",
        "research_paper",
        "research_similar",
        "research_github"
      ],
      "working": true
    },
    "evidence_strength": "readme_checked",
    "example_checks": [
      {
        "claim": "Offline SSRF guard tests pass against the installed source tree",
        "command": "<guest-path> test_ssrf_guard.py",
        "deferred": false,
        "evidence": "['<resolve-error>'] want=['<resolve-error>']\nOK    flush: forbidden host -> blocked url: got=['http://127.0.0.1/x'] want=['http://127.0.0.1/x']\nOK    flush: public host not blocked: got=False want=False\nOK    flush: 127.0.0.1 cached False: got=False want=False\nOK    flush: 8.8.8.8 cached True: got=True want=True\nOK    flush: drains pending: got=[] want=[]\n== network-layer residual disclosure ==\nOK    observer docstring still discloses the issued-before-teardown residual: got=True want=True\nOK    web_fetch docstring still discloses the Camoufox SSRF residual: got=True want=True\n\nALL  TESTS PASS",
        "expected": "ALL  TESTS PASS",
        "input": "no arguments",
        "kind": "example",
        "outcome": "command completed successfully",
        "returncode": 0,
        "source": "test_ssrf_guard.py",
        "verdict": "yes"
      },
      {
        "claim": "Release gate ./run-tests.sh runs all suites and package build",
        "command": "",
        "deferred": false,
        "evidence": "Not run: the gate requires pip install -e '.[all,dev]' (the all extra runs camoufox fetch, which downloads a browser that is not permitted here) and provisions a Valkey fixture via tests/fixtures/valkey/install.sh. Only the offline test_ssrf_guard.py was run instead.",
        "expected": "documented command completes successfully",
        "input": "",
        "kind": "example",
        "outcome": "Not run: the gate requires pip install -e '.[all,dev]' (the all extra runs camoufox fetch, which downloads a browser that is not permitted here) and provisions a Valkey fixture via tests/fixtures/valkey/install.sh. Only the offline test_ssrf_guard.py was run instead.",
        "source": "README.md Install from source / For development",
        "verdict": "blocked"
      },
      {
        "claim": "Live SSRF redirect check with a browser",
        "command": "",
        "deferred": true,
        "evidence": "Needs a local browser (render extra / Camoufox), which was not installed; the shared CDP endpoint is not wired to this product's render path.",
        "expected": "documented command completes successfully",
        "input": "",
        "kind": "example",
        "outcome": "Needs a local browser (render extra / Camoufox), which was not installed; the shared CDP endpoint is not wired to this product's render path.",
        "source": "README.md Install from source",
        "verdict": "deferred"
      }
    ],
    "status": "PASS"
  },
  "deviations": [
    "Did not run pip install '.[all]' or 'python -m camoufox fetch' because the all extra's camoufox fetch downloads a browser, which is not permitted; installed the non-render extras instead.",
    "Did not run ./run-tests.sh (release gate): needs '.[all,dev]' and a Valkey fixture install; ran only test_ssrf_guard.py.",
    "Workspace is not a git checkout, so the pinned commit c0ee5646d2c23ae27692d52f4c74387180c6cf24 could not be verified; the provided snapshot was used as is.",
    "Did not set any provider key or obtain credentials."
  ],
  "fault_owner": "prerequisite",
  "main_blocker": null,
  "operation": "Run the web-retrieval-mcp stdio MCP server and call its tools (web_fetch / web_search)",
  "operation_scope": "Python 3.12 venv with the base package plus tavily, cache and keyring extras; no provider API keys (EXA_API_KEY, FIRECRAWL_API_KEY, TAVILY_API_KEY) and no local Camoufox browser. Verified offline: server starts, lists six tools, web_fetch refuses non-http(s) schemes and loopback/localhost hosts (SSRF guard), web_search reports the missing EXA_API_KEY and FIRECRAWL_API_KEY credentials. Not verified: real web search results or fetching a public page, which need provider keys.",
  "outcome": "working",
  "plain_failure": null,
  "steps": [
    "python3 -m venv <guest-path>",
    "<guest-path> -m pip install '<guest-path>,cache,keyring]'",
    "web-retrieval-mcp (stdio MCP server, driven with an MCP client)",
    "web_fetch url=http://127.0.0.1:9/ -> RETRIEVAL_FAILED refused non-public address",
    "web_search query=python package metadata -> SEARCH_FAILED no usable credential for EXA_API_KEY",
    "PYTHONPATH=src python test_ssrf_guard.py -> ALL  TESTS PASS"
  ],
  "stuck_step": null,
  "suggested_fix": "Provide EXA_API_KEY and FIRECRAWL_API_KEY (optional TAVILY_API_KEY) through the environment or keys.env, then re-run web_search and web_fetch on a public URL. Unrelated to the product code: the server itself starts and its guards work."
}
