Machine-readable readiness/discovery API and governed control-plane plugin for Craft CMS and Commerce.
Current plugin version: 0.3.9
This plugin gives external/internal agents a stable interface for:
- health checks for automation (
/agents/v1/health) - readiness summaries (
/agents/v1/readiness) - auth introspection for token diagnostics (
/agents/v1/auth/whoami) - product snapshot browsing (
/agents/v1/products) - control policies/approvals/execution ledger/audit (
/agents/v1/control/*) - read-only CLI discovery commands (
craft agents/*)
The API now includes:
- read surfaces for diagnostics and discovery
- sign-and-control primitives for governed machine actions:
- policy evaluation
- approval gates
- idempotent execution ledger
- immutable audit events
It also exposes proposal-oriented discovery files at root-level endpoints:
GET /llms.txtGET /commerce.txt
Requirements:
- PHP
^8.2 - Craft CMS
^5.0
After Plugin Store publication:
composer require klick/agents:^0.3.9
php craft plugin/install agentsFor monorepo development, the package can also be installed via path repository at plugins/agents.
Recommended local workflow:
- develop in a dedicated Craft sandbox (not production-bound project roots)
- link plugin via local Composer path repo only in that sandbox
- use the scripted bootstrap/fixture/smoke/release steps in DEVELOPMENT.md
- restore production-bound projects to store-backed versions after local debugging
Environment variables:
PLUGIN_AGENTS_ENABLED(true/false)PLUGIN_AGENTS_API_TOKEN(required when token enforcement is enabled)PLUGIN_AGENTS_API_CREDENTIALS(JSON credential set with optional per-credential scopes)PLUGIN_AGENTS_REQUIRE_TOKEN(default:true)PLUGIN_AGENTS_ALLOW_INSECURE_NO_TOKEN_IN_PROD(default:false, keep false)PLUGIN_AGENTS_ALLOW_QUERY_TOKEN(default:false)PLUGIN_AGENTS_FAIL_ON_MISSING_TOKEN_IN_PROD(default:true)PLUGIN_AGENTS_TOKEN_SCOPES(comma/space list, default scoped read set)PLUGIN_AGENTS_REDACT_EMAIL(default:true, applied when sensitive scope is missing)PLUGIN_AGENTS_RATE_LIMIT_PER_MINUTE(default:60)PLUGIN_AGENTS_RATE_LIMIT_WINDOW_SECONDS(default:60)PLUGIN_AGENTS_WEBHOOK_URL(optional HTTPS endpoint for change notifications)PLUGIN_AGENTS_WEBHOOK_SECRET(required when webhook URL is set; used for HMAC signature)PLUGIN_AGENTS_WEBHOOK_TIMEOUT_SECONDS(default:5)PLUGIN_AGENTS_WEBHOOK_MAX_ATTEMPTS(default:3, max queue retry attempts)PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL(default:false; enables refund-approval/control surfaces)
These are documented in .env.example.
Enablement precedence:
- If
PLUGIN_AGENTS_ENABLEDis set, it overrides CP/plugin setting state. - If
PLUGIN_AGENTS_ENABLEDis not set, CP/plugin settingenabledcontrols runtime on/off.
- Docs: https://github.com/klick/agents
- Issues: https://github.com/klick/agents/issues
- Source: https://github.com/klick/agents
By default, v1 routes require token-based access (PLUGIN_AGENTS_REQUIRE_TOKEN=true).
Fail-closed behavior in production is enabled by default (PLUGIN_AGENTS_FAIL_ON_MISSING_TOKEN_IN_PROD=true).
If PLUGIN_AGENTS_REQUIRE_TOKEN=false is set in production, the plugin will still enforce token auth unless PLUGIN_AGENTS_ALLOW_INSECURE_NO_TOKEN_IN_PROD=true is explicitly enabled.
Credential sources:
PLUGIN_AGENTS_API_CREDENTIALS(strict JSON credential objects with per-credential scopes)PLUGIN_AGENTS_API_TOKEN(legacy single-token fallback)- Control Panel managed credentials (API Keys tab: create/edit scopes/rotate/revoke/delete with last-used metadata)
Managed credentials are stored in plugin DB tables and participate in runtime auth alongside env credentials.
Credential lifecycle permission keys (CP):
agents-viewCredentialsagents-manageCredentials(create + edit scopes/display name)agents-rotateCredentialsagents-revokeCredentialsagents-deleteCredentials
Control-plane permission keys (CP):
agents-viewControlPlaneagents-manageControlPoliciesagents-manageControlApprovalsagents-executeControlActions
Supported token transports:
Authorization: Bearer <token>(default)X-Agents-Token: <token>(default)?apiToken=<token>only whenPLUGIN_AGENTS_ALLOW_QUERY_TOKEN=true
Example credential JSON:
[
{"id":"integration-a","token":"token-a","scopes":["health:read","readiness:read","products:read"]},
{"id":"integration-b","token":"token-b","scopes":"orders:read orders:read_sensitive"}
]Object-map credential JSON is also supported:
{
"integration-a": {"token":"token-a","scopes":["health:read","readiness:read"]},
"integration-b": {"token":"token-b","scopes":"orders:read orders:read_sensitive"}
}Validation notes:
- Single-object shape is accepted when it includes
token(orvalue) and optionalid/scopes. - Scalar values in keyed-object mode are ignored to avoid accidental token expansion from malformed JSON.
Base URL (this project): /agents/v1
Read/discovery endpoints:
GET /healthGET /readinessGET /auth/whoamiGET /productsGET /ordersGET /orders/show(requires exactly one ofidornumber)GET /entriesGET /entries/show(requires exactly one ofidorslug)GET /changesGET /sectionsGET /capabilitiesGET /openapi.json
Control-plane endpoints (only when PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true):
GET /control/policiesPOST /control/policies/upsertGET /control/approvalsPOST /control/approvals/requestPOST /control/approvals/decideGET /control/executionsPOST /control/actions/executeGET /control/audit
Root-level discovery files:
GET /llms.txt(public when enabled)GET /commerce.txt(public when enabled)- Discovery aliases:
GET /capabilities->GET /agents/v1/capabilitiesGET /openapi.json->GET /agents/v1/openapi.json
Read scopes:
health:readreadiness:readauth:readproducts:readorders:readorders:read_sensitiveentries:readentries:read_all_statuseschanges:readsections:readcapabilities:readopenapi:readcontrol:policies:read(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:approvals:read(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:executions:read(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:audit:read(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)
Write scopes:
control:policies:write(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:approvals:request(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:approvals:decide(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:approvals:write(legacy combined scope, backward-compatible; only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)control:actions:execute(only whenPLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)
Craft-native command routes:
craft agents/product-listcraft agents/order-listcraft agents/order-showcraft agents/entry-listcraft agents/entry-showcraft agents/section-listcraft agents/discovery-prewarmcraft agents/auth-checkcraft agents/discovery-checkcraft agents/readiness-checkcraft agents/smoke
Examples:
# Product discovery (text output)
php craft agents/product-list --status=live --limit=10
# Product discovery (JSON output)
php craft agents/product-list --status=all --search=emboss --limit=5 --json=1
# Low stock view
php craft agents/product-list --low-stock=1 --low-stock-threshold=10 --limit=25
# Orders from last 14 days
php craft agents/order-list --status=shipped --last-days=14 --limit=20
# Show a single order
php craft agents/order-show --number=A1B2C3D4
php craft agents/order-show --resource-id=12345
# Entries
php craft agents/entry-list --section=termsConditionsB2b --status=live --limit=20
php craft agents/entry-show --slug=shipping-information
php craft agents/entry-show --resource-id=123
# Sections
php craft agents/section-list
# Prewarm llms.txt + commerce.txt cache
php craft agents/discovery-prewarm
php craft agents/discovery-prewarm --target=llms --json=1
# Auth posture check
php craft agents/auth-check
php craft agents/auth-check --strict=1 --json=1
# Discovery/readiness/smoke checks
php craft agents/discovery-check --json=1
php craft agents/readiness-check --json=1
php craft agents/smoke --json=1CLI output defaults to human-readable text. Add --json=1 for machine consumption.
Identifier notes for show commands:
agents/order-show: use exactly one of--numberor--resource-id.agents/entry-show: use exactly one of--slugor--resource-id.
q(search text)status(live|pending|disabled|expired|all, defaultlive)sort(updatedAt|createdAt|title, defaultupdatedAt)limit(1..200, default 50)cursor(opaque cursor; legacy pagination + incremental continuation)updatedSince(RFC3339 timestamp bootstrap for incremental mode, for example2026-02-24T12:00:00Z)
/orders:status(handle orall),lastDays(default 30),limit(1..200)/ordersincremental:cursor(opaque),updatedSince(RFC3339). When incremental params are used andlastDaysis omitted, the default window is0(no date-created cutoff)./orders/show: exactly one ofidornumber
/entries:section,type,status,search(orq),limit(1..200)/entriesincremental:cursor(opaque),updatedSince(RFC3339)/entries/show: exactly one ofidorslug; optionalsectionwhen usingslug
/changes:types(optional comma list:products,orders,entries),updatedSince(RFC3339 bootstrap),cursor(opaque continuation),limit(1..200)/changesreturns normalizeddata[]items with:resourceType(product|order|entry)resourceId(string)action(created|updated|deleted)updatedAt(RFC3339 UTC)snapshot(minimal object forcreated|updated,nullfordeletedtombstones)
POST /control/policies/upsertbody:handle(required)actionPattern(required, wildcard-compatible)displayName,riskLevel,enabled,requiresApproval,config
GET /control/approvalsquery:status,actionType,limitPOST /control/approvals/requestbody:actionType(required)actionRef,reason,payload,metadatametadata.source,metadata.agentId,metadata.traceIdare required (agent provenance)- idempotency via
X-Idempotency-Keyheader (oridempotencyKeybody field)
POST /control/approvals/decidebody:approvalId(required)decision(approved|rejected)decisionReason
GET /control/executionsquery:status,actionType,limitPOST /control/actions/executebody:actionType(required)idempotencyKey(required, header or body)actionRef,approvalId,payload- response may include
idempotentReplay=truewhen the key was already processed
GET /control/auditquery:category,actorId,limit
cursortakes precedence overupdatedSincewhen both are provided.- Incremental mode uses deterministic ordering:
updatedAt, thenid. - Incremental responses include
page.syncMode=incremental,page.hasMore,page.nextCursor, and snapshot window metadata. - Cursor tokens are opaque and may expire; restart from a recent
updatedSincecheckpoint if needed. /changescursor continuity also preserves the selectedtypesfilter.- Invalid
updatedSince/cursorinputs return400 INVALID_REQUESTwith stable error payload fields.
Webhook notifications are optional and are enabled only when both PLUGIN_AGENTS_WEBHOOK_URL and PLUGIN_AGENTS_WEBHOOK_SECRET are configured.
Behavior:
- Events are queued asynchronously on
product|order|entrycreate/update/delete changes. - Event payload mirrors
/changesitems:resourceType,resourceId,action,updatedAt,snapshot. - Retry behavior uses queue retries up to
PLUGIN_AGENTS_WEBHOOK_MAX_ATTEMPTS. - Variant changes are emitted as
productupdatedevents.
Request headers:
X-Agents-Webhook-Id: unique event idX-Agents-Webhook-Timestamp: unix timestampX-Agents-Webhook-Signature:sha256=<hex hmac>
Signature verification:
- signed string:
<timestamp>.<raw-request-body> - algorithm:
HMAC-SHA256 - secret:
PLUGIN_AGENTS_WEBHOOK_SECRET
Queue note:
- Webhooks are delivered by Craft queue workers; ensure
php craft queue/runorphp craft queue/listenis active in environments where webhook delivery is required.
/capabilities: machine-readable list of supported endpoints + CLI commands./openapi.json: OpenAPI 3.1 descriptor for this API surface.
Discovery file behavior:
llms.txt/commerce.txtare generated dynamically as plain text.- They include
ETag+Last-Modifiedheaders and support304 Not Modified. - Output is cached and invalidated on relevant content/product updates.
Example:
curl -H "Authorization: Bearer $PLUGIN_AGENTS_API_TOKEN" \
"https://example.com/agents/v1/products?status=live&sort=title&limit=2"- JSON only
- All API responses include
X-Request-Id. - Guarded JSON/error responses set
Cache-Control: no-store, private. - Products response includes:
data[]with minimal product fields (id,title,slug,status,updatedAt,url, etc.)pagewithnextCursor,limit,count
- Changes response includes:
data[]normalized change items (resourceType,resourceId,action,updatedAt,snapshot)pagewithnextCursor,hasMore,limit,count,updatedSince,snapshotEnd
- Health/readiness include plugin, environment, and readiness score fields.
- Error responses use a stable schema:
{
"error": "UNAUTHORIZED",
"message": "Missing or invalid token.",
"status": 401,
"requestId": "agents-9fd2b20abec4a65f"
}- Rate limiting headers are returned on each guarded request:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
- Rate limiting is applied before and after auth checks to throttle invalid-token attempts.
- Exceeded limits return HTTP
429withRATE_LIMIT_EXCEEDED. - Missing/invalid credentials return HTTP
401. - Missing required scope returns HTTP
403withFORBIDDEN. - Read/discovery endpoints are
GET/HEADonly. - Control-plane write endpoints accept
POSTand are token-authenticated API actions. - Misconfigured production token setup returns HTTP
503withSERVER_MISCONFIGURED. - Disabled runtime state returns HTTP
503withSERVICE_DISABLED. - Invalid request payload/params return HTTP
400withINVALID_REQUEST. - Missing resource lookups return HTTP
404withNOT_FOUND. - Unexpected server errors return HTTP
500withINTERNAL_ERROR. - Query-token auth is disabled by default to reduce token leakage risk.
- Credential parsing is strict for
PLUGIN_AGENTS_API_CREDENTIALS(credential objects only) and ignores malformed scalar entries. - Sensitive order fields are scope-gated; email is redacted by default unless
orders:read_sensitiveis granted. - Entry access to non-live statuses is scope-gated by
entries:read_all_statuses. - Endpoint is not meant for frontend/public user flows; token is the intended control plane.
Note: llms.txt and commerce.txt are public discovery surfaces and are not guarded by the API token.
- Capture
X-Request-Idfrom the failing response. - Confirm the error
status+errorcode pair. - Match the code to the fix path:
UNAUTHORIZED(401): token missing/invalid or wrong transport.FORBIDDEN(403): token missing required scope.INVALID_REQUEST(400): malformed query or invalid identifier combination.NOT_FOUND(404): requested resource does not exist.METHOD_NOT_ALLOWED(405): endpoint does not support the HTTP method used.RATE_LIMIT_EXCEEDED(429): respectX-RateLimit-*and retry after reset.SERVICE_DISABLED(503): plugin runtime disabled by env/CP setting.SERVER_MISCONFIGURED(503): token/security env configuration invalid.INTERNAL_ERROR(500): unexpected server-side failure.
- Correlate by
X-Request-Idin server logs for root-cause details.
These values can be overridden from your project via config/agents.php:
<?php
return [
'enableLlmsTxt' => true,
'enableCommerceTxt' => true,
'llmsTxtCacheTtl' => 86400,
'commerceTxtCacheTtl' => 3600,
'llmsSiteSummary' => 'Product and policy discovery for assistants.',
'llmsIncludeAgentsLinks' => true,
'llmsIncludeSitemapLink' => true,
'llmsLinks' => [
['label' => 'Support', 'url' => '/support'],
['label' => 'Contact', 'url' => '/contact'],
],
'commerceSummary' => 'Commerce metadata for discovery workflows.',
'commerceCatalogUrl' => '/agents/v1/products?status=live&limit=200',
'commercePolicyUrls' => [
'shipping' => '/shipping-information',
'returns' => '/returns',
'payment' => '/payment-options',
],
'commerceSupport' => [
'email' => 'support@example.com',
'phone' => '+1-555-0100',
'url' => '/contact',
],
'commerceAttributes' => [
'currency' => 'USD',
'region' => 'US',
],
];Agentssection now uses 3 primary subnav views by default:agents/dashboard/overview(Dashboard)agents/settings(Settings)agents/credentials(API Keys)
- Optional experimental subnav view (enabled via
PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true):agents/control(Return Requests)
- Dashboard includes top tabs:
Overview(agents/dashboard/overview)Readiness(agents/dashboard/readiness)Discovery(agents/dashboard/discovery)Security(agents/dashboard/security)
- Legacy CP paths remain valid and resolve to Dashboard tabs:
agents->dashboard/overviewagents/overview->dashboard/overviewagents/readiness->dashboard/readinessagents/discovery->dashboard/discoveryagents/security->dashboard/securityagents/health->dashboard/readiness
- Dashboard/Overview:
- runtime enabled/disabled state and source (
envvs CP setting) - env-lock aware runtime toggle
- quick endpoint links + discovery refresh entrypoint
- ownership split guidance (
CPvsconfig/agents.phpvs.env)
- runtime enabled/disabled state and source (
- Dashboard/Readiness:
- readiness score, criterion breakdown, component checks, warnings
- health/readiness diagnostic JSON snapshots
- Dashboard/Discovery:
- read-only
llms.txt/commerce.txtstatus, metadata, preview snippets - operator actions: refresh (
all|llms|commerce) and clear cache
- read-only
- Dashboard/Security:
- read-only effective auth/rate-limit/redaction/webhook posture
- centralized warning output from shared security policy logic
- Return Requests (
agents/control, experimental flag required):- queue-first operator flow for requests waiting on human decision
- clear split between decision queue, runs needing follow-up, and historical activity
- agent-first model: CP request form is disabled by default
- optional manual fallback can be enabled in Settings (
allowCpApprovalRequests) - rule-aware execution guardrails (disabled rule blocks, approval linkage checks)
- immutable audit trail with optional advanced snapshot JSON
- API Keys:
- managed credential lifecycle (create/edit scopes/rotate/revoke/delete)
- one-time token reveal on create/rotate
- Verify Dashboard subnav loads and each top tab (
overview,readiness,discovery,security) switches correctly. - Verify legacy aliases (
agents,agents/overview,agents/readiness,agents/discovery,agents/security,agents/health) still resolve to the expected Dashboard tab. - Verify runtime lightswitch is disabled when
PLUGIN_AGENTS_ENABLEDis set. - Verify discovery actions work:
- refresh
all - refresh
llms - refresh
commerce - clear discovery cache
- refresh
- Verify security tab shows posture without exposing token/secret values.
- When
PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true, verify Return Requests flows:- create/update policy
- request approval via API (agent token) with provenance metadata
- approve/reject approval in CP
- execute action with idempotency key
- audit event appears for each state transition
- Verify API and CLI behavior remains unchanged except expected
SERVICE_DISABLEDwhen runtime is off.
- PHP namespace root is now
Klick\\Agents(for exampleKlick\\Agents\\Plugin). - Plugin handle stays
agents(CP nav, routes, and CLI command prefixes remain unchanged).
- Set
PLUGIN_AGENTS_API_TOKENto a strong secret and keepPLUGIN_AGENTS_REQUIRE_TOKEN=true. - Prefer
PLUGIN_AGENTS_API_CREDENTIALSfor per-integration token/scope separation. - Keep
PLUGIN_AGENTS_FAIL_ON_MISSING_TOKEN_IN_PROD=true. - Keep
PLUGIN_AGENTS_ALLOW_INSECURE_NO_TOKEN_IN_PROD=false. - Keep
PLUGIN_AGENTS_ALLOW_QUERY_TOKEN=falseunless legacy clients require it temporarily. - Start with default scopes; only add elevated scopes when required:
orders:read_sensitiveentries:read_all_statusescontrol:policies:write(experimental flag only)control:approvals:request(experimental flag only)control:approvals:decide(experimental flag only)control:approvals:write(legacy compatibility; experimental flag only)control:actions:execute(experimental flag only)
- Verify
capabilities/openapi.jsonoutputs reflect active auth transport/settings. - Run
scripts/security-regression-check.shagainst your environment before promotion.
- Prior behavior effectively granted broad read access to any valid token.
- New default scopes intentionally exclude elevated permissions.
- To preserve legacy broad reads temporarily, set:
PLUGIN_AGENTS_TOKEN_SCOPES=\"health:read readiness:read products:read orders:read orders:read_sensitive entries:read entries:read_all_statuses changes:read sections:read capabilities:read openapi:read\"- If
PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true, optionally append control read scopes:control:policies:read control:approvals:read control:executions:read control:audit:read
# 1) Missing token should fail
curl -i "https://example.com/agents/v1/health"
# 2) Query token should fail by default
curl -i "https://example.com/agents/v1/health?apiToken=$PLUGIN_AGENTS_API_TOKEN"
# 3) Header token should pass
curl -i -H "Authorization: Bearer $PLUGIN_AGENTS_API_TOKEN" \
"https://example.com/agents/v1/health"
# 4) Non-live entries should require elevated scope
curl -i -H "Authorization: Bearer $PLUGIN_AGENTS_API_TOKEN" \
"https://example.com/agents/v1/entries?status=all&limit=1"
# 5) Control policy list (only when PLUGIN_AGENTS_REFUND_APPROVALS_EXPERIMENTAL=true)
curl -i -H "Authorization: Bearer $PLUGIN_AGENTS_API_TOKEN" \
"https://example.com/agents/v1/control/policies"
# 6) Run local regression script
./scripts/security-regression-check.sh https://example.com "$PLUGIN_AGENTS_API_TOKEN"Planned improvements include:
- Expanded filtering and pagination controls for existing read-only endpoints.
- Additional diagnostics for operational readiness and integration health.
- Broader OpenAPI coverage and schema detail improvements.
- Optional export/report formats for automation workflows.
- Action-adapter integration for control-plane execution side effects.
- Continued hardening of auth, rate limiting, and observability.
The formal contract for checkpoint-based sync is documented in INCREMENTAL_SYNC_CONTRACT.md.
Highlights:
cursor-first continuation semantics withupdatedSincebootstrap support.- Deterministic ordering (
updatedAt, thenid) and at-least-once replay model. - Tombstone/delete signaling through
GET /agents/v1/changes.