> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sailresearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Credential injection

> Let a Sailbox use an API key it can never read

Credential injection puts secrets on the network instead of in the Sailbox.
You store an API key with Sail, write a policy that says which HTTPS host gets
it, and attach the policy to a Sailbox. Code inside sends a normal request with
no credential, and Sail adds it on the way out. The Sailbox, and any agent
running on it, can use the API but can never read the key.

## Try it

This stores a secret, injects it as a bearer token on requests to
`httpbin.org`, and asks httpbin to echo the request back:

Save the policy as `demo.json`:

```json demo.json theme={null}
{
  "httpbin.org": {
    "rules": [
      {
        "request": {
          "set": {
            "headers": {
              "authorization": "Bearer ${secrets.DEMO_TOKEN}"
            }
          }
        }
      }
    ]
  }
}
```

Then store the secret, create and attach the policy, and make a request from
inside the Sailbox:

<div className="sail-prompt-shell">
  ```bash theme={null}
  DEMO_TOKEN=me sail secret set DEMO_TOKEN --from-env DEMO_TOKEN
  sail http-policy create demo --file demo.json
  sail box http-policy set <sailbox-id> <policy-id>
  sail box exec <sailbox-id> -- curl -s https://httpbin.org/anything -H foo:bar
  ```
</div>

```json Output theme={null}
{
  "headers": {
    "Accept": "*/*",
    "Authorization": "Bearer me",
    "Foo": "bar",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.88.1"
  },
  "method": "GET",
  "url": "https://httpbin.org/anything"
}
```

The `curl` inside the Sailbox never saw the token. Only the request that
reached httpbin carried it.

The same flow from the SDKs, with a GitHub token:

<CodeGroup>
  ```python Python theme={null}
  import os

  import sail

  sail.Secret.set("GITHUB_TOKEN", os.environ["GITHUB_TOKEN"])

  policy = sail.HttpPolicy.create(
      "github",
      {
          "api.github.com": {
              "rules": [
                  {
                      "request": {
                          "set": {
                              "headers": {
                                  "authorization": "Bearer ${secrets.GITHUB_TOKEN}",
                              },
                          },
                      },
                  },
              ],
          },
      },
  )

  sailbox.set_http_policy(policy)
  ```

  ```typescript TypeScript theme={null}
  import { HttpPolicy, Secret } from "@sailresearch/sdk";

  await Secret.set("GITHUB_TOKEN", process.env.GITHUB_TOKEN!);

  const policy = await HttpPolicy.create("github", {
    "api.github.com": {
      rules: [
        {
          request: {
            set: {
              headers: {
                authorization: "Bearer ${secrets.GITHUB_TOKEN}",
              },
            },
          },
        },
      ],
    },
  });

  await sailbox.setHttpPolicy(policy);
  ```

  ```rust Rust theme={null}
  use sail::Client;
  use serde_json::json;

  let client = Client::from_env()?;

  client
      .set_secret("GITHUB_TOKEN", &std::env::var("GITHUB_TOKEN")?)
      .await?;

  let policy = client
      .create_http_policy(
          "github",
          &json!({
              "api.github.com": {
                  "rules": [{
                      "request": {
                          "set": {
                              "headers": {
                                  "authorization": "Bearer ${secrets.GITHUB_TOKEN}",
                              },
                          },
                      },
                  }],
              },
          }),
      )
      .await?;

  sailbox.set_http_policy(&policy).await?;
  ```
</CodeGroup>

## Secrets

A secret is a named value that belongs to your organization.

<div className="sail-prompt-shell">
  ```bash theme={null}
  sail secret set OPENAI_API_KEY                       # type the value at a hidden prompt
  sail secret set OPENAI_API_KEY --from-env OPENAI_API_KEY
  op read "op://vault/openai/key" | sail secret set OPENAI_API_KEY   # or pipe it in
  sail secret list
  sail secret delete OPENAI_API_KEY
  ```
</div>

Setting a name that already exists replaces its value. The next matching
request from any Sailbox whose policy uses it gets the new value. Names start
with a letter or digit and may contain letters, digits, `_`, and `-`, up to
128 characters. A value is one non-empty line of text up to 64 KiB, with no tabs, line
breaks, or other control characters.

## Policies

A policy is a JSON document that belongs to your organization. Each top-level
key is a host, and each host has an ordered list of rules. Sail picks the most
specific host entry for a request, then the first rule under it that matches,
and applies that rule. If nothing matches, the request goes out unchanged.

```json theme={null}
{
  "api.example.com": {
    "rules": [
      {
        "match": { "method": "POST", "path": { "prefix": "/v1/" } },
        "request": {
          "set": {
            "headers": { "authorization": "Bearer ${secrets.EXAMPLE_KEY}" }
          }
        }
      },
      {
        "request": { "set": { "headers": { "x-api-version": "2" } } }
      }
    ]
  }
}
```

**Hosts.** Write a bare hostname: no `https://`, port, or path.
`api.example.com` matches exactly that host. `*.example.com` matches any
direct subdomain, and `*` matches everything not named elsewhere. Use an exact
host whenever a rule adds a credential: a wildcard sends the credential to
every host it matches.

**Match.** Leave `match` out to cover every request to the host; such a rule
must be last in its list. Otherwise narrow by `method` (upper case, one or a
list), `path`, `headers`, or `query`. Values accept a plain string for an
exact match, `{"prefix": "..."}`, or `{"one_of": [...]}`; a header or query
condition can also assert `"present": false`.

**What a rule does.**

| Key       | What it does                                                                                                                             |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `request` | `set`, `add`, or `remove` headers and query parameters, or `set` the path. `${secrets.NAME}` works inside `set.headers` and `set.query`. |
| `forward` | Send the request to another HTTPS `host` (and optional `port`). Combine with `request` to add a credential for that host.                |
| `respond` | Answer with `status`, `headers`, and `body` without contacting the destination. Cannot be combined with the other two.                   |

<Accordion title="Policy document details">
  * A `forward` host must be exact. If Sail cannot reach it, the request fails;
    it is not sent to the original host instead.
  * `respond` stops that HTTP request only. It is not a network access control;
    other traffic to the same host may still be possible.
  * `request.set.headers` and `request.set.query` are templates: every `$` in
    them must be part of `${secrets.NAME}` or `$$` (a literal dollar sign). In
    every other string `$` is plain text, except that an unescaped `${` is
    rejected because it looks like an unresolved reference.
  * The secret a policy names must exist before the policy is created.
  * An invalid document fails at create time with an error naming the part to
    fix. Sail stores a normalized form, so reading a policy back can return
    lowercased hosts and filled-in defaults; behavior is the same.
  * A policy's rules cannot change after creation. Create and attach a new one.
    Renaming is allowed.
</Accordion>

## Attaching

A Sailbox holds at most one policy, and one policy can serve many Sailboxes.

<div className="sail-prompt-shell">
  ```bash theme={null}
  sail box http-policy set <sailbox-id> <policy-id>   # attach, replacing any current policy
  sail box http-policy show <sailbox-id>
  sail box http-policy clear <sailbox-id>
  ```
</div>

A set or clear applies to HTTPS connections the Sailbox opens after the call
succeeds. A connection that is already open keeps the previous policy until it
closes, so close long-lived connections if the change must apply to the next
request.

## Where secrets live

Secrets are stored by Sail and added at the network edge as the request leaves
the Sailbox. Nothing inside the Sailbox ever holds the value, and no Sail API
returns it: `sail secret show` and `sail secret list` print names and
timestamps only.

<div className="sail-prompt-shell">
  ```bash theme={null}
  sail secret show DEMO_TOKEN
  ```
</div>

```text Output theme={null}
name:        DEMO_TOKEN
created_at:  2026-09-03T01:55:06.18426Z
updated_at:  2026-09-03T01:55:06.18426Z
```

To remove a secret, clear or replace the policy on every Sailbox that uses it,
delete every policy that names it, then delete the secret. Sail refuses the
other orders. `sail http-policy list` shows how many Sailboxes use each policy
and which secrets it names.

## Limitations

* Policies apply to HTTPS only. Plain HTTP and raw TCP are unchanged.
* A policy applies only when the client announces its HTTP version during the
  TLS handshake (ALPN), which almost every client does. For one that does
  not, add `"missing_alpn": "http/1.1"` next to `rules` on an exact host entry
  to treat its connections as HTTP/1.1.
* Request bodies cannot be matched or changed, and responses cannot be
  changed.
