Workspace, shell, and SSRF security boundaries
Corresponding source
| Security layer | Source |
|---|---|
| Workspace paths and policy | nanobot/security/workspace_access.py, workspace_policy.py |
| Network targets and SSRF | nanobot/security/network.py |
| File, Shell, and Sandbox Tools | nanobot/agent/tools/path_utils.py, filesystem.py, shell.py, sandbox.py |
| Runner violation classification | nanobot/agent/runner.py::_classify_violation |
| Repeated-violation feedback | nanobot/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: bwrapon 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→restrictedrestrictToWorkspace=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_pathnanobot/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:
- resolve the current
ToolWorkspace; - verify that
working_dirremains inside the workspace; - use
_guard_command()to inspect dangerous commands, paths, and private-network URLs; - construct a minimal environment;
- wrap the command when a sandbox is configured;
- 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 unsandboxedIt 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
restrictToWorkspaceenabled and no system Provider,workspace_sandbox_status()reports application-level protection withenforced=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
0for 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:
- the URL scheme is HTTP or HTTPS;
- a hostname exists;
- every address for the hostname is resolved;
- 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
localhostor 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.pynanobot/agent/tools/mcp.pynanobot/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=Trueactually justified? - Are there security-boundary tests in addition to a happy path?
11. Source and tests
Source:
nanobot/security/workspace_access.pynanobot/security/workspace_policy.pynanobot/security/network.pynanobot/agent/tools/path_utils.pynanobot/agent/tools/filesystem.pynanobot/agent/tools/shell.pynanobot/agent/tools/sandbox.pynanobot/agent/runner.py::_classify_violationnanobot/utils/runtime.py::repeated_workspace_violation_error
Tests:
tests/security/test_security_network.pytests/security/test_workspace_policy.pytests/security/test_workspace_sandbox.pytests/tools/test_filesystem_tools.pytests/tools/test_exec_platform.pytests/tools/test_exec_security.pytests/tools/test_web_fetch_security.pytests/tools/test_web_fetch_url_sanitization.py
Self-check
- How do the trust boundaries of the application workspace guard and bwrap differ?
- Why can SSRF checking not search only for
127.0.0.1in URL text? - What execution path actually runs after configuring bwrap on Windows?
- Why can a prompt saying “do not read private files” not replace a path guard?