Workspace, shell, and SSRF security boundaries

Corresponding source

Security layerSource
Workspace paths and policynanobot/security/workspace_access.py, workspace_policy.py
Network targets and SSRFnanobot/security/network.py
File, Shell, and Sandbox Toolsnanobot/agent/tools/path_utils.py, filesystem.py, shell.py, sandbox.py
Runner violation classificationnanobot/agent/runner.py::_classify_violation
Repeated-violation feedbacknanobot/utils/runtime.py::repeated_workspace_violation_error

Learning objectives

After this lesson, you should be able to:

  • distinguish workspace scope, filesystem path guard, Shell command guard, and OS sandbox;
  • explain why the SSRF guard inspects both the URL and DNS results;
  • determine the real effect of configuring sandbox: bwrap on Windows;
  • list the security constraints a new Tool must inherit.

Prerequisites

  • Understand absolute paths, relative paths, symbolic links, and Path.resolve().
  • Know that SSRF is the risk of inducing a server to request a private network or cloud metadata endpoint.
  • Treat model-generated arguments as untrusted input even when the system prompt tells the model to follow rules.

1. Where the security boundary begins

In an Agent system, all of these are untrusted:

  • user messages;
  • prompt injection in webpages and documents;
  • model-generated Tool arguments;
  • data returned by an MCP server;
  • filenames and URLs uploaded through a Channel;
  • error text from an external Provider.

Security cannot live only in a prompt. nanobot places key constraints in deterministic code before Tool execution.

2. WorkspaceScope and ToolWorkspace

nanobot/security/workspace_access.py defines:

  • WorkspaceScope: project path, access mode, and sandbox state for one Turn;
  • WorkspaceScopeResolver: resolves the current scope from the inbound message and Session metadata;
  • ToolWorkspace: effective root and restriction flags visible to a Tool;
  • a ContextVar: carries the current Turn’s scope into deep Tool code without changing every function signature.

Default access mode:

  • restrictToWorkspace=true → restricted
  • restrictToWorkspace=false → full

WebSocket can carry project scope. Other Channels use the configured workspace scope by default. Verify branches in WorkspaceScopeResolver.for_turn().

3. Path constraints for filesystem Tools

Filesystem Tools resolve paths and enforce containment through:

  • nanobot/agent/tools/path_utils.py::resolve_workspace_path
  • nanobot/security/workspace_policy.py::resolve_allowed_path

Allowed scope is based on resolved directory relationships, not a string prefix. Depending on the Tool, it can also include:

  • read-only media directories;
  • explicitly allowed additional directories;
  • exact individual files.

Read roots and write roots can differ. A new filesystem Tool should reuse the shared path helper rather than implementing if path.startswith(workspace).

4. Shell guard and sandbox are separate layers

ExecTool._prepare_command() applies an application-level check first:

  1. resolve the current ToolWorkspace;
  2. verify that working_dir remains inside the workspace;
  3. use _guard_command() to inspect dangerous commands, paths, and private-network URLs;
  4. construct a minimal environment;
  5. wrap the command when a sandbox is configured;
  6. create the platform-specific subprocess.
flowchart TD
    A["Model-generated command"] --> B["working_dir containment"]
    B --> C["deny / allow patterns and path guard"]
    C --> D["SSRF URL guard"]
    D --> E{"Sandbox configured?"}
    E -->|"No"| F["Host shell"]
    E -->|"Yes, Unix"| G["wrap_command"]
    G --> H["bwrap"]
    E -->|"Yes, Windows"| I["Log warning"]
    I --> F

The application guard blocks known escape patterns, but it is not kernel-enforced isolation.

5. Current sandbox backend

nanobot/agent/tools/sandbox.py currently registers only:

_BACKENDS = {"bwrap": _bwrap}

The Bubblewrap wrapper broadly provides:

  • a read/write bind mount for the workspace;
  • read-only media directories;
  • tmpfs over the workspace parent to hide config.json;
  • limited views of /tmp, /proc, /dev, and related system paths;
  • --die-with-parent, terminating with the parent process.

On Windows, ExecTool explicitly logs:

Sandbox '...' is not supported on Windows; running unsandboxed

It then executes the command without a bwrap wrapper. Configuring "sandbox": "bwrap" on Windows is therefore not evidence of an OS sandbox.

Describe this boundary precisely

With restrictToWorkspace enabled and no system Provider, workspace_sandbox_status() reports application-level protection with enforced=False. This is the source’s explicit description of its own security level.

6. Other Shell limits

ExecTool also implements:

  • a default 60-second hard timeout and a model-requested timeout capped at 600 seconds;
  • configuration timeout 0 for no limit;
  • output truncation;
  • allow/deny regular expressions;
  • an allowlist for forwarded environment variables;
  • default deny patterns for dangerous disk, deletion, and shutdown commands;
  • protection against direct overwrite of internal Memory history/cursors;
  • subprocess termination on cancellation or timeout.

These reduce risk but cannot enumerate every Shell semantic. High-risk deployments still need containers, OS sandboxing, a least-privilege user, and network policy.

7. SSRF guard

validate_url_target() checks in order:

  1. the URL scheme is HTTP or HTTPS;
  2. a hostname exists;
  3. every address for the hostname is resolved;
  4. any blocked address causes rejection.

Blocked ranges currently include:

  • loopback;
  • RFC 1918 private networks;
  • CGNAT;
  • link-local;
  • common cloud metadata ranges;
  • IPv6 loopback, ULA, and link-local.

IPv4-mapped IPv6 is normalized first so ::ffff:127.0.0.1 cannot bypass the IPv4 blocklist.

Redirect targets are checked again by validate_resolved_url(). tools.ssrfWhitelist can add explicit CIDRs, and load_config() calls _apply_ssrf_whitelist().

8. Narrow exception for local WebUI preview

Code can allow literal loopback under a specific workspace scope for local preview. The allow_loopback=True exception is narrow:

  • hostname must be localhost or a literal loopback;
  • every resolved address must be loopback;
  • RFC 1918, link-local, and metadata ranges remain blocked;
  • a public hostname resolving to loopback is not treated as an allowed local alias.

This is much safer than “allow all private networks.”

9. Credentials and access control

For a learning environment:

  • keep keys in environment variables and store only ${DEEPSEEK_API_KEY} in configuration;
  • do not log resolved configuration, environment variables, or complete HTTP headers;
  • configure WebUI/Gateway tokens, Pairing, and Channel allowlists according to official documentation;
  • do not expose WebUI publicly while retaining localhost trust assumptions;
  • review an MCP HTTP server as an external service and an MCP stdio command as local code execution.

Related entry points:

  • nanobot/pairing/
  • nanobot/gateway/
  • nanobot/channels/websocket.py
  • nanobot/agent/tools/mcp.py
  • nanobot/config/schema.py

10. Security checklist for a new Tool

Before adding a Tool, answer:

  • Are arguments validated by Pydantic/JSON Schema?
  • Do filesystem paths use resolve_workspace_path()?
  • Are write roots narrower than read roots where appropriate?
  • Do URLs use validate_url_target(), with redirects checked again?
  • Could the Tool leak environment variables, tokens, or local paths?
  • Do timeout and cancellation clean up resources?
  • Could its error text encourage the model to bypass policy?
  • Is concurrency_safe=True actually justified?
  • Are there security-boundary tests in addition to a happy path?

11. Source and tests

Source:

  • nanobot/security/workspace_access.py
  • nanobot/security/workspace_policy.py
  • nanobot/security/network.py
  • nanobot/agent/tools/path_utils.py
  • nanobot/agent/tools/filesystem.py
  • nanobot/agent/tools/shell.py
  • nanobot/agent/tools/sandbox.py
  • nanobot/agent/runner.py::_classify_violation
  • nanobot/utils/runtime.py::repeated_workspace_violation_error

Tests:

  • tests/security/test_security_network.py
  • tests/security/test_workspace_policy.py
  • tests/security/test_workspace_sandbox.py
  • tests/tools/test_filesystem_tools.py
  • tests/tools/test_exec_platform.py
  • tests/tools/test_exec_security.py
  • tests/tools/test_web_fetch_security.py
  • tests/tools/test_web_fetch_url_sanitization.py

Self-check

  1. How do the trust boundaries of the application workspace guard and bwrap differ?
  2. Why can SSRF checking not search only for 127.0.0.1 in URL text?
  3. What execution path actually runs after configuring bwrap on Windows?
  4. Why can a prompt saying “do not read private files” not replace a path guard?

Next: Packaging, WebUI build, and cross-platform behavior