> ## Documentation index
> Fetch the complete index at: https://maritime.sh/llms.txt
> Setup procedure for AI agents (install, sign in, deploy, verify): https://maritime.sh/agents.md
> This page as HTML: https://maritime.sh/docs/networks

# Networks

Build a platform

Maritime Networks is the network layer for agents and computers. Every agent and computer has three
network settings: what it may reach on the *internet*, where its
traffic *exits*, and which of your *private* systems it may use. The defaults reach everything from a
shared address and nothing private. Change one setting and the platform enforces it on the host,
outside the workload, so the workload cannot talk its way around it.

## The three settings

| Setting | Values |
| --- | --- |
| `internet` | `all` reaches everything (the default). `list` reaches only the entries of a preset plus the workload's own `allow` list. `none` reaches nothing on the internet. A `deny` list of addresses or CIDRs wins over every allow. |
| `exit` | `shared` leaves from the account's shared address (the default). `fixed` gives the workload its own address that stays the same, for allowlists on the other side. `country` leaves from a residential IP in a chosen country. `through` leaves through one of your private networks, so the other side sees your office or your server. |
| `access` | The private resources the workload may reach, by id. A resource is a host and port you published from your own machine or server with `maritime connect --allow host:port`. Inside the workload the resource answers to its name, such as `db.office.private`. |

Allow entries are domains (`api.example.com`), wildcards (`*.example.com`, subdomains at any depth,
not the apex), IPv4 addresses, or IPv4 CIDRs. Deny entries are IPv4 addresses or CIDRs. The Maritime
platform itself stays reachable in every mode. `list` with no entries and no preset equals `none`.
A fourth switch, `cutOff`, stops all traffic at once and restores it when turned off.

## Presets

A preset is a named allow and deny list shared by every agent and computer in the account. Two are
built in. `coding` allows package registries (PyPI, npm, crates, Go, Maven, RubyGems, NuGet, pub.dev),
source hosting (GitHub), container registries (Docker Hub, GHCR), Linux package mirrors, Nix, and
Hugging Face. `llm-apis` allows the hosted model APIs (OpenAI, Anthropic, Google, OpenRouter, Mistral,
Groq, Together, xAI, DeepSeek, Cohere, Fireworks). Your own presets live in
[Company settings, Policies](https://maritime.sh/private-networks), the CLI, and the API. Deleting a
preset that workloads use is refused and the refusal counts them.

## Residential IPs

The `country` exit sends the workload's traffic out through a residential IP: a home broadband or
mobile address in the country you pick, from Maritime's residential provider. Sites that refuse cloud
addresses see an ordinary home connection. TCP goes through the exit and names resolve there. UDP is
dropped, except DNS.

| Field | Values |
| --- | --- |
| `countryCode` | An ISO code such as `US` or `DE`. |
| `state` | A US state code such as `CA`. Only with `countryCode` `US`. |
| `city` | A city such as `Austin`. The provider checks it when you save; a city it does not know is refused with 422. |
| `session` | `sticky` (the default) keeps one IP for the workload for up to 24 hours. `rotating` gets a new IP for every connection. |

Each workload has its own session, so two workloads never share a sticky IP. To get a new sticky IP
at once, call `new-ip`; the next connection uses a new address. Changing the state, the city, or
the session also starts a new session.

Residential traffic is billed per GB, in and out, at the rate the Exit row shows next to the option
(`options.country.priceLabel` in the API). Residential IPs are paid upfront for each calendar
month (UTC): choosing a residential exit charges the saved card the monthly amount first
(`options.country.upfrontDueCents`, 0 once the month is paid), and a declined card gives 402 with the
reason. The payment covers that month's traffic up to the same amount. It renews on the 1st while a
workload still has a residential exit; if the renewal fails, residential traffic stops until it is paid. Residential traffic counts toward the account's monthly cap
(`countryDataCapGb` in account settings). Past the cap, connections are refused with outcome
`over_cap` until the month ends. Bytes are counted when a connection closes, so a long connection
counts when it ends.

With your own proxy (`ownProxyUrl` in account settings), the `country` exit goes through it
instead. It is not billed, and `state`, `city`, and `session` do not apply.

CLI:

```
# A residential IP in California that stays the same for the agent
maritime agents update <agent_id> --egress country:US --egress-state CA

# A new residential IP in Austin for every connection
maritime agents update <agent_id> --egress country:US --egress-city Austin --egress-session rotating

# A new sticky IP now
maritime agents network new-ip <agent_id>
```

TypeScript:

```typescript
await client.agents.updateNetwork(agentId, {
  exit: { mode: 'country', countryCode: 'US', state: 'CA', session: 'sticky' },
})
await client.agents.newIp(agentId)
```

Python:

```python
client.agents.update_network(
    agent_id,
    exit={"mode": "country", "countryCode": "US", "state": "CA", "session": "sticky"},
)
client.agents.new_ip(agent_id)
```

curl:

```bash
curl -X PUT https://api.maritime.sh/api/v1/network/workloads/agent/$AGENT_ID \
  -H "Authorization: Bearer $MARITIME_API_KEY" -H 'Content-Type: application/json' \
  -d '{"exit": {"mode": "country", "countryCode": "US", "city": "Austin", "session": "rotating"}}'

curl -X POST https://api.maritime.sh/api/v1/network/workloads/agent/$AGENT_ID/new-ip \
  -H "Authorization: Bearer $MARITIME_API_KEY"
```

## Dashboard

Open an agent, then **Settings**. The **Network** card shows the three settings as three rows, each
one a click to change. The Exit row shows the address the internet sees today. The Test box tries one
connection from inside the agent and names what stopped it. View activity lists what the workload
connected to, with the outcome. Computers show the same card.

## CLI

The same six flags set the network on `maritime agents create`, `maritime agents update`, and
`maritime computers create`. Flags not given keep their current value; `none` clears a list.

| Flag | Meaning |
| --- | --- |
| `--internet all\|none\|list` | The internet mode. `--allow-out` implies `list`. |
| `--allow-out <entry>` | One allow entry, repeatable: domain, `*.domain`, IPv4 address, or CIDR. |
| `--deny-out <cidr>` | One deny entry, repeatable. Deny wins over allow. |
| `--network-policy <preset>` | A preset by name or id, such as `coding` or `llm-apis`. |
| `--egress shared\|fixed\|country:<CC>\|through:<network>` | Where the traffic exits. `country:<CC>` is a residential IP. |
| `--egress-state <ST>`, `--egress-city <city>`, `--egress-session sticky\|rotating` | Target the residential IP. Need `--egress country:<CC>`. |
| `--access <resource>` | One private resource by address or id, repeatable. |

```
# Only package registries, source hosting, and one API of your own
maritime agents create --name worker --template-id codex \
  --internet list --network-policy coding --allow-out api.example.com

# Change a running agent: its own fixed address, and a database on your machine
maritime agents update <agent_id> --egress fixed --access db.office.private

# What is in force, and the address the internet sees
maritime agents network <agent_id>

# Try one connection from inside the agent
maritime agents network test <agent_id> api.example.com:443

# Presets shared by every agent and computer in the account
maritime network presets list
maritime network presets create --name partners --allow api.partner.com --allow '*.partner.com'
maritime network presets delete <preset_id>
```

`maritime agents network test` exits non-zero when the destination is not reached, so it works in
scripts. A create that asks for a setting the account cannot have is refused with 422 and creates
nothing.

## API

One object, the same everywhere. Read and change it per workload, send it as `network` at creation,
and manage presets at `/api/v1/network/presets`. The workload kind is `agent`, `computer`, or `sandbox`; a sandbox takes the `shared` and `country` exits only. Every
field in an update is optional and replaces what it names; `presetId: null` clears the preset.

| Operation | Route |
| --- | --- |
| `read` | GET /api/v1/network/workloads/{kind}/{id} |
| `change` | PUT /api/v1/network/workloads/{kind}/{id} `{internet?, exit?, access?, cutOff?}` |
| `test` | POST /api/v1/network/workloads/{kind}/{id}/test `{destination}` |
| `new IP` | POST /api/v1/network/workloads/{kind}/{id}/new-ip: a new sticky residential session |
| `at creation` | POST /api/agents and POST /api/v1/computers with `network: {internet?, exit?, access?}` |
| `presets` | GET /api/v1/network/presets, POST /api/v1/network/presets `{name, allow, deny}`, PUT /api/v1/network/presets/{id} `{name?, allow?, deny?}`, DELETE /api/v1/network/presets/{id} |
| `private networks` | GET and POST /api/v1/network/private-networks `{name}`, PATCH and DELETE /api/v1/network/private-networks/{id} |
| `resources` | POST /api/v1/network/private-networks/{id}/resources `{name, target}`, DELETE /api/v1/network/resources/{id} |
| `install link` | POST /api/v1/network/private-networks/{id}/install `{allowInternet?}`: a one-time link and the commands that connect a machine |
| `account settings` | GET and PUT /api/v1/network/settings `{cutOff?, allowFixed?, allowCountry?, allowThrough?, patientData?, ownProxyUrl?, countryDataCapGb?}` |
| `activity` | GET /api/v1/network/activity ?networkId, workloadKind, workloadId, limit: connections, newest first |

Change:

```json
PUT /api/v1/network/workloads/agent/{agent_id}
{
  "internet": { "mode": "list", "presetId": "coding", "allow": ["api.example.com"], "deny": [] },
  "exit": { "mode": "fixed" },
  "access": ["<resource_id>"]
}
```

At creation:

```json
POST /api/agents
{
  "name": "worker",
  "templateId": "codex",
  "network": { "internet": { "mode": "list", "presetId": "llm-apis" } }
}
```

Test:

```json
POST /api/v1/network/workloads/agent/{agent_id}/test
{ "destination": "api.example.com:443" }

{ "destination": "api.example.com:443", "reached": false, "durationMs": null,
  "reason": "Refused by the internet policy." }
```

The read returns the settings, `exitIp` (the address the internet sees today, null for country and
through), `enforcement` (`pending` until the host confirms, then `active`), `metadataUrl`, and
`options`: the presets, and for each exit mode whether the account may pick it and the sentence to
show when it may not. A 422 carries that same sentence.

## SDK

### TypeScript

```typescript
// At creation. A 422 names the reason and creates nothing.
const agent = await maritime.agents.create({
  name: 'worker',
  template: 'codex',
  network: { internet: { mode: 'list', presetId: 'coding', allow: ['api.example.com'] } },
})

// Later. Every field given replaces what it names.
await maritime.agents.updateNetwork(agent.id, { exit: { mode: 'fixed' } })

const net = await maritime.agents.network(agent.id)
net.exitIp          // the address the internet sees today
net.enforcement     // 'active' once the host confirms

const probe = await maritime.agents.testNetwork(agent.id, 'api.example.com:443')
probe.reached       // false
probe.reason        // 'Refused by the internet policy.'

// Presets
const presets = await maritime.network.presets.list()
await maritime.network.presets.create({ name: 'partners', allow: ['api.partner.com'] })

// Computers take the same object
await maritime.computers.create({ externalUserId: 'u_123', network: { internet: { mode: 'none' } } })
```

### Python

```python
# At creation. A 422 names the reason and creates nothing.
agent = client.agents.create(
    "worker",
    template="codex",
    network={"internet": {"mode": "list", "presetId": "coding", "allow": ["api.example.com"]}},
)

# Later. Every argument given replaces what it names.
client.agents.update_network(agent["id"], exit={"mode": "fixed"})

net = client.agents.network(agent["id"])
net["exitIp"]          # the address the internet sees today
net["enforcement"]     # "active" once the host confirms

probe = client.agents.test_network(agent["id"], "api.example.com:443")
probe["reached"]       # False
probe["reason"]        # "Refused by the internet policy."

# Presets
presets = client.network.presets.list()
client.network.presets.create("partners", allow=["api.partner.com"])

# Computers take the same object
client.computers.create(external_user_id="u_123", network={"internet": {"mode": "none"}})
```

## Inside the workload

A program inside the workload reads its own settings at `http://100.127.254.1/network`. It is the
live view: it changes the moment you change a setting. The `access` entries name what to connect to.

```
$ curl -s http://100.127.254.1/network
{
  "kind": "agent",
  "workloadId": "...",
  "enforcement": "active",
  "cutOff": false,
  "internet": { "mode": "list", "allow": ["github.com", "api.example.com", "..."], "deny": [] },
  "exit": { "mode": "fixed", "ip": "203.0.113.7", "countryCode": null, "networkName": null },
  "access": [
    { "name": "db.office.private", "address": "100.127.0.3", "port": 5432,
      "target": "localhost:5432", "networkName": "Office" }
  ]
}
```

Agents also get a snapshot in their environment at boot:

| Variable | Value |
| --- | --- |
| `MARITIME_NETWORK_URL` | http://100.127.254.1/network |
| `MARITIME_INTERNET` | The internet mode: `all`, `none`, or `list`. |
| `MARITIME_EXIT_IP` | The exit address, when known at boot. |
| `MARITIME_ACCESS_<NAME>` | One per private resource: the name in upper case with other characters replaced by `_`. The value is the address to connect to. |

## Limits

- At most 100 allow entries and 50 deny entries per workload or preset. Deny wins over allow.
- Under `list` and `none`, a connection to a destination not on the list is reset at once. Name
  lookups still work, so a program sees a refused connection, not a hanging one. Activity records the
  destination with outcome `blocked`.
- `fixed` and `country` need a verified email and a paid plan: an active Starter, Growth or Scale
  plan, not the free trial, and not while a payment is failing. `country` is off for accounts that
  declared patient data. `through` needs one of your networks that allows internet. `country` also
  needs Maritime's residential provider or your own proxy (`ownProxyUrl` in account settings). The read tells
  you which, before you try.
- The account settings can turn `fixed`, `country` or `through` off; workloads that use a mode
  turned off leave from the shared address.
- A fixed address is requested when the first workload in a region asks for it and shows as
  `requested` for a short while. It is released when the last workload in that region leaves
  `fixed`.
