Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Shashank-H/gaiter-gaurd/llms.txt

Use this file to discover all available pages before exploring further.

Overview

AI agents interact with GaiterGuard through three core endpoints: POST /proxy for submitting requests, GET /status/:actionId for polling approval status, and POST /proxy/execute/:actionId for executing approved requests. This guide covers the complete integration flow with real code examples.

Authentication

All agent requests require an Agent-Key header. Agent keys are provisioned through the dashboard and start with the agt_ prefix.

Required Headers

Every agent request must include:
Agent-Key
string
required
The agent’s API key in the format agt_<64 hex characters>Example:
Agent-Key: agt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
Obtained from the dashboard when creating an agent.
Idempotency-Key
string
required
Unique identifier for this request to prevent duplicate execution.Required for: POST and PATCH requestsFormat: Any unique string (UUID recommended)Example:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
If the same idempotency key is reused, the cached response from the first request is returned.

Authentication Flow

The gateway authenticates agents using the Agent-Key header (see backend/src/middleware/auth.ts:65):
  1. Extract Agent-Key header from request
  2. Validate format (must start with agt_)
  3. Hash the key using SHA-256
  4. Query agents table for matching key hash
  5. Verify agent is active (isActive = true)
  6. Update lastUsedAt timestamp (fire-and-forget)
Error responses:
  • 401 Unauthorized - Missing, invalid, or revoked Agent-Key
  • 403 Forbidden - Agent not scoped to the requested service

Request Flow

Step 1: Submit Proxy Request

Submit a request to be proxied through the gateway. Endpoint: POST /proxy Request body:
{
  "targetUrl": "https://api.stripe.com/v1/charges",
  "method": "POST",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"amount\": 500, \"currency\": \"usd\", \"customer\": \"cus_123\"}",
  "intent": "Charge customer $5.00 for monthly subscription renewal",
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000"
}
Request fields:
targetUrl
string
required
The full URL to proxy to. Must start with the baseUrl of a service the agent is scoped to.SSRF Protection: The gateway validates that:
  • Hostname matches the registered service
  • Path starts with the service’s base path
  • Target is not a private IP range
method
string
required
HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
headers
object
Request headers to forward. Do not include authentication headers - the gateway injects credentials automatically.Automatically injected:
  • Authorization: Bearer <token> (for bearer/oauth2 auth)
  • Authorization: Basic <base64> (for basic auth)
  • Custom API key headers (for api_key auth)
body
string
Request body as a JSON-stringified string. Omit for GET/HEAD/DELETE.
intent
string
required
Plain English description of what this request does. Used by the LLM for risk assessment.Good intent:
  • “Fetch the latest 10 orders for daily reporting”
  • “Create a new customer record for signup flow”
  • “Charge customer $5.00 for subscription renewal”
Bad intent:
  • “get data” (too vague)
  • “Read orders” for a DELETE request (mismatch → high risk score)
idempotencyKey
string
Unique request identifier. Required for POST and PATCH. Prevents duplicate execution.
The Idempotency-Key header takes precedence over this field if both are provided.

Response Scenarios

Request forwarded immediately and executed.Response:
{
  "id": "ch_123",
  "amount": 500,
  "currency": "usd",
  "status": "succeeded"
}
Headers:
  • X-Proxy-Status: forwarded
  • X-Idempotency-Status: processed (if idempotency key used)
The agent receives the upstream service’s response directly.

Step 2: Poll for Approval

When a request is blocked (HTTP 428), poll the status endpoint until a terminal state is reached. Endpoint: GET /status/:actionId Polling pattern:
  • Poll every 5-10 seconds
  • Continue until status is APPROVED, DENIED, EXPIRED, or EXECUTED
  • Timeout after ~10 minutes (75 polls at 8 seconds)
import time
import requests

GATEWAY_URL = "http://localhost:3000"
AGENT_KEY = "agt_a1b2c3d4..."
ACTION_ID = "550e8400-e29b-41d4-a716-446655440000"

POLL_INTERVAL = 8  # seconds
MAX_POLLS = 75

for attempt in range(MAX_POLLS):
    response = requests.get(
        f"{GATEWAY_URL}/status/{ACTION_ID}",
        headers={"Agent-Key": AGENT_KEY}
    )

    if response.status_code != 200:
        print(f"Status poll error {response.status_code}")
        time.sleep(POLL_INTERVAL)
        continue

    data = response.json()
    status = data["status"]
    print(f"Attempt {attempt + 1}: {status}")

    if status == "PENDING":
        time.sleep(POLL_INTERVAL)
        continue

    elif status == "APPROVED":
        print("Approved - executing...")
        # Proceed to Step 3
        break

    elif status == "DENIED":
        print(f"Denied at {data.get('resolved_at')}")
        # Handle gracefully
        break

    elif status == "EXPIRED":
        print("Approval expired - resubmit via POST /proxy")
        break

    elif status == "EXECUTED":
        # Already executed (concurrent poll)
        result = data["result"]
        print(f"Already executed: HTTP {result['status']}")
        print(result["body"])
        break

else:
    print("Max polls reached - giving up")
Response shapes by status:
{
  "status": "PENDING",
  "action_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-03-03T12:34:56.789Z"
}
Waiting for human review. Continue polling.

Step 3: Execute Approved Request

Once status is APPROVED, execute the request. Endpoint: POST /proxy/execute/:actionId No request body needed - the gateway replays the original stored request with fresh credentials.
response = requests.post(
    f"{GATEWAY_URL}/proxy/execute/{ACTION_ID}",
    headers={"Agent-Key": AGENT_KEY}
)

if response.status_code in (200, 201, 202, 204):
    print(f"Executed successfully: HTTP {response.status_code}")
    print(response.json())
elif response.status_code == 410:
    print("Approval expired before execution")
    # Resubmit via POST /proxy
elif response.status_code == 409:
    print(f"Cannot execute: {response.json()['error']}")
    # Check status again
else:
    print(f"Execute failed: {response.status_code}")
    print(response.json())
Success response: Returns the upstream service response with header:
X-Proxy-Status: executed-approved
Error responses:
  • 409 Conflict - Action status is not APPROVED (check current status)
  • 410 Gone - Approval expired; resubmit via POST /proxy

Error Handling

HTTP Status Codes

401 Unauthorized
error
Missing, invalid, or revoked Agent-KeyAction: Verify Agent-Key is correct and agent is active
403 Forbidden
error
Agent not scoped to the target serviceAction: Grant service access in dashboard → Agents → Edit → Service Scope
404 Not Found
error
  • Service not found matching targetUrl
  • Action not found (ownership check failed)
Action: Verify service is registered and agent is scoped to it
409 Conflict
error
  • Idempotency key conflict (request already processing)
  • Cannot execute action with current status
Action: Wait for in-flight request to complete, or check action status
410 Gone
error
Approval expired before executionAction: Resubmit the original request via POST /proxy
428 Precondition Required
success
Request blocked for human approval (not an error)Action: Enter polling flow
502 Bad Gateway
error
Upstream service returned an errorAction: Check upstream service health and request validity
504 Gateway Timeout
error
Upstream request exceeded 30 second timeoutAction: Retry or contact service owner

Risk Score Reference

The LLM assigns a base risk score by HTTP method (see backend/src/services/risk.service.ts:1):
MethodBase RiskTypical Outcome (threshold=0.5)
GET, HEAD0.05-0.1Usually passes
POST0.3Passes with clear intent
PATCH0.4Passes with clear intent
PUT0.5Expect 428
DELETE0.7Almost always 428
The final score is adjusted based on:
  • Intent clarity (vague intent → higher score)
  • Intent-body mismatch (“read” intent with DELETE → very high score)
  • Sensitive keywords (“delete”, “destroy”, “drop”, etc.)
  • API documentation context (registered with service)

Complete Integration Example

Full Python script with polling and execution:
#!/usr/bin/env python3
import time, json, requests, uuid

GATEWAY_URL = "http://localhost:3000"
AGENT_KEY = "agt_a1b2c3d4..."

def proxy_request(target_url, method, body, intent):
    """Submit request via POST /proxy"""
    response = requests.post(
        f"{GATEWAY_URL}/proxy",
        headers={
            "Agent-Key": AGENT_KEY,
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4())
        },
        json={
            "targetUrl": target_url,
            "method": method,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps(body) if body else None,
            "intent": intent
        }
    )

    if response.status_code in (200, 201, 202, 204):
        return {"status": "forwarded", "data": response.json()}
    elif response.status_code == 428:
        data = response.json()
        print(f"[428] Risk blocked: {data['risk_explanation']}")
        return {"status": "blocked", "action_id": data["action_id"]}
    else:
        raise Exception(f"Proxy failed: {response.status_code} {response.text}")

def poll_approval(action_id, max_polls=75, interval=8):
    """Poll GET /status/:actionId until terminal state"""
    for attempt in range(max_polls):
        response = requests.get(
            f"{GATEWAY_URL}/status/{action_id}",
            headers={"Agent-Key": AGENT_KEY}
        )
        data = response.json()
        status = data["status"]
        print(f"  Poll {attempt+1}: {status}")

        if status == "PENDING":
            time.sleep(interval)
        elif status == "APPROVED":
            return {"status": "approved"}
        elif status == "DENIED":
            return {"status": "denied"}
        elif status == "EXPIRED":
            return {"status": "expired"}
        elif status == "EXECUTED":
            return {"status": "executed", "result": data["result"]}
        else:
            raise Exception(f"Unknown status: {status}")

    raise Exception("Max polls reached")

def execute_approved(action_id):
    """Execute via POST /proxy/execute/:actionId"""
    response = requests.post(
        f"{GATEWAY_URL}/proxy/execute/{action_id}",
        headers={"Agent-Key": AGENT_KEY}
    )

    if response.status_code in (200, 201, 202, 204):
        return response.json()
    elif response.status_code == 410:
        raise Exception("Approval expired before execution")
    else:
        raise Exception(f"Execute failed: {response.status_code} {response.text}")

# Example usage
if __name__ == "__main__":
    # Submit a high-risk DELETE request
    result = proxy_request(
        target_url="https://api.example.com/v1/users/123",
        method="DELETE",
        body=None,
        intent="Delete inactive test user account"
    )

    if result["status"] == "forwarded":
        print("Request forwarded directly")
        print(result["data"])

    elif result["status"] == "blocked":
        action_id = result["action_id"]
        print(f"Polling for approval: {action_id}")

        poll_result = poll_approval(action_id)

        if poll_result["status"] == "approved":
            print("Executing approved request...")
            exec_result = execute_approved(action_id)
            print("Execution result:")
            print(exec_result)

        elif poll_result["status"] == "denied":
            print("Request denied by human")

        elif poll_result["status"] == "expired":
            print("Approval expired - resubmit if still needed")

        elif poll_result["status"] == "executed":
            print("Already executed:")
            print(poll_result["result"])

Next Steps

Agent Skill

Install the Claude agent skill for automatic integration

Configuration

Tune risk threshold and approval TTL

Build docs developers (and LLMs) love