# Protocol Controls

`egress.protocolRules` adds protocol-aware controls after outbound traffic is allowed by `trafficRules`.

Use it when a sandbox must reach a broad protocol endpoint but only a subset of protocol operations should be allowed. Sandbox0 supports HTTP request controls and remote MCP controls over HTTP or HTTPS.

## How It Fits

| Layer | Purpose |
|-------|---------|
| `trafficRules` | Allow or deny the destination by domain, CIDR, port, and classified app protocol |
| `protocolRules` | Parse an allowed protocol request and allow, deny, or audit protocol operations |
| `credentialRules` | Resolve and inject outbound credentials for matching traffic |

Protocol controls are enforced in the data plane by `netd`. Regional control plane services store and distribute policy; they are not in the per-request data path.

## HTTP Request Policy

HTTP protocol rules inspect outbound HTTP requests. `httpMatch` scopes the rule to matching requests, while `http` defines the operation policy. `denied` is evaluated before `allowed`. If an allowed list is non-empty, only listed values are allowed.

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-api
          action: allow
          domains:
              - api.example.com
          ports:
              - port: 443
                protocol: tcp
    protocolRules:
        - name: api-readonly
          protocol: http
          domains:
              - api.example.com
          ports:
              - port: 443
                protocol: tcp
          tlsMode: terminate-reoriginate
          http:
              methods:
                  allowed:
                      - GET
                      - HEAD
                  denied:
                      - POST
                      - PUT
                      - DELETE
              paths:
                  allowedPrefixes:
                      - /v1/read
                  denied:
                      - /v1/read/private
```

When an HTTP request is denied, netd does not forward it upstream. It returns `403 Forbidden` to the sandbox process and writes the protocol decision to the netd audit event.

## MCP Tool Policy

MCP protocol rules inspect HTTP-carried JSON-RPC requests. For `tools/call`, netd reads `params.name` and evaluates the configured tool policy.

```yaml
mode: block-all
egress:
    trafficRules:
        - name: allow-docs-mcp
          action: allow
          domains:
              - mcp.example.com
          ports:
              - port: 443
                protocol: tcp
    protocolRules:
        - name: docs-mcp-tools
          protocol: mcp
          domains:
              - mcp.example.com
          ports:
              - port: 443
                protocol: tcp
          tlsMode: terminate-reoriginate
          httpMatch:
              methods:
                  - POST
              paths:
                  - /mcp
          mcp:
              tools:
                  allowed:
                      - read_file
                  denied:
                      - write_file
                      - run_command
```

`denied` is evaluated before `allowed`. If `allowed` is empty, tools are allowed unless they are listed in `denied`. If `allowed` is non-empty, only listed tools are allowed.

When a tool call is denied, netd does not forward the request upstream. It returns a JSON-RPC error response to the sandbox process and writes the protocol decision to the netd audit event.

MCP request bodies must be bounded and inspectable. If netd cannot safely read the JSON-RPC request body, the protocol rule fails closed and returns a JSON-RPC policy error.

## Update Examples

The SDK and CLI examples below update an existing sandbox. Set `SANDBOX_ID` to the sandbox you want to control.

<Endpoint method="PUT">
/api/v1/sandboxes/{'{id}'}/network
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `package main

import (
    "context"
    "log"
    "os"

    sandbox0 "github.com/sandbox0-ai/sdk-go"
    "github.com/sandbox0-ai/sdk-go/pkg/apispec"
)

func main() {
    ctx := context.Background()
    client, err := sandbox0.NewClient(
        sandbox0.WithToken(os.Getenv("SANDBOX0_TOKEN")),
        sandbox0.WithBaseURL(os.Getenv("SANDBOX0_BASE_URL")),
    )
    if err != nil {
        log.Fatal(err)
    }

    _, err = client.Sandbox(os.Getenv("SANDBOX_ID")).UpdateNetworkPolicy(ctx, apispec.SandboxNetworkPolicy{
        Mode: apispec.SandboxNetworkPolicyModeBlockAll,
        Egress: apispec.NewOptNetworkEgressPolicy(apispec.NetworkEgressPolicy{
            TrafficRules: []apispec.TrafficRule{{
                Name:    apispec.NewOptString("allow-docs-mcp"),
                Action:  apispec.TrafficRuleActionAllow,
                Domains: []string{"mcp.example.com"},
                Ports: []apispec.PortSpec{{
                    Port:     443,
                    Protocol: apispec.NewOptString("tcp"),
                }},
            }},
            ProtocolRules: []apispec.ProtocolRule{{
                Name:     apispec.NewOptString("docs-mcp-tools"),
                Protocol: apispec.ProtocolRuleProtocolMcp,
                Domains:  []string{"mcp.example.com"},
                Ports: []apispec.PortSpec{{
                    Port:     443,
                    Protocol: apispec.NewOptString("tcp"),
                }},
                TlsMode: apispec.NewOptEgressTLSMode(apispec.EgressTLSModeTerminateReoriginate),
                HttpMatch: apispec.NewOptHTTPMatch(apispec.HTTPMatch{
                    Methods: []string{"POST"},
                    Paths:   []string{"/mcp"},
                }),
                Mcp: apispec.NewOptMCPProtocolRule(apispec.MCPProtocolRule{
                    Tools: apispec.NewOptMCPToolPolicy(apispec.MCPToolPolicy{
                        Allowed: []string{"read_file"},
                        Denied:  []string{"write_file", "run_command"},
                    }),
                }),
            }},
        }),
    })
    if err != nil {
        log.Fatal(err)
    }
}`
    },
    {
      label: "Python",
      language: "python",
      code: `import os

from sandbox0 import Client
from sandbox0.apispec.models.egress_tls_mode import EgressTLSMode
from sandbox0.apispec.models.http_match import HTTPMatch
from sandbox0.apispec.models.mcp_protocol_rule import MCPProtocolRule
from sandbox0.apispec.models.mcp_tool_policy import MCPToolPolicy
from sandbox0.apispec.models.network_egress_policy import NetworkEgressPolicy
from sandbox0.apispec.models.port_spec import PortSpec
from sandbox0.apispec.models.protocol_rule import ProtocolRule
from sandbox0.apispec.models.protocol_rule_protocol import ProtocolRuleProtocol
from sandbox0.apispec.models.sandbox_network_policy import SandboxNetworkPolicy
from sandbox0.apispec.models.sandbox_network_policy_mode import SandboxNetworkPolicyMode
from sandbox0.apispec.models.traffic_rule import TrafficRule
from sandbox0.apispec.models.traffic_rule_action import TrafficRuleAction

with Client(
    token=os.environ["SANDBOX0_TOKEN"],
    base_url=os.environ["SANDBOX0_BASE_URL"],
) as client:
    client.sandbox(os.environ["SANDBOX_ID"]).update_network_policy(
        SandboxNetworkPolicy(
            mode=SandboxNetworkPolicyMode.BLOCK_ALL,
            egress=NetworkEgressPolicy(
                traffic_rules=[
                    TrafficRule(
                        name="allow-docs-mcp",
                        action=TrafficRuleAction.ALLOW,
                        domains=["mcp.example.com"],
                        ports=[PortSpec(port=443, protocol="tcp")],
                    )
                ],
                protocol_rules=[
                    ProtocolRule(
                        name="docs-mcp-tools",
                        protocol=ProtocolRuleProtocol.MCP,
                        domains=["mcp.example.com"],
                        ports=[PortSpec(port=443, protocol="tcp")],
                        tls_mode=EgressTLSMode.TERMINATE_REORIGINATE,
                        http_match=HTTPMatch(methods=["POST"], paths=["/mcp"]),
                        mcp=MCPProtocolRule(
                            tools=MCPToolPolicy(
                                allowed=["read_file"],
                                denied=["write_file", "run_command"],
                            ),
                        ),
                    )
                ],
            ),
        )
    )
`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `import { Client, models } from "sandbox0";

const client = new Client({
    token: process.env.SANDBOX0_TOKEN!,
    baseUrl: process.env.SANDBOX0_BASE_URL!,
});

async function main() {
    await client.sandbox(process.env.SANDBOX_ID!).updateNetworkPolicy({
        mode: models.SandboxNetworkPolicyModeEnum.BlockAll,
        egress: {
            trafficRules: [
                {
                    name: "allow-docs-mcp",
                    action: models.TrafficRuleAction.Allow,
                    domains: ["mcp.example.com"],
                    ports: [{ port: 443, protocol: "tcp" }],
                },
            ],
            protocolRules: [
                {
                    name: "docs-mcp-tools",
                    protocol: models.ProtocolRuleProtocol.ProtocolRuleProtocolMcp,
                    domains: ["mcp.example.com"],
                    ports: [{ port: 443, protocol: "tcp" }],
                    tlsMode: models.EgressTLSMode.TerminateReoriginate,
                    httpMatch: {
                        methods: ["POST"],
                        paths: ["/mcp"],
                    },
                    mcp: {
                        tools: {
                            allowed: ["read_file"],
                            denied: ["write_file", "run_command"],
                        },
                    },
                },
            ],
        },
    });
}

main().catch((error) => {
    console.error(error);
    process.exit(1);
});`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox network update \\
  --sandbox-id "$SANDBOX_ID" \\
  --mode block-all \\
  --traffic-rule '{"name":"allow-docs-mcp","action":"allow","domains":["mcp.example.com"],"ports":[{"port":443,"protocol":"tcp"}]}' \\
  --protocol-rule '{"name":"docs-mcp-tools","protocol":"mcp","domains":["mcp.example.com"],"ports":[{"port":443,"protocol":"tcp"}],"tlsMode":"terminate-reoriginate","httpMatch":{"methods":["POST"],"paths":["/mcp"]},"mcp":{"tools":{"allowed":["read_file"],"denied":["write_file","run_command"]}}}'`
    }
  ]}
/>

## HTTPS Inspection

For HTTPS HTTP and MCP protocol rules, set `tlsMode: terminate-reoriginate` on the protocol rule. Sandbox0 then uses the netd MITM CA already exposed to procd-managed processes.

Without TLS termination, netd can still apply destination-level `trafficRules`, but it cannot read HTTPS request contents or enforce request-level protocol policy.

## Current Scope

HTTP protocol controls support request method and path allow/deny lists for HTTP/1.1 and HTTP/2 HTTPS requests when TLS termination is enabled.

MCP protocol controls target remote MCP over HTTP/HTTPS Streamable HTTP style requests:

- `tools/call` is enforced by tool name.
- `tools/list` is audited but not filtered.
- HTTP/1.1 and HTTP/2 HTTPS requests can be inspected when TLS termination is enabled.
- Local `stdio` MCP does not traverse netd and must be controlled by a tool runner or procd-level policy.
- Tool argument semantics are server-specific. Use tool-name allowlists as the strong boundary; treat argument-level checks as future protocol-adapter extensions.

## Next Steps

<CardGroup>
  <Card title="Network" href="/docs/sandbox/network" cta="Continue">
    Configure outbound destination policy and protocol-aware rules.
  </Card>

  <Card title="Egress Auth" href="/docs/sandbox/credential/egress-auth" cta="Continue">
    Inject credentials for allowed outbound traffic.
  </Card>
</CardGroup>
