Fishnet supports custom service proxies for any HTTP API. Add GitHub, Stripe, Slack, or internal APIs with configurable authentication, endpoint blocking, and rate limiting.
Overview
Custom services are defined under [custom.<name>] in fishnet.toml. Each service gets:
Automatic credential injection from vault or environment variables
Endpoint blocking with glob patterns
Independent rate limiting
Request anomaly detection
Full audit logging
Requests are proxied to /custom/<name>/* and forwarded to the configured base_url.
Basic Configuration
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
blocked_endpoints = []
rate_limit = 100
rate_limit_window_seconds = 3600
Define a custom service proxy. <name> can be any identifier (e.g., github, stripe, slack).
The service will be accessible at /custom/<name>/*.
Upstream API base URL. Cannot be empty. Example values:
https://api.github.com
https://api.stripe.com
https://slack.com/api
https://internal.company.com/api
Authentication
Fishnet can automatically inject authentication headers using credentials from the vault or environment variables.
From Vault
Store the credential in Fishnet’s encrypted vault:
fishnet vault set github
# Enter: ghp_xxxxxxxxxxxxxxxxxxxx
Configure authentication header:
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
# No auth_value_env means use vault credential
Fishnet attaches:
Authorization: Bearer ghp_xxxxxxxxxxxxxxxxxxxx
From Environment Variable
Use an environment variable for the credential:
export GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx"
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
Fishnet reads $GITHUB_TOKEN at request time and attaches:
Authorization: Bearer ghp_xxxxxxxxxxxxxxxxxxxx
HTTP header name for authentication
custom.<name>.auth_value_prefix
Prefix added before the credential value. Common values:
Bearer (OAuth tokens)
token (GitHub)
"" (empty for raw API keys)
custom.<name>.auth_value_env
Environment variable containing the credential.
If empty, Fishnet looks for a vault credential with service name <name>.
Credential Resolution Order
Vault : If auth_value_env is empty, use vault credential for service <name>
Environment : If auth_value_env is set, read that environment variable
Error : If neither exists, request is denied
Example Configurations
GitHub (Personal Access Token)
Stripe (API Key)
Slack (Bot Token)
Internal API (Custom Header)
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
Blocked Endpoints
Prevent dangerous operations using glob patterns.
custom.<name>.blocked_endpoints
array[string]
default: "[]"
List of endpoint patterns to block. Patterns support:
HTTP method prefix: DELETE /repos/**
Glob wildcards: ** (any path segments), * (any characters)
Case-sensitive path matching
Pattern Matching
Patterns are formatted as METHOD /path/pattern:
[ custom . github ]
blocked_endpoints = [
"DELETE /repos/**" , # Block all repo deletions
"DELETE /orgs/**" , # Block all org deletions
"DELETE /orgs/*/members/**" , # Block removing org members
"DELETE /teams/*/members/**" , # Block removing team members
"PUT /repos/**/admin/**" , # Block admin permission changes
]
Pattern rules:
** matches any number of path segments
* matches any characters within a segment
Method is case-sensitive (DELETE not delete)
Path matching is case-sensitive
Example: GitHub Safety Policy
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
blocked_endpoints = [
# Prevent destructive operations
"DELETE /repos/**" ,
"DELETE /orgs/**" ,
"DELETE /user/repos/**" ,
# Prevent removing collaborators
"DELETE /repos/**/collaborators/**" ,
"DELETE /orgs/**/members/**" ,
"DELETE /orgs/**/teams/**" ,
"DELETE /teams/**/members/**" ,
# Prevent admin escalation
"PUT /repos/**/admin/**" ,
"PATCH /orgs/**/memberships/**" ,
]
Blocked requests return:
{
"error" : "endpoint blocked by custom policy"
}
And trigger a critical alert:
{
"alert_type" : "high_severity_denied_action" ,
"severity" : "critical" ,
"service" : "custom.github" ,
"message" : "Denied high-severity action DELETE /repos/org/repo: blocked by custom policy"
}
Rate Limiting
Each custom service has independent rate limiting.
Maximum number of requests allowed within the time window
custom.<name>.rate_limit_window_seconds
Time window in seconds for rate limit (default 1 hour)
[ custom . github ]
rate_limit = 2 # 2 requests
rate_limit_window_seconds = 60 # per minute
When rate limit is exceeded:
{
"error" : "rate limit exceeded, retry after 42s" ,
"retry_after_seconds" : 42
}
HTTP status: 429 Too Many Requests
Common Rate Limit Patterns
# Aggressive: 100 requests per hour (production)
[ custom . github ]
rate_limit = 100
rate_limit_window_seconds = 3600
# Conservative: 2 requests per minute (demo/testing)
[ custom . github ]
rate_limit = 2
rate_limit_window_seconds = 60
# Permissive: 1000 requests per day
[ custom . internal ]
rate_limit = 1000
rate_limit_window_seconds = 86400
Making Requests
Fishnet handles authentication automatically. Your agent just calls the proxy:
Path Mapping
/custom/<name>/path → <base_url>/path
Example:
GET /custom/github/user/repos
→ GET https://api.github.com/user/repos
Example: GitHub
import requests
# List repositories (Fishnet adds Authorization header)
response = requests.get(
"http://localhost:8473/custom/github/user/repos"
)
# Create an issue
response = requests.post(
"http://localhost:8473/custom/github/repos/owner/repo/issues" ,
json = {
"title" : "Bug report" ,
"body" : "Something is broken"
}
)
# This would be blocked:
try :
response = requests.delete(
"http://localhost:8473/custom/github/repos/owner/repo"
)
except requests.HTTPError as e:
print ( f "Blocked: { e.response.json() } " )
# {"error": "endpoint blocked by custom policy"}
Example: Stripe
import requests
# Create a customer (Fishnet adds Authorization: Bearer sk_...)
response = requests.post(
"http://localhost:8473/custom/stripe/v1/customers" ,
data = {
"email" : "[email protected] " ,
"name" : "Jane Doe"
}
)
customer = response.json()
print ( f "Created customer: { customer[ 'id' ] } " )
Connection Pooling
Custom services use Fishnet’s HTTP client with connection pooling.
Per-Service Pool Size
Override the default pool size for a specific custom service:
[ http ]
pool_max_idle_per_host = 16 # Global default
[ http . upstream_pool_max_idle_per_host ]
" custom.github " = 8 # Override for GitHub
" custom.stripe " = 4 # Override for Stripe
http.upstream_pool_max_idle_per_host.custom.<name>
Maximum idle connections to keep pooled for this custom service.
Overrides the global http.pool_max_idle_per_host default.
Higher values reduce latency for high-throughput services, but consume more memory.
Audit Logging
Every custom service request is logged:
{
"id" : "aud_..." ,
"intent_type" : "api_call" ,
"service" : "github" ,
"action" : "POST /repos/owner/repo/issues" ,
"decision" : "approved" ,
"timestamp" : 1735689600 ,
"policy_version_hash" : "0x1234..." ,
"intent_hash" : "0x5678..."
}
Denied requests include a reason:
{
"decision" : "denied" ,
"reason" : "endpoint blocked by custom policy"
}
Alerts
Custom services support anomaly detection:
[ alerts ]
anomalous_volume = true
new_endpoint = true
time_anomaly = true
high_severity_denied_action = true
Anomalous volume : Unusual spike in request frequency
New endpoint : First time accessing a specific endpoint
Time anomaly : Requests at unusual hours
High severity denied : Blocked endpoint attempts
Alerts include the service name:
{
"alert_type" : "new_endpoint" ,
"severity" : "warning" ,
"service" : "custom.github" ,
"message" : "New endpoint detected: POST /repos/owner/repo/issues"
}
Multiple Custom Services
You can define multiple custom services:
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
blocked_endpoints = [ "DELETE /repos/**" ]
rate_limit = 100
rate_limit_window_seconds = 3600
[ custom . stripe ]
base_url = "https://api.stripe.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "STRIPE_API_KEY"
blocked_endpoints = []
rate_limit = 50
rate_limit_window_seconds = 60
[ custom . internal ]
base_url = "https://api.internal.company.com"
auth_header = "X-API-Key"
auth_value_prefix = ""
auth_value_env = "INTERNAL_API_KEY"
blocked_endpoints = [ "DELETE /**" ]
rate_limit = 1000
rate_limit_window_seconds = 86400
Each service has:
Independent rate limits
Separate credential storage
Own connection pool
Isolated audit logs
Complete Example
# fishnet.toml
[ custom . github ]
base_url = "https://api.github.com"
auth_header = "Authorization"
auth_value_prefix = "Bearer "
auth_value_env = "GITHUB_TOKEN"
# Block destructive operations
blocked_endpoints = [
"DELETE /repos/**" ,
"DELETE /orgs/**" ,
"DELETE /orgs/**/members/**" ,
"DELETE /orgs/**/teams/**" ,
"DELETE /teams/**/members/**" ,
"PUT /repos/**/admin/**" ,
]
# Conservative rate limit for safety
rate_limit = 2
rate_limit_window_seconds = 60
# Alerts
[ alerts ]
anomalous_volume = true
new_endpoint = true
high_severity_denied_action = true
# Connection pooling
[ http . upstream_pool_max_idle_per_host ]
" custom.github " = 8
Set credential:
export GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx"
fishnet start
Use from agent:
import requests
# Safe operation: create an issue
response = requests.post(
"http://localhost:8473/custom/github/repos/owner/repo/issues" ,
json = { "title" : "Feature request" , "body" : "Add dark mode" }
)
print ( f "Created issue # { response.json()[ 'number' ] } " )
# Blocked operation: delete repo
response = requests.delete(
"http://localhost:8473/custom/github/repos/owner/repo"
)
print (response.json())
# {"error": "endpoint blocked by custom policy"}