Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions examples/forcefield/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# --- ForceField gateway URL ---------------------------------------------
# Local docker: http://host.docker.internal:8080
# Same network: http://forcefield-edge-proxy:8080
# Hosted (GCP): https://forcefield-gateway-546798516374.northamerica-northeast1.run.app
FORCEFIELD_BASE_URL=http://host.docker.internal:8080

# --- ForceField tenant key ----------------------------------------------
# Issued by your ForceField tenant (POST /v1/onboard on the tenant manager,
# or your dashboard). Lacuna sends it upstream as `x-api-key` to
# authenticate the deployment as a ForceField principal.
FORCEFIELD_API_KEY=ffgw-replace-me

# --- Upstream provider keys (BYOK) --------------------------------------
# Lacuna injects these as `Authorization: Bearer <key>`. The ForceField
# gateway strips its own auth headers and forwards the bearer straight
# through to the real provider (api.openai.com / api.anthropic.com).
# ForceField never stores or bills for these keys.
OPENAI_API_KEY=sk-your-openai-key-here
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here

# --- Optional: Tailscale trust mode -------------------------------------
# Only relevant if you self-host ForceField inside the same tailnet as
# Lacuna. The hosted Cloud Run gateway cannot use Tailscale trust because
# Lacuna's egress IP is not in the tailnet CGNAT range -- for SaaS use the
# `x-api-key` path above. Set these on the ForceField deployment, NOT on
# Lacuna:
# TAILSCALE_TRUST_ENABLED=true
# TAILSCALE_IDENTITY_HEADER=Tailscale-User-Login
# TAILSCALE_TRUSTED_PROXY_CIDRS=100.64.0.0/10 # tailnet CGNAT range
155 changes: 155 additions & 0 deletions examples/forcefield/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Lacuna + ForceField

This example shows how to chain Lacuna with the
[ForceField LLM Security Gateway](https://github.com/forcefield-ai/force_field_llm_security_gateway)
to combine Lacuna's tailnet-native access control with ForceField's
prompt-injection / PII / secret-leak detection.

## Why chain them

Lacuna and ForceField solve different problems and sit cleanly at different
layers of the request path:

| Layer | Concern | Tool |
|------------------|----------------------------------------------------------|------------|
| Identity / ACL | Which tailnet user may call which provider/model | Lacuna |
| Content security | Is this prompt a jailbreak? Does the response leak PII? | ForceField |
| Provider routing | Where does the request actually go (OpenAI/Anthropic/.) | Lacuna |

Composing the two lets you keep Lacuna's zero-config Tailscale identity model
while gaining detection, redaction, and audit capabilities that Lacuna
intentionally does not implement.

## Request flow

```
tailnet user
|
v
+---------+ Tailscale-User-Login x-api-key: <ff-key>
| Lacuna | --------------------------------+ Authorization: Bearer <provider-key>
+---------+ | POST /v1/chat/completions
| (capability check, routing, | POST /v1/messages
| per-user metrics) v
| +-------------+
| | ForceField |
| | gateway |
| +-------------+
| | (auth via x-api-key,
| | scan + redact, then strip
| | FF auth and forward the
| | bearer untouched)
| v
| +---------+
+------------------------------> | OpenAI |
| / Anthropic
+---------+
```

Lacuna is configured with ForceField as a custom OpenAI-compatible upstream
*and* an Anthropic-native upstream. For both providers Lacuna sends two
independent headers:

- `x-api-key: ${FORCEFIELD_API_KEY}` -- authenticates the deployment to FF.
- `Authorization: Bearer ${OPENAI_API_KEY}` (or `${ANTHROPIC_API_KEY}`) --
the user-owned upstream key. ForceField's `/v1/chat/completions` and
`/v1/messages` handlers strip their own auth headers and forward the
bearer straight through to api.openai.com / api.anthropic.com.

The provider bill goes to whoever owns the upstream key, not to ForceField.

## Files

| File | Purpose |
|------------------------------|------------------------------------------------------|
| `lacuna-config.json` | Lacuna config: ForceField as OpenAI-compat provider |
| `docker-compose.yml` | Runs Lacuna locally; expects ForceField on the host |
| `.env.example` | Environment variables (copy to `.env`) |
| `claude_code.settings.json` | Claude Code config pointing Anthropic SDK at Lacuna |
| `smoke_test.py` | End-to-end test: benign prompt + jailbreak attempt |
| `gcp-cloud-run/` | Variant pointing Lacuna at a hosted ForceField |

## Quick start (local)

1. Run ForceField locally, following the
[ForceField README](https://github.com/forcefield-ai/force_field_llm_security_gateway#quick-start).
The edge-proxy must be reachable at `http://host.docker.internal:8080`
(or change `lacuna-config.json`).

2. Provision a ForceField API key for your tenant (POST `/v1/onboard` on
the tenant manager, or use the dashboard) and put it -- plus your real
upstream provider keys -- in `.env`:

```
cp .env.example .env
# edit .env: FORCEFIELD_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
```

3. Start Lacuna:

```
docker compose up
```

4. Send a request through the chain:

```
./smoke_test.py
```

The benign prompt should return a model completion. The jailbreak prompt
should be blocked by ForceField with a non-2xx response or a refusal
message in the `forcefield_metadata` field.

## Quick start (hosted ForceField on GCP Cloud Run)

See [gcp-cloud-run/README.md](gcp-cloud-run/README.md). The only difference
is the `baseurl` in the Lacuna config and that the ForceField key lives in
your secrets store of choice.

## Notes & caveats

- **OpenAI and Anthropic are both BYOK.** ForceField's gateway exposes
`/v1/chat/completions` (OpenAI-compatible, BYOK by default) and
`/v1/messages` (Anthropic-native passthrough with SSE streaming and
inline PII redaction). The Lacuna config in this example declares both
providers and routes by URL prefix: `/forcefield-openai/v1/chat/...`
and `/forcefield-anthropic/v1/messages`.
- **ForceField never holds your provider key.** Lacuna sends
`Authorization: Bearer ${OPENAI_API_KEY}` (or `${ANTHROPIC_API_KEY}`)
alongside `x-api-key: ${FORCEFIELD_API_KEY}`. The gateway reads its own
key off `x-api-key`, strips the FF auth headers, and forwards the bearer
untouched to api.openai.com / api.anthropic.com. The provider bill goes
to the key owner, not to ForceField. ForceField's security pipeline
(detectors, PII redaction, output moderation) runs on local models -- no
external LLM cost.
- Bedrock and Gemini ingress on ForceField are not yet implemented.
- **Two-header auth, not one.** Lacuna 0.20.x sets exactly one auth header
per provider via `authorization`. To send both the FF tenant key *and*
the upstream provider key, this example uses `authorization: bearer`
with `apikey: ${PROVIDER_KEY}` and pins
`headers: { "x-api-key": "${FORCEFIELD_API_KEY}" }` as a static header.
- **Model-glob enforcement is provider-dependent.** Lacuna only extracts
the request `model` for Anthropic / Bedrock / Gemini. The OpenAI
chat-completion and responses handlers do not inspect the body, so a
`models: ["gpt-*"]` capability would deny every request. This example
uses `models: ["*"]` for the OpenAI provider; rely on Lacuna's
`capabilities_header` (Tailscale grants) or downstream FF policy to
restrict OpenAI models.
- **Capabilities header is opt-in here.** The default config does not set
`capabilities_header`. Lacuna treats a missing capability header as
`deny_all`, so leaving it on would block every request from clients
that aren't passing through Tailscale grants. Re-enable it (and pass
`Tailscale-App-Capability`) once you are running Lacuna inside a real
tailnet.
- **User attribution.** Lacuna injects `Tailscale-User-Login` upstream
when `identity_header` is set. ForceField records it in its audit trail.
- **Tailscale trust mode is self-host only.** The hosted Cloud Run gateway
cannot use it because Lacuna's egress IP to Cloud Run is not in the
tailnet CGNAT range. Use the `x-api-key` path for SaaS deployments;
Tailscale trust is for ForceField instances co-located with Lacuna in
the tailnet.
- **Defence in depth.** Lacuna's capability filter runs *first*, so
requests blocked at the ACL layer never hit ForceField -- saving
inference cost. Requests that pass Lacuna are then scanned by FF's
detectors and postprocessor.
7 changes: 7 additions & 0 deletions examples/forcefield/claude_code.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"apiKeyHelper": "echo '-'",
"env": {
"ANTHROPIC_BASE_URL": "https://lacuna.flare.ts.net/forcefield-anthropic/",
"ANTHROPIC_AUTH_TOKEN": "-"
}
}
35 changes: 35 additions & 0 deletions examples/forcefield/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
lacuna:
image: ghcr.io/flared/lacuna:latest
container_name: lacuna
command:
- "--config"
- "/opt/lacuna-config.json"
- "--host"
- "0.0.0.0"
- "--port"
- "8080"
ports:
- "${LACUNA_HOST_PORT:-8080}:8080"
environment:
# ForceField gateway reachable from inside the container.
# On Docker Desktop (macOS/Windows), host.docker.internal works.
# On Linux, replace with the host's LAN IP or the FF container name
# if you are running everything on the same docker network.
- FORCEFIELD_BASE_URL=${FORCEFIELD_BASE_URL:-http://host.docker.internal:8080}
- FORCEFIELD_API_KEY=${FORCEFIELD_API_KEY:?set FORCEFIELD_API_KEY in .env}
# Upstream provider keys (BYOK). Lacuna injects these as
# `Authorization: Bearer <key>` and FF forwards them straight to
# api.openai.com / api.anthropic.com.
- OPENAI_API_KEY=${OPENAI_API_KEY:?set OPENAI_API_KEY in .env}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:?set ANTHROPIC_API_KEY in .env}
volumes:
- ./lacuna-config.json:/opt/lacuna-config.json:ro
extra_hosts:
# Allow host.docker.internal on Linux too.
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 3s
retries: 5
62 changes: 62 additions & 0 deletions examples/forcefield/gcp-cloud-run/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Lacuna + hosted ForceField (GCP Cloud Run)

Variant of the parent example that points Lacuna at a ForceField gateway
deployed on Google Cloud Run.

The reference instance is at:

```
https://forcefield-gateway-546798516374.northamerica-northeast1.run.app
```

It exposes both `/v1/chat/completions` (OpenAI-compatible BYOK) and
`/v1/messages` (Anthropic-native BYOK with SSE streaming and inline PII
redaction).

## Setup

1. Get a ForceField tenant key. Either ask Force Field to provision one
for you, or POST to the tenant manager directly:

```sh
curl -X POST https://forcefield-tenant-manager-dhowwtjefa-nn.a.run.app/v1/onboard \
-H 'content-type: application/json' \
-d '{"name":"your-team","contact_email":"you@example.com","plan":"professional"}'
```

The response includes a `raw_key` that starts with `ffgw_...` -- that
is your `FORCEFIELD_API_KEY`.

2. Set environment variables before launching Lacuna:

```sh
export FORCEFIELD_BASE_URL=https://forcefield-gateway-546798516374.northamerica-northeast1.run.app
export FORCEFIELD_API_KEY=ffgw_...
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
```

3. Start Lacuna with this directory's config:

```sh
lacuna --config gcp-cloud-run/lacuna-config.json
```

Or via Docker, mounting the config and forwarding the env vars.

## Cloud Run notes

- Cloud Run terminates TLS for you -- the `baseurl` must be `https://`.
- Cloud Run idle-scales to zero. The first request after a cold period
incurs the gateway's startup latency (~1-3s for the FastAPI app, longer
if detector models cold-start). Set `--min-instances=1` on the gateway
service if cold starts are unacceptable.
- IAM auth is independent from FF tenant auth. The reference instance
above is publicly reachable; the `x-api-key` header is the only auth
layer between you and your tenant. If your gateway requires GCP IAM,
put it behind an internal load balancer and run Lacuna in the same
VPC connector with a service-account identity token.
- **Tailscale trust mode is not viable here.** Lacuna's egress IP to
Cloud Run is not in the tailnet CGNAT range, so `TAILSCALE_TRUST_ENABLED`
on the gateway will reject every request. Tailscale trust is for
self-hosted ForceField sitting inside the same tailnet as Lacuna.
41 changes: 41 additions & 0 deletions examples/forcefield/gcp-cloud-run/lacuna-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"lacuna": {
"logging": {
"format": "json",
"level": "info"
},
"identity_header": "Tailscale-User-Login"
},
"providers": {
"forcefield-openai": {
"name": "OpenAI via ForceField (BYOK, Cloud Run)",
"baseurl": "${FORCEFIELD_BASE_URL}",
"authorization": "bearer",
"apikey": "${OPENAI_API_KEY}",
"headers": {
"x-api-key": "${FORCEFIELD_API_KEY}"
},
"capability": {
"models": ["*"]
},
"compatibility": {
"openai_chat": true
}
},
"forcefield-anthropic": {
"name": "Anthropic via ForceField (BYOK, Cloud Run)",
"baseurl": "${FORCEFIELD_BASE_URL}",
"authorization": "bearer",
"apikey": "${ANTHROPIC_API_KEY}",
"headers": {
"x-api-key": "${FORCEFIELD_API_KEY}"
},
"capability": {
"models": ["claude-*"]
},
"compatibility": {
"anthropic_messages": true
}
}
}
}
41 changes: 41 additions & 0 deletions examples/forcefield/lacuna-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"lacuna": {
"logging": {
"format": "json",
"level": "info"
},
"identity_header": "Tailscale-User-Login"
},
"providers": {
"forcefield-openai": {
"name": "OpenAI via ForceField (BYOK)",
"baseurl": "${FORCEFIELD_BASE_URL}",
"authorization": "bearer",
"apikey": "${OPENAI_API_KEY}",
"headers": {
"x-api-key": "${FORCEFIELD_API_KEY}"
},
"capability": {
"models": ["*"]
},
"compatibility": {
"openai_chat": true
}
},
"forcefield-anthropic": {
"name": "Anthropic via ForceField (BYOK)",
"baseurl": "${FORCEFIELD_BASE_URL}",
"authorization": "bearer",
"apikey": "${ANTHROPIC_API_KEY}",
"headers": {
"x-api-key": "${FORCEFIELD_API_KEY}"
},
"capability": {
"models": ["claude-*"]
},
"compatibility": {
"anthropic_messages": true
}
}
}
}
Loading