php-agentic/docs/api-keys.md
Snider a2a9423ad6 security: fix SQL injection and add workspace scoping to MCP tools
- Replace orderByRaw with parameterised CASE statements
- Add Task::scopeOrderByPriority() and scopeOrderByStatus()
- Add AgentPlan::scopeOrderByStatus()
- Add workspace validation to StateSet, StateGet, StateList tools
- Add workspace validation to PlanGet, PlanList tools
- Add SecurityTest.php with comprehensive isolation tests

Fixes SEC-002, SEC-003 from security audit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 12:21:01 +00:00

7 KiB

title description updated
API Keys Guide to Agent API key management 2026-01-29

API Key Management

Agent API keys provide authenticated access to the MCP tools and agentic services. This guide covers key creation, permissions, and security.

Key Structure

API keys follow the format: ak_ + 32 random alphanumeric characters.

Example: ak_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

The key is only displayed once at creation. Store it securely.

Creating Keys

Via Admin Panel

  1. Navigate to Workspace Settings > API Keys
  2. Click "Create New Key"
  3. Enter a descriptive name
  4. Select permissions
  5. Set expiration (optional)
  6. Click Create
  7. Copy the displayed key immediately

Programmatically

use Core\Mod\Agentic\Services\AgentApiKeyService;

$service = app(AgentApiKeyService::class);

$key = $service->create(
    workspace: $workspace,
    name: 'My Agent Key',
    permissions: [
        AgentApiKey::PERM_PLANS_READ,
        AgentApiKey::PERM_PLANS_WRITE,
        AgentApiKey::PERM_SESSIONS_WRITE,
    ],
    rateLimit: 100,
    expiresAt: now()->addYear()
);

// Only available once
$plainKey = $key->plainTextKey;

Permissions

Available Permissions

Permission Constant Description
plans.read PERM_PLANS_READ List and view plans
plans.write PERM_PLANS_WRITE Create, update, archive plans
phases.write PERM_PHASES_WRITE Update phases, manage tasks
sessions.read PERM_SESSIONS_READ List and view sessions
sessions.write PERM_SESSIONS_WRITE Start, update, end sessions
tools.read PERM_TOOLS_READ View tool analytics
templates.read PERM_TEMPLATES_READ List and view templates
templates.instantiate PERM_TEMPLATES_INSTANTIATE Create plans from templates
notify:read PERM_NOTIFY_READ List push campaigns
notify:write PERM_NOTIFY_WRITE Create/update campaigns
notify:send PERM_NOTIFY_SEND Send notifications

Permission Checking

// Single permission
$key->hasPermission('plans.write');

// Any of several
$key->hasAnyPermission(['plans.read', 'sessions.read']);

// All required
$key->hasAllPermissions(['plans.write', 'phases.write']);

Updating Permissions

$service->updatePermissions($key, [
    AgentApiKey::PERM_PLANS_READ,
    AgentApiKey::PERM_SESSIONS_READ,
]);

Rate Limiting

Configuration

Each key has a configurable rate limit (requests per minute):

$key = $service->create(
    workspace: $workspace,
    name: 'Limited Key',
    permissions: [...],
    rateLimit: 50  // 50 requests/minute
);

// Update later
$service->updateRateLimit($key, 100);

Checking Status

$status = $service->getRateLimitStatus($key);
// Returns:
// [
//     'limit' => 100,
//     'remaining' => 85,
//     'reset_in_seconds' => 45,
//     'used' => 15
// ]

Response Headers

Rate limit info is included in API responses:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 85
X-RateLimit-Reset: 45

When rate limited (HTTP 429):

Retry-After: 45

IP Restrictions

Keys can be restricted to specific IP addresses or ranges.

Enabling Restrictions

// Enable with whitelist
$service->enableIpRestrictions($key, [
    '192.168.1.0/24',      // CIDR range
    '10.0.0.5',            // Single IPv4
    '2001:db8::1',         // Single IPv6
    '2001:db8::/32',       // IPv6 CIDR
]);

// Disable restrictions
$service->disableIpRestrictions($key);

Managing Whitelist

// Add single entry
$key->addToIpWhitelist('192.168.2.0/24');

// Remove entry
$key->removeFromIpWhitelist('192.168.1.0/24');

// Replace entire list
$key->updateIpWhitelist([
    '10.0.0.0/8',
    '172.16.0.0/12',
]);

Parsing Input

For user-entered whitelists:

$result = $service->parseIpWhitelistInput("
    192.168.1.1
    192.168.2.0/24
    # This is a comment
    invalid-ip
");

// Result:
// [
//     'entries' => ['192.168.1.1', '192.168.2.0/24'],
//     'errors' => ['invalid-ip: Invalid IP address']
// ]

Key Lifecycle

Expiration

// Set expiration on create
$key = $service->create(
    ...
    expiresAt: now()->addMonths(6)
);

// Extend expiration
$service->extendExpiry($key, now()->addYear());

// Remove expiration (never expires)
$service->removeExpiry($key);

Revocation

// Immediately revoke
$service->revoke($key);

// Check status
$key->isRevoked();  // true
$key->isActive();   // false

Status Helpers

$key->isActive();    // Not revoked, not expired
$key->isRevoked();   // Has been revoked
$key->isExpired();   // Past expiration date
$key->getStatusLabel();  // "Active", "Revoked", or "Expired"

Authentication

Making Requests

Include the API key as a Bearer token:

curl -H "Authorization: Bearer ak_your_key_here" \
     https://mcp.host.uk.com/api/agent/plans

Authentication Flow

  1. Middleware extracts Bearer token
  2. Key looked up by SHA-256 hash
  3. Status checked (revoked, expired)
  4. IP validated if restrictions enabled
  5. Permissions checked against required scopes
  6. Rate limit checked and incremented
  7. Usage recorded (count, timestamp, IP)

Error Responses

HTTP Code Error Description
401 unauthorised Missing or invalid key
401 key_revoked Key has been revoked
401 key_expired Key has expired
403 ip_not_allowed Request IP not whitelisted
403 permission_denied Missing required permission
429 rate_limited Rate limit exceeded

Usage Tracking

Each key tracks:

  • call_count - Total lifetime calls
  • last_used_at - Timestamp of last use
  • last_used_ip - IP of last request

Access via:

$key->call_count;
$key->getLastUsedForHumans();  // "2 hours ago"

Best Practices

  1. Use descriptive names - "Production Agent" not "Key 1"
  2. Minimal permissions - Only grant needed scopes
  3. Set expiration - Rotate keys periodically
  4. Enable IP restrictions - When agents run from known IPs
  5. Monitor usage - Review call patterns regularly
  6. Revoke promptly - If key may be compromised
  7. Separate environments - Different keys for dev/staging/prod

Example: Complete Setup

use Core\Mod\Agentic\Services\AgentApiKeyService;
use Core\Mod\Agentic\Models\AgentApiKey;

$service = app(AgentApiKeyService::class);

// Create a production key
$key = $service->create(
    workspace: $workspace,
    name: 'Production Agent - Claude',
    permissions: [
        AgentApiKey::PERM_PLANS_READ,
        AgentApiKey::PERM_PLANS_WRITE,
        AgentApiKey::PERM_PHASES_WRITE,
        AgentApiKey::PERM_SESSIONS_WRITE,
        AgentApiKey::PERM_TEMPLATES_READ,
        AgentApiKey::PERM_TEMPLATES_INSTANTIATE,
    ],
    rateLimit: 200,
    expiresAt: now()->addYear()
);

// Restrict to known IPs
$service->enableIpRestrictions($key, [
    '203.0.113.0/24',  // Office network
    '198.51.100.50',   // CI/CD server
]);

// Store the key securely
$plainKey = $key->plainTextKey;  // Only chance to get this!