Sandbox API
dcc_mcp_core (sandbox module)
Script execution sandbox with API allowlist, path controls, audit logging, and input validation.
Overview
Enterprise users (game studios, VFX facilities) have strong security requirements that vanilla Python-based DCC MCP integrations cannot satisfy. The sandbox module provides:
- API allowlist — restrict which DCC actions an Agent may invoke
- Path allowlist — restrict filesystem access to safe directories
- Audit log — structured record of every action invocation (
AuditEntry/AuditLog) - Input validation — field-level rules with injection guards (
InputValidator) - Read-only mode — Agent can query but not mutate the scene
SandboxPolicy
Security policy configuration for a sandbox session.
Constructor
from dcc_mcp_core import SandboxPolicy
policy = SandboxPolicy()Methods
| Method | Returns | Description |
|---|---|---|
allow_actions(actions) | None | Restrict execution to only these actions (replaces previous allowlist) |
deny_actions(actions) | None | Deny these actions even if on the allowlist |
allow_paths(paths) | None | Allow filesystem access inside these directory paths |
set_timeout_ms(ms) | None | Set execution timeout in milliseconds |
set_max_actions(count) | None | Set max number of actions allowed per session |
set_read_only(read_only) | None | Enable/disable read-only mode |
Properties
| Property | Type | Description |
|---|---|---|
is_read_only | bool | Whether policy is in read-only mode |
Example
policy = SandboxPolicy()
policy.allow_actions(["get_scene_info", "list_objects", "get_object_info"])
policy.deny_actions(["delete_scene", "delete_object"])
policy.allow_paths(["/studio/assets", "/tmp/renders"])
policy.set_timeout_ms(5000)
policy.set_max_actions(100)
policy.set_read_only(False)
print(policy.is_read_only) # FalseUse allowlists, not denylists
Start with no actions allowed, then explicitly permit safe ones. This is more secure than listing everything you want to block.
SandboxContext
Main sandbox execution context. Bundles a SandboxPolicy with an AuditLog and an action counter.
Constructor
from dcc_mcp_core import SandboxPolicy, SandboxContext
policy = SandboxPolicy()
policy.allow_actions(["get_scene_info", "list_objects"])
ctx = SandboxContext(policy)Methods
| Method | Returns | Description |
|---|---|---|
set_actor(actor) | None | Set caller identity for audit entries |
execute_json(action, params_json) | str | Execute action with JSON params, returns JSON result |
is_allowed(action) | bool | Check if action is permitted by current policy |
is_path_allowed(path) | bool | Check if path is within an allowed directory |
Properties
| Property | Type | Description |
|---|---|---|
action_count | int | Number of actions successfully executed |
audit_log | AuditLog | The audit log for this context |
Execution
ctx.set_actor("claude-agent")
# Execute with JSON params — returns JSON string
result = ctx.execute_json("get_scene_info", "{}")
print(result) # '{"name": "my_scene", "object_count": 42, ...}'
# Check permissions without executing
if ctx.is_allowed("delete_object"):
ctx.execute_json("delete_object", '{"name": "pSphere1"}')Errors raise RuntimeError
execute_json() raises RuntimeError if the action is denied, validation fails, or a sandbox error occurs.
Audit Log Access
log = ctx.audit_log
print(len(log)) # number of recorded entries
for entry in log.entries():
print(f"{entry.actor}: {entry.action} → {entry.outcome} ({entry.duration_ms}ms)")
# Filtered views
denied = log.denials()
succeeded = log.successes()
# Serialize to JSON
json_str = log.to_json()AuditEntry
A single audit record for one action invocation. All attributes are read-only properties.
Properties
| Property | Type | Description |
|---|---|---|
timestamp_ms | int | Unix timestamp in milliseconds |
actor | str | None | Caller identity, or None |
action | str | Action name |
params_json | str | Parameters as JSON string |
duration_ms | int | Execution duration in milliseconds |
outcome | str | "success", "denied", "error", or "timeout" |
outcome_detail | str | None | Denial reason or error message, or None |
for entry in ctx.audit_log.entries():
print(f"[{entry.timestamp_ms}] {entry.actor}: {entry.action}")
print(f" params: {entry.params_json}")
print(f" outcome: {entry.outcome} ({entry.duration_ms}ms)")
if entry.outcome_detail:
print(f" detail: {entry.outcome_detail}")AuditLog
Read-only view of the sandbox audit log.
Methods
| Method | Returns | Description |
|---|---|---|
entries() | list[AuditEntry] | All recorded entries |
successes() | list[AuditEntry] | Entries with outcome "success" |
denials() | list[AuditEntry] | Entries with outcome "denied" |
entries_for_action(action) | list[AuditEntry] | Entries for a specific action |
to_json() | str | All entries serialized as a JSON array |
__len__() | int | Number of entries |
log = ctx.audit_log
print(f"Total: {len(log)}")
print(f"Successes: {len(log.successes())}")
print(f"Denials: {len(log.denials())}")
# All scene_info queries
scene_queries = log.entries_for_action("get_scene_info")
# Export for logging system
import json
entries_json = log.to_json()InputValidator
Field-level input validator with injection guards. Use to validate parameters before passing to SandboxContext.execute_json().
Constructor
from dcc_mcp_core import InputValidator
validator = InputValidator()Methods
| Method | Returns | Description |
|---|---|---|
require_string(field, max_length=None, min_length=None) | None | Register a required string field with optional length bounds |
require_number(field, min_value=None, max_value=None) | None | Register a required numeric field with optional range |
forbid_substrings(field, substrings) | None | Injection guard: field must not contain any listed substring |
validate(params_json) | tuple[bool, str | None] | Validate JSON params; returns (True, None) or (False, error) |
Example
from dcc_mcp_core import InputValidator
validator = InputValidator()
# Field constraints
validator.require_string("name", min_length=1, max_length=64)
validator.require_number("radius", min_value=0.01, max_value=1000.0)
# Injection guards
validator.forbid_substrings("script", ["__import__", "exec(", "eval(", "os.system"])
# Valid input
ok, error = validator.validate('{"name": "sphere1", "radius": 2.0}')
print(ok, error) # True, None
# Injection attempt blocked
ok, error = validator.validate('{"script": "__import__(os)"}')
print(ok, error) # False, "field 'script' contains forbidden substring '__import__'"
# Runtime error on invalid JSON
try:
validator.validate("not json")
except RuntimeError as e:
print(f"Invalid JSON: {e}")validate() vs ToolValidator
InputValidator is for sandbox field-level rules (length, range, injection guards). ToolValidator (from the actions module) validates against a JSON Schema. Use InputValidator inside a sandbox; use ToolValidator at the tool dispatch layer.
Best Practices
- Always use allowlists — Start with no actions allowed, then explicitly permit safe ones
- Set timeouts — Prevent runaway scripts from hanging the DCC
- Limit action counts —
set_max_actions()prevents runaway agent loops - Enable audit logging — Always inspect
ctx.audit_logafter a session - Use read-only mode — Enable when only querying data to prevent accidental mutations
- Add injection guards — Use
InputValidator.forbid_substrings()for any script/code parameters - Validate paths — Use
allow_paths()+ctx.is_path_allowed()to prevent path traversal
Full Example
from dcc_mcp_core import SandboxPolicy, SandboxContext, InputValidator
# Build policy
policy = SandboxPolicy()
policy.allow_actions(["get_scene_info", "list_objects", "run_script"])
policy.allow_paths(["/studio/assets"])
policy.set_timeout_ms(10000)
policy.set_max_actions(50)
# Build validator for run_script
validator = InputValidator()
validator.require_string("script", max_length=10000)
validator.forbid_substrings("script", [
"__import__", "exec(", "eval(", "os.system", "subprocess",
])
# Create context
ctx = SandboxContext(policy)
ctx.set_actor("my-ai-agent")
# Execute a safe read query
result = ctx.execute_json("get_scene_info", "{}")
print(result)
# Attempt a potentially dangerous action (blocked by policy)
try:
ctx.execute_json("delete_scene", "{}")
except RuntimeError as e:
print(f"Blocked: {e}")
# Review audit log
for entry in ctx.audit_log.entries():
print(f"{entry.action}: {entry.outcome}")