Usage
Lifecycle
import { GuardAgent } from "guardagent";
const agent = new GuardAgent(config); // validates config, restores install id
await agent.start(); // starts flush, status, and rules loops
agent.sendEvent(ev); // enqueue a security event
agent.sendMetric(m); // enqueue a security metric
await agent.flushBuffer(); // force a flush cycle
await agent.getStatus(); // current agent status snapshot
agent.getStats(); // buffer occupancy, drops, retries, rules
await agent.getDynamicRules(); // latest dynamic rules (TTL-cached) or null
await agent.healthCheck(); // true when the ingestion API is reachable
await agent.stop(); // final flush, stop loops, close Redis
await agent.close(); // stop plus a final status report
await agent.initializeRedis(handler); // attach a Redis handler for persistence
new GuardAgent(...) validates the config up front and throws
ConfigError listing every problem. start is idempotent-guarded;
stop flushes what fits and confirms persisted records for everything
it successfully sends. No exported method throws into the host request
path: telemetry failures are logged, counted, and reported through the
optional onError hook.
Events and metrics
SecurityEvent and SecurityMetric mirror the ingestion API's
BatchTelemetryRequest payload. The wire format is snake_case with
ISO-8601 timestamps and UUID idempotency keys; the agent accepts both
camelCase and snake_case input and converts at the transport boundary.
timestamp defaults to now and idempotencyKey to a fresh UUID (it
deduplicates retries server-side, so keep it stable when you retry
yourself). Field-by-field descriptions live on the classes in
src/models.ts.
Known eventType values are exported as KNOWN_EVENT_TYPES, and the
accepted metric types as METRIC_TYPES; the server accepts any
eventType string.
Buffering and overflow
Each kind (events, metrics) has its own buffer (bufferSize, default
100). When a buffer fills, bufferOverflowPolicy decides:
| Policy | Behavior |
|---|---|
drop (default) |
evicts the oldest item of the same kind, counts a drop |
block |
waits for a flush to free a slot; durability over the new writer |
raise |
returns a BufferFullError without buffering |
A combined occupancy at or above highWatermarkRatio (default 0.8)
triggers an early flush; at most maxConcurrentFlushes early flushes run
at once.
Delivery semantics
- Flushes run on
flushInterval(default 30s), on watermark, and on demand - A failed batch retries up to
retryAttemptswith exponential backoff (backoffFactor), honoringRetry-Afteron 429 (default 60s, capped at 300s) - A 413 (decompressed body over 262144 bytes) splits the batch or drops its oldest item rather than retrying a permanently oversized payload
- 400, 404, and 422 are permanent: the batch is dropped, not retried
- A 200 with
success: false(or a non-emptyerrorsarray) is a partial failure: the batch is requeued for retry, not acknowledged - A per-kind failure streak backs off between attempts (base
flushInterval, exponential, capped at 300s); the streaks of events and metrics are independent - The circuit breaker opens after 5 consecutive failures and half-opens to probe recovery
Signing
When payloadSigningSecret is set, every request carries
X-Payload-Signature: v1=<hex hmac-sha256>. The signature covers the
UNCOMPRESSED JSON body; the server decompresses and verifies afterward.
Gzip (compressionEnabled) only affects the wire bytes.
Encryption
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, 12-byte nonce prefix, 16-byte auth tag,
padded urlsafe base64). An invalid key raises at startup; the agent never
falls back to plaintext. Signing and compression apply to the envelope
body exactly as they do to plaintext batches.
Redis persistence
With a Redis handler attached (via initializeRedis, or redis.url in
config with the ioredis peer installed), every buffered item is
persisted under a globally-unique key before the send attempt and
confirmed (deleted) on success, so a crash between buffer and network
loses nothing. Persisted records carry a 3600s TTL and are reloaded at
startup. See Configuration.
Dynamic rules
start() also runs a rule-sync loop: every dynamicRuleInterval seconds
(default 300, minimum 60) the agent polls GET /api/v1/rules and caches
the payload in a DynamicRules object. getDynamicRules() serves the
cached copy while it is younger than the server-supplied ttl
(seconds), refetches once it expires, and returns null when nothing
has been fetched yet or the server has no payload; a failed poll keeps
the last good rules cached for the next cycle and never throws.
The wire payload mirrors the Python agent's DynamicRules model
(guard_agent/models.py): snake_case JSON with ISO-8601 timestamps,
endpoint_rate_limits as { endpoint: [requests, window] } pairs, and
blocked_cloud_providers as an array. It is what the guard-core engine
consumes through
guard_core/handlers/dynamic_rule_handler.py (get_dynamic_rules),
covering IP black/whitelists and ban duration, country lists, global and
per-endpoint rate limits, blocked cloud providers and user agents,
suspicious patterns, the enable_* overrides with the auto_ban_*
thresholds, and the emergency-mode triple with the optional rule
message.
getStats() reports rulesFetched, cachedRules, rulesLastUpdate,
and loopFailures.rules. The rules loop only runs when projectId is configured.