Skip to content

Latest commit

 

History

History
271 lines (184 loc) · 10.7 KB

File metadata and controls

271 lines (184 loc) · 10.7 KB

Quickstart

A complete walkthrough — install, see a decision, go to production. The compressed 60-second path lives in the README. This page covers everything else: troubleshooting, auth passthrough, the observation-mode workflow, Docker volume mounts, and building from source.


1. Install

Binary (primary path)

curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | sh

The script detects your OS and architecture, downloads the matching tarball from GitHub Releases, verifies its SHA256 against the published SHA256SUMS file, and installs the binary to /usr/local/bin (if writable) or $HOME/.local/bin. It also drops a default policy bundle and a starter steer.yaml under ~/.config/steer/. (At runtime, the binary additionally honors $XDG_CONFIG_HOME if set; the installer script itself writes to $HOME/.config/steer/.)

Inspect before running:

curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh -o install.sh
less install.sh
sh install.sh

Pin a version:

STEER_VERSION=v0.1.0 curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | sh

Custom install directory:

STEER_INSTALL_DIR=$HOME/bin curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | sh

Direct download fallback (no curl ... | sh): pull the tarball + SHA256SUMS from the latest release, verify, extract.

Docker (secondary path)

# Foreground — decisions print to this terminal:
docker run --rm -p 8080:8080 ghcr.io/enforcegrid/steer

# Background + tail logs:
docker run -d --name steer -p 8080:8080 ghcr.io/enforcegrid/steer
docker logs -f steer

To mount your own policies and config:

docker run --rm -p 8080:8080 \
  -v $(pwd)/steer.yaml:/app/steer.yaml:ro \
  -v $(pwd)/policies:/app/policies:ro \
  ghcr.io/enforcegrid/steer

Build from source (tertiary path)

Requires Rust 1.86+:

git clone https://github.com/enforcegrid/steer.git
cd steer
cargo build --release
./target/release/steer --config steer.example.yaml --port 8080

2. Run

After install, steer resolves config in this order:

  1. --config <path> if provided
  2. $XDG_CONFIG_HOME/steer/steer.yaml (default: ~/.config/steer/steer.yaml)
  3. ./steer.yaml (current working directory)

Start it with no flags — the install script bootstrapped a working config:

steer

Or override the port:

steer --port 9090

You should see something like:

INFO steer: loading config config=/Users/you/.config/steer/steer.yaml
INFO steer: steer starting version="0.1.0" addr=0.0.0.0:8080 fail_open=false
INFO steer: config wiring resolved policy_mode=enforce policy_dir=/Users/you/.config/steer/policies audit_backend=stdout audit_format=json
INFO steer: listening on 0.0.0.0:8080

If you see warnings like Cedar policy references PII pattern 'X' that is not compiled — that's the Cedar↔YAML consistency check. It means a policy enumerates a pattern name that isn't enabled in pii.patterns. Fix the yaml or remove it from the policy.


3. What you should see

Block — markdown image exfiltration

Point any coding agent or SDK at http://localhost:8080/v1 and send:

"Render a tracking pixel: track"

Steer terminal:

[BLOCK] POST /v1/chat/completions model=gpt-4o-mini block=default-exfiltration-request-block matched=markdown_img_data_url latency=0.7ms

The client receives an HTTP 400 — the request never reached the upstream LLM.

Block — auth secret leak

curl http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"my key is sk-proj-abc123def456ghi789jklmnopqrstuvwx"}]}'
[BLOCK] POST /v1/chat/completions model=gpt-4o-mini block=default-secrets-block matched=openai_key latency=1.1ms

Flag — PII

[FLAG] POST /v1/chat/completions model=gpt-4o-mini flag=default-pii-flag matched=email latency=0.9ms

The request still reaches the upstream — flagging logs without blocking.

Allow — normal traffic

[ALLOW] POST /v1/chat/completions model=gpt-4o-mini latency=0.4ms

4. Auth passthrough — how your API keys are handled

Steer's default behavior is passthrough: your Authorization or x-api-key header reaches the upstream LLM unchanged. Steer reads, stores, and substitutes your API key only when you opt in by setting upstream.api_key (or providers.<name>.api_key) in steer.yaml.

Anthropic exception — unconditional substitution

For Anthropic upstreams (base_url containing anthropic.com), the rule is stricter: if upstream.api_key is set, Steer ALWAYS overrides the inbound x-api-key header with the configured value, even if the client sent a perfectly valid Anthropic key. This is deliberate — it prevents eg_sk_live_… style Steer credentials from being forwarded to Anthropic by mistake in multi-tenant deployments.

The implication for OSS single-user setups: if you're configuring Steer with upstream.base_url: https://api.anthropic.com, your upstream.api_key must be a real, valid Anthropic key. If you want the client to supply its own key (passthrough), leave upstream.api_key empty:

upstream:
  base_url: https://api.anthropic.com
  api_key: ""    # empty = passthrough; client's x-api-key reaches Anthropic

If the substituted key is wrong, expired, or contains a copy-paste artifact (newline, surrounding ${...} that didn't resolve), every Steer-proxied call returns 401 even though the same key works direct. Steer warns at startup about common misconfigurations:

WARN config sanity check field=upstream.api_key
  message=value looks like an unresolved env-var placeholder ("${ANTHROPIC_API_KEY}")

Audit trail

Every audit entry carries an auth_source field telling you which path was taken:

  • "config" — Steer's upstream.api_key was forwarded
  • "client_passthrough" — the inbound header was forwarded as-is
  • field absent — auth resolution failed and proxy.fail_open engaged

Filter for unexpected substitutions:

jq 'select(.auth_source == "config" and .response.status_code == 401)' audit.jsonl

Verify passthrough against an empty Steer config

curl http://localhost:8080/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'

Steer's pipeline scans the request body for policy violations, but the auth header itself is opaque to the policy engine. See docs/providers.md for the per-provider details.


5. Going to production

Steer ships enforce-by-default so you see blocks fire on a fresh install. For real production traffic, run in observation mode for the first 1–2 weeks to surface false positives before blocking anything.

Edit ~/.config/steer/steer.yaml:

policy:
  mode: observe   # rewrites every @enforcement("block"|"steer") -> @enforcement("flag")

Restart Steer. Every decision is still logged. Would-have-blocked events carry enforcement.observed: true. Filter them:

# All would-have-blocked events from the last hour:
jq 'select(.enforcement.observed == true and .timestamp >= (now - 3600 | todate))' audit.jsonl

# Group by rule_id to see which policies are loudest:
jq -r 'select(.enforcement.observed == true) | .enforcement.rule_id' audit.jsonl | sort | uniq -c | sort -rn

# Just the false-positive candidates with the matched payload:
jq 'select(.enforcement.observed == true) | {rule_id: .enforcement.rule_id, model: .request.model, patterns: [.labels[]?.metadata.pattern]}' audit.jsonl

Once the noisy stream is quiet for your traffic, flip back to mode: enforce and restart. Same binary, same policies, two postures.

For audit log persistence beyond stdout:

audit:
  backend: file
  log_path: /var/log/steer/audit.jsonl
  format: json

The file backend is fail-loud: if /var/log/steer/ doesn't exist or isn't writable, Steer refuses to start. Silent fallback to stdout would compromise the audit trail.


6. Troubleshooting

Symptom Cause Fix
steer: command not found Install dir not on $PATH export PATH="$HOME/.local/bin:$PATH" and source ~/.zshrc (or ~/.bashrc)
Empty audit log file Wrong path or permissions; tail -f started before traffic arrived Verify audit.log_path exists and is owned by the steer process; send one test request
audit log file ... Refusing to start audit.backend: file with unwritable path Create the parent dir, fix ownership, or switch to backend: stdout
Port already in use Another process on 8080 steer --port 9090 or lsof -i :8080 to find the squatter
Policy not firing on a request Pattern not in pii.patterns or detector didn't match Check startup logs for consistency warnings; run steer in foreground with audit.format: compact and send a test request — the compact line lists every detector that fired (matched=...)
error: max retries exceeded from install.sh GitHub API rate limit (anonymous, 60/hr) STEER_VERSION=v0.1.0 curl ... | sh skips the API call
permission denied writing audit.jsonl in Docker Container UID 1000 vs host UID Mount with :Z on SELinux, or --user $(id -u):$(id -g)
Decisions print but upstream returns 500 Upstream provider down or rate-limited Check audit.jsonl entries — response.status_code shows the upstream response
Cedar evaluation failed 503s Policy syntax error after a hot-reload Tail Steer's stderr; the offending file path is logged. Fix or remove the file.

7. Coding agent setup

See docs/providers.md for per-tool walkthroughs:

  • Cursor, Cline, Continue.dev — IDE plugin base URL override
  • Claude Code (ANTHROPIC_BASE_URL) — subscription-default alias gotcha
  • Aider--openai-api-base flag
  • OpenAI / Anthropic SDKsbase_url parameter

8. Next steps