Search by

rennf93 / guard-agent-php

rennf93

PHP telemetry agent for the guard-core ecosystem: buffers security events, metrics, and agent status locally and ships them to the Guard Core App ingestion API with at-least-once semantics

Package info

github.com/rennf93/guard-agent-php

pkg:composer/rennf93/guard-agent-php

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v3.0.2 2026-09-23 23:17 UTC

README

PHP telemetry agent for the 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.

Docs: https://rennf93.github.io/guard-agent-php/

This completes the agent-per-language lineup: guard-agent (Python), guardagent (TypeScript), guard-agent-rs (Rust), guard-agent-go (Go), and now guard-agent-php (PHP). Like the other ports it is a library, not a process: it never throws into the host request path, and every telemetry failure is a log line plus a counter bump.

Install

composer require rennf93/guard-agent-php

Requires 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.

Usage

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();
});

Long-running workers (CLI daemons, RoadRunner, Swoole-style loops)

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 and pushes status reports on the status 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

Reliability semantics

  • 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/422 rejections are exempt (they are batch-level, not health signals). 429 counts; Retry-After is honored up to a 300s cap.
  • Permanent rejection: 400/404/422 drop the batch (logged and counted, never retried, never thrown). A 413 splits the batch in half and retries each half; a singleton that still exceeds the cap is dropped.
  • Partial success is failure: a 200 with success: false or a non-empty errors[] requeues the whole batch.
  • Degraded state: the status report reads degraded when 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 block overflow policy). See AGENTS.md for the full contract.

The uncompressed-signature note

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. This differs from the Python/TypeScript/Go agents, which sign the post-gzip bytes and silently fail verification whenever compression kicks in; the PHP agent signs what the server actually verifies.

Persistence

With redis configured, every accepted item is written to Redis under a globally-unique key ({prefix}:agent_events:event_<nanos>_<8hex>, TTL 3600s) on enqueue; 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.

Documentation

  • AGENTS.md (mirrored byte-identically in CLAUDE.md): architecture, configuration reference, reliability semantics, testing.
  • src/.agents/skills/guard-agent-php/SKILL.md: agent-oriented quick reference.

Related projects

  • 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.

License

MIT. See LICENSE.