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

# Configure the skill store (S3 or minio)

> Connect an object store once so model-agnostic skills load their bodies on demand. Credentials are entered by an admin and encrypted at rest.

Model-agnostic skills keep a token-cheap stub (a name and a one-line description) in every request and load the full body only when a request matches. The bodies live in an object store you connect once. Any S3-compatible store works: AWS S3 in the cloud, or the minio container that ships with the stack for local use

The store credentials are entered a single time by an admin on the Skills page. The secret is encrypted before it touches the database and is never returned by any read; the gateway is the only component that decrypts it, and it does so server-side to fetch a skill body. End users never see or enter a key, and a skill row itself carries only a `content_url`, never a credential

<Info>
  You configure the store once. Every `s3://` skill reuses that one connection. Which users may see which skill is controlled separately by access groups, not by handing out keys
</Info>

## Option A: AWS S3

Use this for a real cloud bucket shared across environments

The walkthrough below uses a bucket named `forceai-skills` in `us-east-1` holding one object, `pdf.md`. Substitute your own names as you go

<Steps>
  <Step title="Create the bucket and upload a skill file">
    In the AWS console go to **S3 -> Create bucket**. Give it a name such as `forceai-skills` and pick a region such as `us-east-1`. Leave "Block all public access" on; the gateway reaches objects with credentials, never public URLs. Upload your skill body as an object, for example `pdf.md`, so its key is `forceai-skills/pdf.md`
  </Step>

  <Step title="Create a scoped IAM access key">
    This is the one value the ForceAI form cannot generate for you; it comes from AWS. In **IAM -> Users -> Create user** add a user such as `forceai-skills-loader` with no console access, then attach an inline policy scoped to just this bucket:

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Action": ["s3:GetObject", "s3:PutObject"],
        "Resource": "arn:aws:s3:::forceai-skills/*"
      }]
    }
    ```

    `s3:GetObject` is required so the gateway can load a body on demand. `s3:PutObject` is only needed if you will use the in-dashboard "Upload body" mode; drop it if you always upload objects yourself. Then under **Security credentials -> Create access key** copy the Access key ID (starts with `AKIA`) and the Secret access key. The secret is shown once, so copy it now
  </Step>

  <Step title="Open the store panel in ForceAI">
    Sign in to the dashboard at `http://localhost:3000` (default `admin` / your `LITELLM_MASTER_KEY`). In the left navigation open **Skills**, then select the **Model-agnostic skills** tab. The store lives in the single row under the "Object store (S3-compatible)" heading
  </Step>

  <Step title="Fill the store row field by field">
    The row is left to right: a provider dropdown, then four text boxes, then the save button. Set each one:

    | # | Field             | Value                | Note                                     |
    | - | ----------------- | -------------------- | ---------------------------------------- |
    | 1 | Provider          | `AWS S3`             | dropdown, second option                  |
    | 2 | Bucket            | `forceai-skills`     | bucket name only, no `s3://` and no path |
    | 3 | Endpoint URL      | leave blank          | AWS default; only minio needs this       |
    | 4 | Access key id     | your `AKIA...` value | from step 2                              |
    | 5 | Secret access key | your secret          | write-only; masked as you type           |

    The region does not have a field in this row and defaults correctly for `us-east-1`. For any other region use the [API form](#configure-the-store-by-api), which takes `region` explicitly
  </Step>

  <Step title="Save">
    Click **Save store**. The tag at the end of the row flips from `not configured` to `configured`, and field 5 changes to show "secret set (leave blank to keep)". The secret is encrypted at rest and is never returned again; on a later edit you can leave field 5 blank to keep the stored one
  </Step>
</Steps>

<Warning>
  For AWS S3 leave the Endpoint URL (field 3) blank. Pointing it at a minio-style URL is the most common misconfiguration and makes every load fail
</Warning>

## Option B: local minio

The stack ships a minio container on port `9000` with its console on `9001`, so you can prove the whole flow with no cloud account

<Steps>
  <Step title="Create the bucket in minio">
    Open the minio console at `http://localhost:9001` and sign in with the dev credentials `forceai` / `forceai-minio-dev` (or your `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` overrides). Go to **Buckets -> Create Bucket** and add `skills`
  </Step>

  <Step title="Pick credentials">
    Reuse the root credentials above, or create a dedicated pair under **Access Keys -> Create** for least privilege
  </Step>

  <Step title="Enter the connection in ForceAI">
    On the **Skills** page **Model-agnostic Skills** tab set:

    | Field             | Value               |
    | ----------------- | ------------------- |
    | Provider          | `minio`             |
    | Bucket            | `skills`            |
    | Endpoint URL      | `http://minio:9000` |
    | Access key id     | `forceai`           |
    | Secret access key | `forceai-minio-dev` |

    Click **Save**
  </Step>
</Steps>

<Warning>
  The endpoint must be `http://minio:9000`, using the container name `minio`, not `localhost`. The gateway resolves it on the internal docker network, where `localhost` would point at the gateway itself
</Warning>

## Add a skill

With the store showing `configured`, click **+ Add skill** on the same panel. A dialog opens with a mode toggle at the top and four fields below

<Steps>
  <Step title="Pick the source mode">
    Leave the toggle on **Reference URL** to point at an object that already exists in the bucket (this is the common case). Switch to **Upload body** only when you want to paste the `SKILL.md` text and have ForceAI write it to the bucket for you; that mode needs the `s3:PutObject` permission from step 2 above
  </Step>

  <Step title="Fill the fields">
    For the `forceai-skills/pdf.md` object from the walkthrough:

    | Field       | Value                                        | Note                                           |
    | ----------- | -------------------------------------------- | ---------------------------------------------- |
    | name        | `pdf`                                        | unique across skills                           |
    | content URL | `s3://forceai-skills/pdf.md`                 | the `s3://bucket/key` form, not the https link |
    | description | `Extract text and tables from PDF documents` | the cheap stub the selector matches on         |
    | keywords    | `pdf, extract, tables, document`             | comma separated                                |
  </Step>

  <Step title="Save">
    Click **OK**. The skill appears in the table below with its Enabled switch already on. Leave it enabled
  </Step>
</Steps>

<Warning>
  Enter the content URL as `s3://forceai-skills/pdf.md`, not as `https://forceai-skills.s3.us-east-1.amazonaws.com/pdf.md`. The `s3://` form keeps the bucket private and lets the gateway authenticate with your stored key. The `https://` form only works if you make the object publicly readable, which defeats the point of the access key
</Warning>

## Verify a skill loads

Enable skills on a request and confirm the body is injected for that turn:

```bash theme={null}
curl -s http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $FORCEAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-haiku-4-5",
    "metadata": {"forceai_skills": true},
    "messages": [{"role": "user", "content": "help me extract tables from a PDF"}]
  }' | jq '.usage, .choices[0].message.content'
```

Open the request in **Logs**, expand the Raw view, and look at `spend_logs_metadata.forceai_skills`. It records `available`, `loaded`, `loaded_names`, and `tokens_loaded`, so you can confirm the matching skill was fetched from the store and injected while the non-matching skills were never loaded

## Configure the store by API

The dashboard form is a thin wrapper over one admin-only endpoint on the control plane. The secret is write-only: it is accepted here and never returned by the matching `GET`

```bash theme={null}
curl -X PUT http://localhost:4001/forceai/skills/store \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "s3",
    "bucket": "forceai-skills",
    "access_key_id": "AKIA...",
    "secret_access_key": "...",
    "region": "us-east-1"
  }'
```

For minio, set `"provider": "minio"` and add `"endpoint_url": "http://minio:9000"`. On an update you may omit `secret_access_key` to keep the stored one

<Info>
  `GET /forceai/skills/store` returns the provider, bucket, endpoint, region, and a `secret_set` boolean only. It never returns the secret itself
</Info>
