Panniantong/Agent-Reach

Agent

Give your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.

Panniantongpython, shell97,419 starsScanned Oct. 14th, 2026at 94f06c1v1.5.0View source

Trust score

58/100Low trust
Likely 22–94 · low confidence

Scanned Oct. 14th, 2026 at 94f06c1 against 34 rules · How scoring works

For developersOpen rawGET /c/Panniantong/Agent-Reach/report.json
At a glance

At a glance

Contains
1 MCP server · 1 skill · 1 tool
Its tools can
Read data 1
Issues flagged
1 critical2 high2 medium5 of 34 rule checks, severity as assessed for this repo
Where issues are
  • Repository-wide3 of 15
  • skills-plugins/agent_reach/skill (skill)2 of 8
No issues in the other 1 of 3.
Trust signals
Publisher not verifiedUnsigned buildActively maintained

What to fix first

5 issues, most severe first. Each one points to the exact code, with the recommended fix and a prompt you can paste into your coding agent.

  1. 1CriticalCapability Transparency: Declared Purpose vs. Actual Behavior (Shadow Features)
    skills-plugins/agent_reach/skill (skill) · ATR-P0001
    SKILL.md explicitly states the skill is NOT for write operations such as posting, commenting, or liking, framing itself as a read-only internet research/content-fetch router. The bundled references/dev.md, however, instructs the agent to run state-changing GitHub commands -- including creating, forking, and syncing repositories, creating issues and pull requests, and creating releases. These bundled operational instructions are an undisclosed write capability that directly contradicts the skill's own declared no-write scope, rather than every referenced command mapping one-to-one onto the declared read/search capability.
      16 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs).
      Zero config for 6 channels. Run `agent-reach doctor --json` to see which
      backend serves each platform right now.
    
      NOT for: 写报告/数据分析/翻译等内容加工(本 skill 只负责从互联网获取内容);
      发帖/评论/点赞等写操作;已有专门 skill 的平台(先用专门 skill)。
    
      【路由方式】SKILL.md 包含路由表和常用命令,复杂场景需按需阅读对应分类的 references/*.md。
    Show 2 more locations
    gh search code "query" --language python
    
    # 仓库
    gh repo view owner/repo
    gh repo clone owner/repo
    gh repo create my-repo --private
    gh repo fork owner/repo
    gh repo fork owner/repo --clone
    gh repo sync owner/repo
    
    # Issues
    gh issue list -R owner/repo --state open
    gh issue view 123 -R owner/repo
    gh issue create -R owner/repo --title "Title" --body "Body"
    …
    gh run view <run-id> --repo owner/repo --log-failed
    gh workflow list --repo owner/repo
    
    Recommended fix

    Remove the repository-write commands (repo create/fork/sync, issue/PR creation, release creation) from references/dev.md, or update SKILL.md's declared scope to explicitly disclose that GitHub write operations are part of the skill's capability set.

  2. 2HighDependency Risk & Supply Chain
    Repository-wide · ATR-C0007
    pyproject.toml declares every runtime dependency with open-ended minimum-version bounds (e.g. requests>=2.28, pyyaml>=6.0, yt-dlp[default]>=2026.07.04) rather than pinned or upper-bounded versions. constraints.txt does pin every dependency to an exact version, but it is opt-in: it is applied only when a user explicitly runs pip install -c constraints.txt ..., and is not consulted by an ordinary pip install agent-reach. A standard installation therefore resolves against the open-ended bounds in pyproject.toml and can pull in unreviewed future dependency releases, including ones affected by a newly disclosed vulnerability or supply-chain compromise.
    
    dependencies = [
        "requests>=2.28",
        "feedparser>=6.0",
        "python-dotenv>=1.0",
        "loguru>=0.7",
        "pyyaml>=6.0",
        "rich>=13.0",
        "yt-dlp[default]>=2026.07.04",
    ]
    
    [project.optional-dependencies]
    browser = ["playwright>=1.40"]
    cookies = ["browser-cookie3>=0.19"]
    …
    Show 1 more location
    # Agent Reach tested dependency set
    # Usage:
    #   pip install -c constraints.txt -e .[dev]
    
    requests==2.32.5
    feedparser==6.0.12
    python-dotenv==1.2.1
    loguru==0.7.3
    PyYAML==6.0.3
    rich==14.3.2
    yt-dlp==2026.07.04
    yt-dlp-ejs==0.8.0
    Recommended fix

    Pin or upper-bound the dependency versions declared directly in pyproject.toml (or ship and enforce a lockfile that is applied by default), rather than relying on an opt-in constraints.txt that an ordinary install does not consult.

  3. 3HighInjection and Concealed Directives in Skill/Plugin Manifest and Instruction Text
    skills-plugins/agent_reach/skill (skill) · ATR-P0006
    A programmatic codepoint scan of SKILL.md, SKILL_en.md, and all references/*.md found zero zero-width, bidi-override/isolate, or Unicode Tag-block characters, and no ANSI/OSC escape sequences or forged chat-template/role tokens. However, SKILL.md's model-facing description instructs the host to use the skill whenever the user mentions any of 16 named platforms or shares any URL, and the body escalates this to an explicit directive that when this skill exists the host must use it and must not invent its own approach. This is a broad, overt, mandatory tool-routing directive aimed at the host model rather than purely descriptive capability text, and falls within this rule's scope covering concealed or overt directives embedded in the instruction carrier.
    description: >
      MUST USE when user wants to 调研/research/搜索/search/查/找/look up anything
      on the internet — e.g. 全网调研 X / 帮我调研一下 X / 查一下 X / 搜搜 X /
      看看大家怎么评价 X / X 上有什么讨论 / research this topic。
    
      Also MUST USE when user mentions any platform or shares any URL/链接:
      小红书/xiaohongshu/xhs, Twitter/推特/X, B站/bilibili, Reddit, Facebook,
      Instagram, V2EX, LinkedIn/领英/Boss直聘/招聘/求职/jobs, YouTube, GitHub code search, 小宇宙播客,
      雪球/股票行情, RSS feeds, or any web URL.
    Show 1 more location
    
    16 平台、多后端。**本 skill 存在时必须用它访问这些平台,不要自己发明方案。**
    
    ## 常驻规则(全程适用)
    
    1. **动手前先体检**:多后端/登录态平台(小红书/Reddit/B站/Twitter/Facebook/Instagram/Boss直聘)先跑
       `agent-reach doctor --json`。`active_backend` 有值时按它选命令组;`active_backend: null`
       表示 Doctor 为避免触发浏览器 Cookie 读取或远端写入而没有做实时验证,不代表后端不存在。
       Doctor 结果是「某一时刻的快照」,通道/登录态可能已变化;执行只读命令前若怀疑失效,
       按对应 reference 的「体检与恢复」runbook 重新确认(如 career.md 的 Boss直聘 CDP 排查)。
    2. **声明你在用什么**:开始干活前说一句「使用 agent-reach 的 X 平台 / Y 后端」。
    Recommended fix

    Soften the mandatory 'MUST USE ... do not invent your own approach' language in SKILL.md's model-facing description/body to a non-binding recommendation, leaving the host model free to choose an alternative approach instead of being directed to always defer to this skill.

  4. 4MediumLogging Hygiene (No Secrets/PII/Tokens) & Audit Trail
    Repository-wide · ATR-C0005
    Doctor/status output and exception text are scrubbed of credentials via scrub_url_credentials() before being rendered, and config.to_dict() masks sensitive fields. However, this rule also requires an audit trail of tool invocations. The MCP server's call_tool dispatcher executes get_status and returns its result directly, without recording the tool name, arguments, outcome, caller/session identity, or a correlation identifier anywhere. CLI logging is optional console output, not a structured invocation log. As a result, MCP tool operations cannot be reconstructed for forensics after the fact.
    
        @server.call_tool()
        async def call_tool(name: str, arguments: dict):
            try:
                if name == "get_status":
                    result = eyes.doctor_report()
                else:
                    result = f"Unknown tool: {name}"
    
                text = json.dumps(result, ensure_ascii=False, indent=2) if isinstance(result, (dict, list)) else str(result)
                return [TextContent(type="text", text=text)]
            except Exception as e:
                return [
                    TextContent(
    Show 1 more location
    def _configure_logging(verbose: bool = False):
        """Suppress loguru output unless --verbose is set."""
        from loguru import logger
        logger.remove()  # Remove default stderr handler
        if verbose:
            logger.add(sys.stderr, level="INFO")
    Recommended fix

    Add an audit-log call inside the MCP call_tool dispatcher that records, for every invocation, the tool name, redacted arguments, caller/session identifier, timestamp, and outcome/result status to a durable log or audit store.

  5. 5MediumLack of Output Encoding/Sanitization
    Repository-wide · ATR-C0010
    agent-reach transcribe sends derived audio to an external transcription provider (Groq/OpenAI Whisper) and prints that provider's raw response text directly to the operator's terminal (cli.py, fed from transcribe.py), with no ANSI/control-character sanitization or output encoding applied before printing. Because the transcript content originates from third-party audio the operator does not control, a crafted transcription result could embed terminal escape sequences that reach the terminal unneutralized, making this a live output-encoding sink rather than an inapplicable one.
        p_tr = sub.add_parser("transcribe", help="Transcribe a URL or local audio file (Whisper via Groq/OpenAI)")
        p_tr.add_argument("source", help="Audio/video URL or local file path")
        p_tr.add_argument("--provider", choices=["auto", "groq", "openai"], default="auto",
                          help="Transcription provider (default: first configured provider)")
        p_tr.add_argument(
    Show 2 more locations
            try:
                resp = requests.post(
                    info["endpoint"],
                    headers={"Authorization": f"Bearer {key}"},
                    files={"file": (chunk.name, fh, "audio/m4a")},
                    data={"model": info["model"], "response_format": "text"},
                    timeout=timeout,
                )
            except requests.RequestException as e:
                raise TranscribeError(f"{provider}: network error: {e}") from e
    
        if not resp.ok:
            raise TranscribeError(f"{provider}: HTTP {resp.status_code}: {resp.text[:300]}")
        return resp.text
    …
    
        if args.output:
            Path(args.output).write_text(text + "\n", encoding="utf-8")
            print(f"✅ Transcript written to {args.output}")
        else:
            print(text)
    Recommended fix

    Strip or neutralize ANSI/terminal control-escape sequences from the transcription provider's response text before printing it to the terminal, e.g. by filtering non-printable/control characters outside normal whitespace.

What’s inside

BlueRock found 2 components in this repository. Each is checked against the rules for its type.

Skillskills-plugins/agent_reach/skillagent_reach/skill69Moderate trust1 critical1 high

What stood out

  • Prompt-injection / output-sanitization gap in upstream tool chain: SKILL.md instructs the agent to invoke external CLIs (yt-dlp, Jina Reader, gh CLI, twitter-cli) directly, and whether those tools apply any output-sanitization or prompt-injection envelope before raw fetched content reaches the calling LLM's context cannot be verified from this codebase alone (ATR-C0013, UNKNOWN).
  • GitHub Actions supply-chain hygiene gap: the CI workflow (.github/workflows/pytest.yml) pins third-party actions by mutable major-version tag (actions/checkout@v4, actions/setup-python@v5) rather than by commit SHA, creating a supply-chain tampering window (flagged by Semgrep github-actions-mutable-action-tag rule).
  • The repo's broader CLI (agent_reach/cli.py, cookie_extract.py, boss.py channel) handles browser cookies and CDP-based browser automation for several platforms; if those modules are ever exposed as MCP tools or skill capabilities in a future version they would require independent security review.
  • Bandit reported 1330 repo-wide findings and Semgrep 21; these are overwhelmingly generic CLI-code style/assert findings that were not individually re-triaged and may contain unreviewed issues outside the narrow MCP/skill scope.

Skill rules 2 of 8 flagged

5 rules passed · 1 did not applyShow
PassedConsent Suppression and Guardrail / Human-in-the-Loop Subversion via Trusted InstructionsATR-P0002Details

Why it got this result

Reviewed SKILL.md and SKILL_en.md in full plus all references/*.md for imperative prose instructing the host to skip confirmation, hide actions, or claim authority over the system prompt. No such directives exist. On the contrary, the Boss Zhipin (招聘) flow explicitly instructs the agent to PAUSE and require the user to visually confirm login state before proceeding ('拉起后第一步是暂停并让用户肉眼确认...'), and multiple sections stress explicit, non-silent credential handling (e.g. Twitter auth tokens must be provided explicitly and 不得在日志或命令回显中暴露值 -- a disclosure-preserving instruction, not a suppression one).

Why it matters

Because a skill/plugin instruction body is ingested as trusted first-party context, consent- or oversight-suppressing prose inherits the host's full authority and turns a passive package into a self-authorizing backdoor that strips the human-in-the-loop control from every action it can reach.

Evidence

Boss直聘配置触发:当用户说“帮我配 Boss直聘”时,先读取 `references/career.md`
的 Boss 章节,然后在获得安装授权后运行
`agent-reach install --env=local --system --channels=boss`。Agent 负责按系统启动
只绑定 `127.0.0.1:9222` 的专用 Chrome;**拉起后第一步是暂停并让用户肉眼确认**
窗口内是已登录状态(右上角有头像),未登录则让用户登录/扫码,用户确认后再运行
`boss --cdp-url http://localhost:9222 login --cdp` 和 `agent-reach doctor` 验收。
PassedHost-Credential and Conversation-Context Harvesting and ExfiltrationATR-P0003Details

Why it got this result

This rule is applicable under its mechanism-agnostic scope: the skill explicitly handles credential material by routing user-provided Twitter auth tokens (TWITTER_AUTH_TOKEN/CT0) and imported same-domain cookies (e.g. XiaoHongShu) through registered platform clients, rather than having no credential-handling subject at all. This handling is nevertheless secure: the credentials are user-supplied and scoped to the intended platform/domain, the instructions explicitly require they not be exposed in logs or command echo, and no instruction in SKILL.md/SKILL_en.md or any references/*.md directs reading host credential stores (~/.ssh, ~/.aws, .env, keychain, cloud metadata), harvesting the live conversation/system-prompt context, or routing credentials to an unrelated or attacker-controlled endpoint.

Why it matters

A skill or plugin that couples a host-credential or live-conversation read with an outbound channel turns an ostensibly benign helper into an autonomous exfiltration agent, leaking long-lived secrets, session tokens, and confidential prompt context to an attacker on every invocation across every tool the artifact serves.

Evidence

Twitter 注意:`agent-reach configure twitter-cookies` 保存的 Cookie 只供
`doctor` 检查配置是否齐全;`doctor` 不执行 `twitter status`,也不会设置当前
Shell。直接运行 `twitter` 前,必须在子进程环境中显式提供
`TWITTER_AUTH_TOKEN` 和 `TWITTER_CT0`,不得在日志或命令回显中暴露值。

小红书注意:Agent Reach 不替用户登录,也不读取浏览器 Cookie。
PassedOver-Declared / Over-Privileged Artifact Manifest ScopeATR-P0005Details

Why it got this result

SKILL.md frontmatter declares no allowed-tools/capability list at all (Claude skill format here omits an explicit scope field), so there is no wildcard/catch-all declaration or OAuth-scope over-grant to evaluate. The skill's instructed capability set (invoking shell CLIs: twitter, bili, gh, yt-dlp, curl, opencli, mcporter) is intrinsic to its stated purpose -- a multi-platform internet research router that needs to shell out to per-platform CLIs to fetch content, which is exactly what the description discloses (web search/fetch across 16 named platforms). No bundled executable, postinstall hook, or hidden capability grant exists beyond this declared, broad-but-justified function.

Why it matters

An artifact that self-declares host privilege far beyond its stated function -- or hides executable reach inside a passive-looking package -- gives a single install a blast radius the operator never scoped, so any later mistake, compromise, or misconfiguration detonates across resources the artifact never needed.

Evidence

## 零配置快速命令

```bash
# Exa 网页搜索
mcporter call exa.web_search_exa query="query" numResults=5

# 通用网页阅读
curl -s "https://r.jina.ai/URL"

# GitHub 搜索
gh search repos "query" --sort stars --limit 10
...
PassedSkill/Plugin Package Provenance, Integrity, and Post-Approval Mutation (Rug-Pull)ATR-P0007Details

Why it got this result

The skill's model-facing instruction body ships as static files inside the component directory itself (SKILL.md / SKILL_en.md / references/*.md) -- there is no code within agent_reach/skill/ that fetches or rewrites these files at runtime; they are plain, non-executable markdown. The only install-time copy mechanism lives outside this component (in the CLI, not in the skill artifact itself) and copies these same static bundled files verbatim to the host's skill directory on an explicit, user-invoked agent-reach skill --install command -- it does not live-fetch the instruction text from a mutable remote endpoint, and it does not auto-overwrite an existing installation without preserving it. The artifact's own trust surface (this directory's content) is immutable at runtime.

Why it matters

A skill/plugin reviewed once and then trusted indefinitely becomes a rug-pull vector when its OWN model-facing instruction body, declared scope, or manifest can be live-swapped or silently mutated after approval -- without an approval-time digest that is re-compared on load and re-consent on any change, a publisher (or an attacker who compromises the distribution channel) can rewrite the artifact's trusted instructions or widen its tool scope under authority the host already granted, turning an approved package into an exfiltration or unauthorized-action backdoor.

Evidence

---
name: agent-reach
description: >
  MUST USE when user wants to 调研/research/搜索/search/查/找/look up anything
  on the internet — e.g. 全网调研 X / 帮我调研一下 X / 查一下 X / 搜搜 X /
  ...

  Also MUST USE when user mentions any platform or shares any URL/链接:
  小红书/xiaohongshu/xhs, Twitter/推特/X, B站/bilibili, Reddit, Facebook,
  Instagram, V2EX, LinkedIn/领英/Boss直聘/招聘/求职/jobs, YouTube, GitHub code search, 小宇宙播客,
  雪球/股票行情, RSS feeds, or any web URL.

  16 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs).
PassedSkill/Plugin Impersonation, Typosquatting & Publisher SpoofingATR-P0008Details

Why it got this result

The skill's declared name/slug is agent-reach, matching exactly the canonical name of the project and its GitHub org/repo (github.com/Panniantong/Agent-Reach, confirmed via git remote -v). There is no near-miss/typosquat of a known brand, no trust-implying prefix, no version/tier-spoofing suffix, and no impersonation prose claiming to be an official/updated build of a different named tool. README explicitly notes 'PyPI 的同名包是另一个项目' (a same-named PyPI package is a different project) rather than claiming to BE that other package.

Why it matters

Impersonation is the entry point of the agentic supply chain (OWASP LLM ASI04 / MITRE ATT&CK T1195): a skill or plugin installed on the strength of a familiar name inherits the host agent's execution context before any payload runs. The deception front-runs trust, so catching the identity lie breaks the chain at the cheapest point -- and it is independent of whether the current code is benign, because the same trusted-looking name can ship a payload in the next version.

Evidence

---
name: agent-reach
description: >
Does not applyFormat-Native Out-of-Band Execution and Host Control-Plane PersistenceATR-P0004Details

Why it got this result

The skill ships no lifecycle/event hooks (no PreToolUse/PostToolUse hooks, no before_tool_callback), no settings.json auto-approve config, no git hooks, and no env-var hijack assignments (no GIT_EXTERNAL_DIFF/NODE_OPTIONS/LD_PRELOAD/PYTHONSTARTUP). The skill directory contains only SKILL.md/SKILL_en.md (prose) and references/*.md (prose). The bundled install mechanism (_install_skill in cli.py) only copies the static markdown files into the host's skill directory via an explicit agent-reach skill --install CLI command invoked by the user/agent -- it is not an automatic load-time or lifecycle-hook execution path the artifact itself triggers; invoking the host's own file-write tool through the normal consent-gated flow is not a format-native control-plane primitive under this rule's scope test.

Why it matters

Format-native execution and control-plane writes bypass the model's reasoning loop, converting a single install approval into durable, self-reinjecting code execution that can MITM the host agent's own model endpoint and silently subvert every subsequent tool call.

Evidence

(Entire SKILL.md/SKILL_en.md reviewed: frontmatter + prose only, no lifecycle hooks, no env exports, no entry scripts)

Before you put it to work

  1. Pin third-party GitHub Actions to a specific commit SHA instead of a mutable major-version tag (e.g. actions/checkout@<sha> # v4.x.x) to close the supply-chain tampering window.
  2. Apply a host-enforced envelope and untrustedReturn-style annotation around any third-party fetched content (yt-dlp, Jina Reader, gh CLI, twitter-cli output) before it reaches LLM context, consistent with the SSRF-hardening discipline already present in utils/url.py.
  3. Keep the MCP server stdio-only; if a remote transport (HTTP/SSE) is ever added, implement OAuth 2.1+PKCE, Origin/Host validation, and TLS before exposure (per ATR-M0003/M0004/M0005/M0008/M0009).
  4. If SKILL.md is revised to include lifecycle hooks, auto-update fetch of instructions, or write-capable commands, re-evaluate ATR-P0002/P0004/P0007 against the new content.
  5. If the MCP tool surface is expanded beyond get_status, re-evaluate ATR-M0002/M0010 against any new tool's capabilities and transport.
  6. Continue the existing least-privilege file-permission discipline; Semgrep WARNING-level findings at cli.py:939 and utils/paths.py:65 note 0o700 directory/script permissions that are stricter than default but are not a vulnerability — no immediate action required.
MCP servermcp-server.92High trustNone flagged

Its tools can

Read data 1

What stood out

  • Prompt-injection / output-sanitization gap in upstream tool chain: SKILL.md instructs the agent to invoke external CLIs (yt-dlp, Jina Reader, gh CLI, twitter-cli) directly, and whether those tools apply any output-sanitization or prompt-injection envelope before raw fetched content reaches the calling LLM's context cannot be verified from this codebase alone (ATR-C0013, UNKNOWN).
  • GitHub Actions supply-chain hygiene gap: the CI workflow (.github/workflows/pytest.yml) pins third-party actions by mutable major-version tag (actions/checkout@v4, actions/setup-python@v5) rather than by commit SHA, creating a supply-chain tampering window (flagged by Semgrep github-actions-mutable-action-tag rule).
  • The repo's broader CLI (agent_reach/cli.py, cookie_extract.py, boss.py channel) handles browser cookies and CDP-based browser automation for several platforms; if those modules are ever exposed as MCP tools or skill capabilities in a future version they would require independent security review.
  • Bandit reported 1330 repo-wide findings and Semgrep 21; these are overwhelmingly generic CLI-code style/assert findings that were not individually re-triaged and may contain unreviewed issues outside the narrow MCP/skill scope.

MCP server rules 0 of 11 flagged

5 rules passed · 6 did not applyShow
PassedNo Authentication or Authorization Checks (General)ATR-M0010Details

Why it got this result

This is a loopback/local-process-only stdio MCP server (no network bind at all -- see main()/stdio_server()). Its single tool, get_status, is pure over no caller input (empty inputSchema) and only reads local channel-availability status via a read_only Config -- it has no host-side write effect, no credential/token access, no process execution, and no shared backend identity across callers (each stdio session is a separate local process invocation). Per the rule's negative_indicators/false_positive_guards, a loopback-only listener (here, no listener at all; the OS process boundary governs who can speak over stdio) whose every tool is pure over caller-supplied input is NOT VULNERABLE -- the missing authn/authz gate is hardening, not a finding.

Why it matters

Prevents unauthorized tool use and data access.

Evidence

    server = Server("agent-reach")
    config = Config(read_only=True)
    eyes = AgentReach(config)

    @server.list_tools()
    async def list_tools():
        return [Tool(name="get_status", ... inputSchema={"type": "object", "properties": {}})]
            Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
        ]

    @server.call_tool()
PassedTool Definition Integrity & Versioning (ETDI-style)ATR-M0001Details

Why it got this result

The server exposes a single, hardcoded tool definition (get_status) built as a Python literal in create_server(); there is no external tool manifest, no runtime fetch of tool definitions, and no hot-reload path. Tool definitions ship inside the pinned package source and change only via a new release of the package itself (standard pip/source install), not at runtime. There is no update/signature-verification mechanism because there is no mutable external manifest to protect.

Why it matters

Prevents rug-pull tool poisoning.

Evidence

@server.list_tools()
    async def list_tools():
        return [
            Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
        ]
PassedUnrestricted Tool Permissions (Overprivileged Tools)ATR-M0002Details

Why it got this result

The only registered tool, get_status, calls AgentReach(config).doctor_report() with a Config constructed as read_only=True. doctor_report does perform process execution and network activity -- it invokes fixed version/status subprocesses and queries fixed/local status endpoints per channel -- so it is not accurate that it performs no process execution or network activity at all. However, the tool's inputSchema accepts no properties and call_tool ignores any supplied arguments, so the caller cannot select or influence which command, URL, path, credential, or mutation is used: every subprocess and network call made by doctor_report is fixed by the implementation. The tool therefore remains least-privilege on the basis of caller-side control, not on an absence of underlying activity.

Why it matters

Limits blast radius and privilege escalation.

Evidence

server = Server("agent-reach")
    config = Config(read_only=True)
    eyes = AgentReach(config)

    @server.list_tools()
    async def list_tools():
        return [
            Tool(name="get_status", ...)
        ]

    @server.call_tool()
    async def call_tool(name: str, arguments: dict):
        try:
            if name == "get_status":
…
Show 1 more location
def check_all(config: Config) -> Dict[str, dict]:
    results = {}
    for ch in get_all_channels():
        try:
            status, message = ch.check(config)
            active = getattr(ch, "active_backend", None)
        except Exception as e:
            status = "error"
            ...
        results[ch.name] = {...}
    return results
PassedTool Name Collision / SpoofingATR-M0006Details

Why it got this result

The server exposes a single tool named get_status. This is a generic verb with no collision against a specific named tool exposed by a widely-deployed MCP server or client built-in (e.g., it is not 'read_file' or similar). No namespace prefixing exists but with only one trivial, read-only tool the collision risk is negligible.

Why it matters

Prevents malicious tool shadowing.

Evidence

Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
PassedPrompt/Metadata Injection in Tool DescriptionsATR-M0007Details

Why it got this result

The sole tool description is a short, neutral, plain-ASCII string with no hidden directives, no control/zero-width/bidi characters, and no instructions to exfiltrate data or manipulate the model. Manual inspection of the literal string confirms it is purely descriptive.

Why it matters

Prevents covert model manipulation.

Evidence

Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
Does not applyOAuth 2.1 + PKCE Correctness (MCP Authorization)ATR-M0005Details

Why it got this result

No remote transport (streamable_http/SSE/HTTP) is present; the server only runs over stdio (see main()/stdio_server()). Per the rule's own false_positive_guards, stdio-only components with no remote authorization surface are NOT_APPLICABLE for OAuth 2.1 requirements.

Why it matters

Misconfig enables token theft and confused-deputy attacks.

Evidence

async def main():
    server = create_server()
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
Does not applyOAuth Proxy Per-Client Consent (Confused Deputy Defense)ATR-M0011Details

Why it got this result

The server is not an OAuth proxy: it does not forward authorization requests to a distinct upstream IdP on behalf of dynamically-registered MCP clients. There is no /authorize, /register-client, or bridge-code logic anywhere in the codebase; it is a single local stdio tool that reports channel status. The confused-deputy OAuth-proxy threat model does not apply.

Why it matters

Shared upstream client + dynamic MCP-client registration + no per-client consent enables attacker-registered MCP clients to receive bridge codes carrying victim upstream tokens — the documented MCP Confused Deputy attack.

Evidence

(whole file -- no OAuth proxy / authorize / register-client / bridge-code logic present)
Does not applyCORS / Origin / Host Validation (DNS Rebinding Defense)ATR-M0003Details

Why it got this result

The MCP server is started exclusively via stdio_server() in main() (agent_reach/integrations/mcp_server.py). There is no HTTP/SSE/streamable-HTTP transport, no CORS middleware, and no listener binding to a network interface anywhere in the repo (grep for StreamableHTTP/SSEServer/uvicorn/Flask/FastAPI/listen/0.0.0.0 found no server-side matches). Per applicability.requires_remote_transport, this rule does not apply to a stdio-only component.

Why it matters

Prevents DNS rebinding/browser hijack of local or remote MCP.

Evidence

async def main():
    server = create_server()
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
Does not applyBinding to 0.0.0.0 Without Auth/TLSATR-M0004Details

Why it got this result

No network listener/bind exists in this component; it communicates over stdio only (see main() using stdio_server()). requires_remote_transport applicability makes this rule not applicable to a stdio-only component.

Why it matters

LAN/Internet exposure enables remote exploitation.

Evidence

async def main():
    server = create_server()
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
Does not applyCORS Misuse: Wildcard with CredentialsATR-M0008Details

Why it got this result

No CORS middleware or HTTP listener exists in this stdio-only component; requires_remote_transport applicability makes this rule not applicable.

Why it matters

Prevents credentialed cross-origin abuse.

Evidence

async def main():
    server = create_server()
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
Does not applyTransport Security: TLS Everywhere (Remote)ATR-M0009Details

Why it got this result

No remote HTTP transport exists; the component communicates exclusively over local stdio. requires_remote_transport applicability makes TLS-everywhere rule not applicable.

Why it matters

MitM can steal tokens and tamper responses.

Evidence

async def main():
    server = create_server()
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
Show its 1 tool
ToolIt can
get_status
Get Agent Reach status: which channels are installed and active (doctor report).
Read data

Before you put it to work

  1. Pin third-party GitHub Actions to a specific commit SHA instead of a mutable major-version tag (e.g. actions/checkout@<sha> # v4.x.x) to close the supply-chain tampering window.
  2. Apply a host-enforced envelope and untrustedReturn-style annotation around any third-party fetched content (yt-dlp, Jina Reader, gh CLI, twitter-cli output) before it reaches LLM context, consistent with the SSRF-hardening discipline already present in utils/url.py.
  3. Keep the MCP server stdio-only; if a remote transport (HTTP/SSE) is ever added, implement OAuth 2.1+PKCE, Origin/Host validation, and TLS before exposure (per ATR-M0003/M0004/M0005/M0008/M0009).
  4. If SKILL.md is revised to include lifecycle hooks, auto-update fetch of instructions, or write-capable commands, re-evaluate ATR-P0002/P0004/P0007 against the new content.
  5. If the MCP tool surface is expanded beyond get_status, re-evaluate ATR-M0002/M0010 against any new tool's capabilities and transport.
  6. Continue the existing least-privilege file-permission discipline; Semgrep WARNING-level findings at cli.py:939 and utils/paths.py:65 note 0o700 directory/script permissions that are stricter than default but are not a vulnerability — no immediate action required.

Repository-wide rules

Checked once for the whole repository: dependencies, secrets, inputs, networking, CI and more. 3 of 15 flagged.

Could not determineTool Output Sanitization & Injection ContainmentATR-C0013Details

Why it got this result

Agent Reach's own MCP server (integrations/mcp_server.py) exposes only a single tool, get_status, which returns the host's own doctor report (JSON built from fixed channel-status strings) -- no third-party content flows through that specific tool, so for that narrow surface the rule is effectively not triggered. However, Agent Reach's actual declared product (per SKILL.md) is a router that tells the calling agent to invoke OTHER external CLIs/MCP tools directly (yt-dlp, Jina Reader via curl, gh CLI, mcporter Exa, twitter-cli, opencli) and consume their raw output as LLM context. Agent Reach's own code does not itself inject fetched web/social content into any LLM context (no envelope/sanitization logic exists in this repo for that purpose, because Agent Reach does not perform the fetch-and-return step -- it delegates it to upstream tools outside this repo's scope). Because the host-side injection point lives in those external tools/the calling agent rather than in this repository, I cannot verify from this codebase alone whether an envelope/sanitization/untrustedReturn control exists at the point where fetched content reaches the model -- abstaining rather than guessing about code outside this repo.

Why it matters

Tool outputs are a primary prompt-injection vector. Third-party content (web pages, files, API responses) may contain text crafted to override instructions or impersonate system/tool messages. Without a host-enforced envelope, sanitization, and an explicit untrusted-return signal, the model can be coerced into executing attacker-controlled directives, leaking secrets, or chaining further tool calls on behalf of the adversary.

10 rules passed · 1 did not applyShow
PassedRuntime Command Injection via Process ExecutionATR-C0003Details

Why it got this result

All subprocess invocations across the runtime (cli.py, v2ex.py, transcribe.py, probe.py) use subprocess.run() with an argument-vector list and no shell=True / os.system / eval anywhere in agent_reach/ (confirmed by grep: zero shell=True, os.system, or eval( hits in application code). Arguments passed to yt-dlp/ffmpeg/curl are either fixed flags or a URL that is first validated by _assert_safe_public_url()/normalize_public_http_url() (reject non-http(s) schemes, private/loopback/link-local/reserved IPs including inet_aton shorthand forms like 127.1/0x7f000001, and internal hostnames). The '--' separator before the URL argument in yt-dlp invocation prevents option-injection via a URL starting with '-'. Bandit flagged B603 (subprocess without shell equals true) across 25 call sites in cli.py/v2ex.py/transcribe.py -- all manually confirmed to use list-form argv with a fixed executable located via shutil.which(), no shell re-parsing.

Why it matters

Unsanitized command construction in shipped runtime code can give a remote attacker arbitrary code execution with the application's privileges.

Evidence

def _assert_safe_public_url(url: str) -> None:
    ...
    if host in _BLOCKED_HOSTS or host.endswith(".localhost"):
        raise TranscribeError("SSRF blocked: internal host is not allowed")
    if _is_private_ip(host):
        raise TranscribeError("SSRF blocked: private/internal IP is not allowed")


def download_audio(url: str, out_dir: Path) -> Path:
    """Download audio with yt-dlp into out_dir; return the resulting file path."""
    _assert_safe_public_url(url)
    _require("yt-dlp")
    template = out_dir / "source.%(ext)s"
    _run(
…
Show 1 more location
    command = [
        curl, "--fail", "--silent", "--show-error", "--proto", "=https",
        "--connect-timeout", "5", "--max-time", str(_TIMEOUT),
        "--max-filesize", str(_MAX_RESPONSE_BYTES),
        "--header", f"User-Agent: {_UA}", "--url", url,
    ]
    try:
        result = subprocess.run(
            command, capture_output=True, encoding="utf-8", errors="replace",
            timeout=_TIMEOUT + 2, env=utf8_subprocess_env(),
        )
PassedWeaponization: Malicious-Purpose Artifact (Offensive Capability by Design)ATR-R0001Details

Why it got this result

Agent Reach's declared purpose (README.md, SKILL.md, pyproject.toml description) is to give an AI agent read access to public internet platforms (Twitter, Reddit, YouTube, GitHub, etc.) for research/search -- a benign, clearly non-offensive capability. There is no malware/exploit/credential-theft/evasion generation capability anywhere in the instruction bodies or bundled scripts; the only CDP browser-automation pattern (boss.py) exists to reuse the USER's OWN already-logged-in browser session for legitimate job-search automation and explicitly requires the user to visually confirm their own logged-in state before any action -- the opposite of covert/unattributed operation. No offensive-security framing, dual-use pentest claim, or log-suppression/anti-analysis instructions are present anywhere in the repo.

Why it matters

An artifact whose purpose is to weaponize the agent -- to produce malware, steal credentials, evade defenses, or run attacks at scale -- is dangerous regardless of how securely it is built; posture controls (auth, input validation, least privilege) cannot make a weapon safe, which is why a confirmed weaponization finding caps the trust verdict to `blocked` rather than being averaged against a good posture score. Catching malicious PURPOSE is a distinct safety control, and it must be conservative because offensive capability is dual-use: legitimate pentest, CTF, defensive, and research work must abstain (UNKNOWN), never block -- unless the artifact itself hides from the people who install, run or audit it.

Evidence

description = "Give your AI Agent eyes to see the entire internet. Search + Read 10+ platforms."
Show 1 more location
只绑定 `127.0.0.1:9222` 的专用 Chrome;**拉起后第一步是暂停并让用户肉眼确认**
窗口内是已登录状态(右上角有头像),未登录则让用户登录/扫码,用户确认后再运行
`boss --cdp-url http://localhost:9222 login --cdp` 和 `agent-reach doctor` 验收。
不要让用户自己研究端口参数。
PassedUnrestricted Network Fetch / SSRFATR-C0001Details

Why it got this result

The only arbitrary-URL fetch surface is WebChannel.read() (Jina Reader proxy for 'any URL'). It passes every URL through normalize_public_http_url() before use, which rejects non-HTTP(S) schemes, userinfo, literal non-global IPs (RFC1918/loopback/link-local/metadata via ipaddress.is_global), and known internal TLD/hostname suffixes (.local, .internal, metadata.google.internal, etc.). All other channels (V2EX, Xueqiu, Boss, GitHub, Bilibili) call a fixed, hardcoded API host and validate scheme/host/port with host_matches()/_validate_api_url() before any request, so user input cannot redirect the destination host. Responses are size-capped (_MAX_RESPONSE_BYTES) and timeouts are set on every urlopen/subprocess call.

Why it matters

Enables internal pivoting and data exfiltration.

Evidence

def normalize_public_http_url(url: str) -> str:
    ...
    literal_address = _literal_ip_address(host)
    if (
        parsed.scheme.lower() not in {"http", "https"}
        or not host
        or parsed.username is not None
        or parsed.password is not None
        or "%" in host
        or host in _BLOCKED_PUBLIC_FETCH_HOSTS
        or host.endswith(_BLOCKED_PUBLIC_FETCH_SUFFIXES)
        or ("." not in host and literal_address is None)
        or (literal_address is not None and not literal_address.is_global)
    ):
…
Show 2 more locations
def read(self, url: str) -> str:
    url = normalize_public_http_url(url)
    jina_url = f"https://r.jina.ai/{url}"
    req = urllib.request.Request(
        jina_url,
        headers={"User-Agent": _UA, "Accept": "text/plain"},
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        body = resp.read(_MAX_RESPONSE_BYTES + 1)
def _validate_api_url(url: str) -> None:
    """Allow only the public V2EX HTTPS JSON API."""
    ...
    if (
        parsed.scheme.lower() != "https"
        or (parsed.hostname or "").lower() not in {"v2ex.com", "www.v2ex.com"}
        or port not in {None, 443}
        or parsed.username is not None
        or parsed.password is not None
        or not parsed.path.startswith("/api/")
    ):
        raise ValueError("only the V2EX HTTPS API is allowed")
PassedStatic SecretsATR-C0002Details

Why it got this result

.env.example ships only commented-out placeholder values with no real secret material. gitleaks scan found 0 findings repo-wide. Secrets are read from ~/.agent-reach/config.yaml (created at runtime, not committed) or from process.env. Config.to_dict() masks sensitive fields as '[REDACTED]'. Semgrep's atr-c0002 credential-to-log-sink rule flagged 4 lines (cli.py:1519,1532,1536 and cookie_extract.py:401) -- all 4 were read: cli.py:1519/1532/1536 write the literal status string 'requires login' into config.set()/print() (not a credential value), and cookie_extract.py:401 writes AUTH_TOKEN/CT0 to a private 0600 file (~/.config/bird/credentials.env) via atomic_write_private_text, shell-quoted with shlex.quote -- this is a private credential file write, not a log/stdout sink, and the file is owner-only. No credential value is printed, logged, or committed anywhere.

Why it matters

Plaintext secrets are easily exfiltrated.

Evidence

        if auth_token and ct0:
            config.set("twitter_auth_token", auth_token)
            config.set("twitter_ct0", ct0)

            print("✅ Twitter cookies 已保存到 ~/.agent-reach/config.yaml")
Show 2 more locations
                legacy_results = (
                    (
                        "~/.config/xfetch/session.json",
                        _sync_xfetch_session(auth_token, ct0),
                    ),
                    (
                        "~/.config/bird/credentials.env",
                        _sync_bird_env(auth_token, ct0),
                    ),
                )
        bird_dir = home_dir() / ".config" / "bird"
        make_private_dir(bird_dir)
        env_path = bird_dir / "credentials.env"
        atomic_write_private_text(
            env_path,
            f"AUTH_TOKEN={shlex.quote(auth_token)}\n"
            f"CT0={shlex.quote(ct0)}\n",
        )
        return True

+6 more locations in the finding JSON

PassedMissing or Inadequate Input ValidationATR-C0008Details

Why it got this result

The single MCP tool (get_status) takes no arguments (empty inputSchema), eliminating that attack surface entirely. The CLI entry points use argparse with explicit choices=[...] allowlists for all enum-like parameters (configure key, --from-browser browser, --platform, format platform) so out-of-range values are rejected by argparse itself before reaching application code. Free-text inputs that matter for security (URLs passed to web/transcribe channels) are validated by dedicated schema-like validators -- normalize_public_http_url(), _validate_api_url(), _assert_safe_public_url() -- which enforce scheme/host/port/userinfo constraints before any network or subprocess call. Config file inputs are size-capped (_MAX_CONFIG_BYTES) and type-checked (must parse to a dict) before use.

Why it matters

Frontline control to prevent multiple classes of vulns.

Evidence

    p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser")
    p_conf.add_argument("key", nargs="?", default=None,
                        choices=["proxy", "github-token", "groq-key", "openai-key",
                                 "twitter-cookies", "youtube-cookies",
                                 "xhs-cookies"],
                        help="What to configure (omit if using --from-browser)")
    p_conf.add_argument("value", nargs="*", help="The value(s) to set")
Show 2 more locations
    @server.list_tools()
    async def list_tools():
        return [
            Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
        ]
        try:
            payload = read_small_text_no_follow(
                self.config_path,
                max_bytes=_MAX_CONFIG_BYTES,
            )
        except PrivatePathError as exc:
            raise ConfigSecurityError(str(exc)) from exc
        if payload is None:
            self.data = {}
            return

        loaded = yaml.safe_load(payload) or {}
        if not isinstance(loaded, dict):
            raise ConfigError("配置文件顶层必须是对象")
PassedInsecure DeserializationATR-C0009Details

Why it got this result

No use of pickle, marshal, or yaml.load/yaml.unsafe_load anywhere in the codebase (grep for these patterns across agent_reach/ returned zero matches). All YAML parsing uses yaml.safe_load (config.py load(), github.py hosts.yml parsing) and all JSON parsing uses the standard json module, neither of which executes arbitrary code on untrusted input.

Why it matters

Can lead to arbitrary code execution.

Evidence

        loaded = yaml.safe_load(payload) or {}
        if not isinstance(loaded, dict):
            raise ConfigError("配置文件顶层必须是对象")
        self.data = loaded
Show 1 more location
    try:
        payload = yaml.safe_load(raw)
    except yaml.YAMLError as exc:
        raise GitHubConfigError("gh hosts.yml 不是有效的 UTF-8 YAML") from exc
PassedPath Traversal & Symlink Bypass (Filesystem Tools)ATR-C0011Details

Why it got this result

The only MCP tool exposed (get_status in integrations/mcp_server.py) takes no filesystem arguments at all -- it has an empty inputSchema and calls eyes.doctor_report() with no caller-supplied path. The CLI's filesystem writes (skill install, config, cookie sync) write only to fixed, hardcoded locations under the user's home directory (~/.agent-reach, ~/.claude/skills, ~/.config/bird, etc.) -- none of these paths are derived from untrusted/caller-supplied input. Every private-file read/write path goes through utils/paths.py helpers (ensure_no_symlink_path, read_small_text_no_follow, atomic_write_private_text) which walk every path component with os.lstat and reject any symlink before opening, preventing symlink-redirection attacks even though the destination paths are already fixed.

Why it matters

Traversal exposes arbitrary host files; can lead to code execution.

Evidence

    @server.list_tools()
    async def list_tools():
        return [
            Tool(name="get_status",
                 description="Get Agent Reach status: which channels are installed and active.",
                 inputSchema={"type": "object", "properties": {}}),
        ]

    @server.call_tool()
    async def call_tool(name: str, arguments: dict):
        try:
            if name == "get_status":
                result = eyes.doctor_report()
Show 1 more location
def ensure_no_symlink_path(path: str | Path, label: str = "路径") -> Path:
    """Reject any existing symlink component without resolving the path."""
    target = Path(path)
    absolute = Path(os.path.abspath(os.fspath(target)))
    current = Path(absolute.anchor)
    for part in absolute.parts[1:]:
        current /= part
        try:
            mode = os.lstat(current).st_mode
        except FileNotFoundError:
            continue
        if stat.S_ISLNK(mode):
PassedCI/CD Workflow Script InjectionATR-C0014Details

Why it got this result

The repository has a single GitHub Actions workflow (.github/workflows/pytest.yml) triggered only by push and pull_request (not pull_request_target, issue_comment, workflow_run, or repository_dispatch). No step interpolates any ${{ github.event.* }}, ${{ inputs.* }}, or other attacker-controlled expression into a run: shell block -- all run steps execute fixed commands (pip install, pytest, python -m build, wheel verification script). The matrix python-version values are static literals defined in the workflow file itself, not derived from any external input. Semgrep's github-actions-mutable-action-tag rule flagged 6 lines (all uses: actions/checkout@v4 / actions/setup-python@v5), which is a supply-chain pinning hygiene note (mutable major-version tag vs. pinned SHA), not a script-injection finding -- there is no shell-injection vector in this workflow.

Why it matters

CI/CD script injection can execute attacker-controlled commands with workflow tokens, repository secrets, trusted-runner access, artifact or cache authority, and release or deployment privileges.

Evidence

name: ci

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
    steps:
…
PassedDoS Protections: Timeouts, Concurrency, Streaming LimitsATR-C0004Details

Why it got this result

Not every HTTP response is byte-capped: several fixed-host health/status checks (bilibili.py, boss.py, xueqiu.py) use a bare resp.read() with no explicit cap. However, these calls target fixed, hardcoded API hosts rather than an attacker-selectable destination. The paths that are both attacker-triggerable and high-cost -- media download (yt-dlp, MAX_SOURCE_BYTES/MAX_AUDIO_SECONDS/MAX_CHUNKS) and the arbitrary-URL web-reader fetch (_MAX_RESPONSE_BYTES) -- remain explicitly bounded, and every urlopen/subprocess call carries a timeout. The exposed MCP surface itself is a single fixed, argument-free status tool with no concurrent untrusted request handling, so resource-exhaustion protection rests on bounding the attacker-controllable paths rather than on a universal response-size cap.

Why it matters

Resource exhaustion can crash or stall MCP.

Evidence

SIZE_LIMIT_BYTES = 24 * 1024 * 1024
CHUNK_SECONDS = 600
MAX_SOURCE_BYTES = 512 * 1024 * 1024
MAX_CHUNKS = 24
MAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024
MAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS
FFPROBE_TIMEOUT_SECONDS = 30
Show 2 more locations
_MAX_RESPONSE_BYTES = 5 * 1024 * 1024
_ANTIBOT_SCAN_BYTES = 4096
        with urllib.request.urlopen(req, timeout=30) as resp:
            body = resp.read(_MAX_RESPONSE_BYTES + 1)
        if len(body) > _MAX_RESPONSE_BYTES:
            raise ValueError(
                f"Jina Reader response exceeds {_MAX_RESPONSE_BYTES} byte limit"
            )
PassedDenial of Wallet / Cost ControlsATR-C0012Details

Why it got this result

The only billable external dependency is the Whisper transcription API (Groq/OpenAI), gated behind a user-supplied API key the user must configure themselves (no key ships with the repo). Each transcription run is bounded: MAX_SOURCE_BYTES (512MB download cap), MAX_AUDIO_SECONDS (4 hours via MAX_CHUNKS=24 * CHUNK_SECONDS=600) reject oversized/too-long media before any paid API call, and chunks are capped at MAX_TOTAL_CHUNK_BYTES. There is no loop or batch mode that could multiply billed calls unboundedly; transcribe() processes one source per invocation. All other channels (web/social/search) either use free public APIs or require the operator's own paid key (Exa), so Agent Reach itself has no mechanism to run up a bill beyond what the operator explicitly triggers per call.

Why it matters

Prevents cost-based disruption.

Evidence

MAX_SOURCE_BYTES = 512 * 1024 * 1024
MAX_CHUNKS = 24  # 4 hours at the standard 10-minute segment size
MAX_TOTAL_CHUNK_BYTES = 96 * 1024 * 1024
MAX_AUDIO_SECONDS = MAX_CHUNKS * CHUNK_SECONDS
Show 1 more location
def _require_duration_within_budget(path: Path) -> float:
    """Reject audio that cannot fit within the bounded chunk budget."""
    duration = _probe_audio_duration(path)
    if duration > MAX_AUDIO_SECONDS:
        max_minutes = MAX_AUDIO_SECONDS // 60
        raise TranscribeError(
            f"audio duration exceeds safety limit of {max_minutes} minutes"
        )
    return duration
Does not applyContainer/Runtime HardeningATR-C0006Details

Why it got this result

Agent Reach ships as a Python package (pip-installable CLI/MCP tool) with no Dockerfile, docker-compose, Helm chart, or Kubernetes manifest anywhere in the repository (confirmed: find for Dockerfile*/docker-compose*/*.tf/*.yaml under deploy-style paths returned nothing beyond the CI workflow and mcporter config). There is no container or cloud deployment artifact for this rule's container-hardening controls (USER, read-only FS, capabilities, seccomp, hostNetwork) to apply to.

Why it matters

Reduces blast radius of exploits.

Evidence

.:1–1
No Dockerfile, docker-compose.yml, Helm chart, or Kubernetes/Terraform manifest found in the repository tree (verified via recursive find for Dockerfile*, docker-compose*, *.tf, helm/, k8s/).
From build to runtime

You know what it is. Now see what it does.

A scan helps you understand what you’re building with. When your agent runs, BlueRock shows you what actually happens across tools and systems. Follow actions in context, understand behavior as it unfolds and add controls when you need them.

BUILDRegistry + scan
RUNSee actual execution
OPERATEObserve, control, optimize

Results reflect the code at the commit and date shown.