PHP telemetry agent for the [guard-core](https://github.com/Guard-Core/guard-core) ecosystem. It buffers security events, performance metrics, and agent status in memory (optionally persisted to Redis) and ships them to the Guard Core App ingestion API with at-least-once delivery semantics: nothing acknowledged is lost, nothing unacknowledged is forgotten.
Website · Docs · Playground · Dashboard · Discord
Guard Core is the Python engine. Framework adapters are thin wrappers that translate native request/response types into Guard Core's protocols. The telemetry agents ship security events and metrics to the monitoring backend. Parallel engine implementations exist for Go, PHP, TypeScript (on npm), and Rust (on crates.io) - all ports of the same reference semantics, conformance-tested against the shared adversarial corpus.
| Package | Role | PyPI |
|---|---|---|
| guard-core | Framework-agnostic security engine | |
| guard-agent | Telemetry agent | |
| fastapi-guard | FastAPI / Starlette adapter | |
| flaskapi-guard | Flask adapter | |
| djapi-guard | Django adapter | |
| tornadoapi-guard | Tornado adapter |
Go modules published via GitHub releases. Production-ready.
| Package | Role | Release |
|---|---|---|
| guard-core-go | Go engine | |
| nethttp-guard | net/http adapter | |
| gin-guard | Gin adapter | |
| echo-guard | Echo (v4) adapter | |
| fiber-guard | Fiber (v3) adapter | |
| guard-agent-go | Telemetry agent |
Published on Packagist under the rennf93 vendor. Production-ready.
| Package | Role | Packagist |
|---|---|---|
| guard-core-php | PHP engine | |
| laravel-guard | Laravel adapter | |
| symfony-guard | Symfony adapter | |
| psr15-guard | PSR-15 adapter | |
| slim-guard | Slim 4 adapter | |
| guard-agent-php | Telemetry agent |
Published under the @guardcore npm scope; source in the guard-core-ts monorepo. Production-ready.
| Package | Role | npm |
|---|---|---|
| @guardcore/core | Core engine | |
| @guardcore/express | Express adapter | |
| @guardcore/nestjs | NestJS adapter | |
| @guardcore/fastify | Fastify adapter | |
| @guardcore/hono | Hono (edge) adapter | |
| guardagent | Telemetry agent |
Published on crates.io. Production-ready.
| Package | Role | crates.io |
|---|---|---|
| guard-core-engine | Core engine crate | |
| guard-core-rs | Facade crate (consumer entry point) | |
| actix-guard-rs | Actix Web adapter | |
| axum-guard-rs | Axum adapter | |
| tower-guard-rs | Tower adapter | |
| rocket-guard-rs | Rocket adapter | |
| guard-agent-rs | Telemetry agent |
| Package | Role | PyPI |
|---|---|---|
| guard-core-mcp | MCP server: config validation, docs search, detection sandbox |
composer require rennf93/guard-agent-phpRequires PHP ^8.2, ext-json, and ext-curl. Crash-recovery persistence to Redis works out of the box through a built-in stream client (no extra extension needed); if you already run ext-redis or predis/predis, adapters for both are provided.
The agent is host-driven: PHP has no background threads, so flush timers run when you call the agent, not behind your back.
use RenzoFranceschini\GuardAgent\Config\AgentConfigResolver;
use RenzoFranceschini\GuardAgent\GuardAgent;
$agent = new GuardAgent(AgentConfigResolver::resolve([
'apiKey' => $_ENV['GUARD_API_KEY'],
'endpoint' => 'https://api.guard-core.com',
'projectId' => 'my-project',
'payloadSigningSecret' => $_ENV['GUARD_SIGNING_SECRET'] ?? null,
]));
$agent->start();
// From anywhere in your request path: never throws, never blocks (default drop policy).
$agent->sendEvent([
'event_type' => 'penetration_attempt',
'ip_address' => $clientIp,
'endpoint' => '/login',
'method' => 'POST',
'metadata' => ['rule' => 'sqli-union-select'],
]);
$agent->sendMetric([
'metric_type' => 'response_time',
'value' => 0.023,
'endpoint' => '/login',
]);
// Ship telemetry at the end of the request (kernel.terminate in Symfony,
// register_shutdown_function in plain PHP, terminate in Laravel).
register_shutdown_function(static function () use ($agent): void {
$agent->flushBuffer();
$agent->stop();
});Do not reach for pcntl_alarm or extension timers: drive the agent from your own loop with tick(), which flushes when the high-watermark or the flush interval is reached, pushes status reports on the status interval, and refreshes the dynamic rules on the dynamic rule interval.
$agent->start();
while (true) {
$agent->tick(); // cheap: no network I/O unless a trigger fires
doWork();
usleep(1_000_000);
}
$agent->stop(); // final flush, releases Redis- At-least-once handshake:
flushBuffer()drains each kind, sends it, and only on success confirms the batch (deleting any Redis records). Transient failures requeue the batch at the front of the buffer in its original order; under pressure the tail (newest items) is evicted and its records confirmed. - Per-kind backoff: after a failed flush the kind is gated for
min(flushInterval * 2^(streak - 1), 300)seconds before the next attempt. - Circuit breaker: 5 consecutive transport failures in 60 seconds open the circuit;
400/404/413/422rejections are exempt (they are batch-level, not health signals).429counts;Retry-Afteris honored up to a 300s cap. - Permanent rejection:
400/404/422drop the batch (logged and counted, never retried, never thrown). A413splits the batch in half and retries each half; a singleton that still exceeds the cap is dropped. - Partial success is failure: a
200withsuccess: falseor a non-emptyerrors[]requeues the whole batch. - Degraded state: the status report reads
degradedwhen the circuit breaker is open, the buffer is at or above 90 percent occupancy, or the lifetime failure rate exceeds 10 percent. - Failure isolation: no public method ever throws into the host path (the one deliberate, opt-in exception is the
blockoverflow policy). SeeAGENTS.mdfor the full contract.
When payloadSigningSecret is set, the transport sends X-Payload-Signature: v1=<hex> where <hex> is hash_hmac('sha256', <uncompressed JSON body>, <secret>). The Guard Core App ingestion API verifies the signature after decompressing the body (its GzipRequestMiddleware inflates Content-Encoding: gzip request bodies before the telemetry router runs), so the HMAC must always cover the uncompressed JSON bytes. The Python, TypeScript and Rust agents sign the same uncompressed bytes.
getDynamicRules() returns the SaaS rule document from GET /api/v1/rules as a DynamicRules value object (snake_case wire keys, mirroring the Python agent's pydantic model). The fetched copy is cached in memory and served while it is younger than its own ttl (seconds, default 300); a failed fetch returns null while the last good rules stay cached for the next poll, and a thrown transport error falls back to the cached copy, so a rules outage never surfaces as a hard failure. The tick() loop refreshes the cache every dynamicRuleInterval seconds (default 300, minimum 60). Fetch statistics surface in getStats() as rulesFetched, cachedRules, rulesLastUpdate, and loopFailures.rules.
When projectEncryptionKey is set (a urlsafe-base64-encoded 256-bit key issued by the core backend), event and metric batches are encrypted with AES-256-GCM and POSTed to /api/v1/events/encrypted as {encrypted_payload, batch_id, agent_version, guard_version, guard_core_version}; the wire format is byte-compatible with the Python agent (canonical JSON plaintext with sort_keys and ensure_ascii, 12-byte nonce prefix, 16-byte auth tag, padded urlsafe base64). An invalid key raises EncryptionConfigException at startup; the agent never falls back to plaintext. Signing and compression apply to the envelope body exactly as they do to plaintext batches.
With redis configured, every accepted item is written to Redis under a globally-unique key ({prefix}:agent_events:event_<epoch nanos>_<8hex>, TTL 3600s) by persistPending(), which flushBuffer() runs after its send attempt (so delivered items are never written) and flushIfNeeded()/tick() run between flushes; adding an item never waits on Redis. If the process ends before the next flush or tick, a shutdown function the agent registers once when persistence is attached writes what is still pending, so a normal end, exit(), an uncaught exception, a fatal error, out of memory and max_execution_time do not lose events (bin/test_shutdown.php proves each against a real Redis). Events are lost only when PHP can run no code: SIGKILL (the OOM killer, kill -9), and SIGTERM unless the host installs its own handler (PHP skips shutdown functions on SIGTERM; with ext-pcntl a host handler that calls exit() makes it durable, and the agent installs none because it would override the host's). The shutdown write runs after the response is built, so a host that wants the client released first calls fastcgi_finish_request() itself; the agent does not, because that would cut off output and shutdown functions registered after it. On start() the buffer reloads whatever a previous process left behind. Every Redis failure is fail-open: logged, counted, and after 3 consecutive write failures paused for a 30s cooldown, so an unhealthy Redis cannot tax the request path. The TTL is the backstop: at worst a lost confirmation duplicates a reload, it never loses data.
AGENTS.md(mirrored byte-identically inCLAUDE.md): architecture, configuration reference, reliability semantics, testing.src/.agents/skills/guard-agent-php/SKILL.md: agent-oriented quick reference.
- guard-core: the framework-agnostic engine library.
- guard-agent (Python), guardagent (TypeScript), guard-agent-rs (Rust), guard-agent-go (Go): sibling agents.
- guard-core-app: the SaaS platform this agent reports to.
MIT. See LICENSE.