Signals API
Every worker, ingest handler, and hub-side scanner writes Trust-Score-relevant
findings to a single table: signal_streams. Trust Score, Anomaly
Correlation, Compliance Pareto, and every dashboard read from there. No
more per-feature silo tables.
Wire-format conventions
| Field | Shape |
|---|---|
source | lowercase_snake_case — cert_scan, dns_check, … |
subject_type | lowercase_snake_case — agent, endpoint, domain, dependency, … |
signal_key | dotted lowercase — cert.expiry_days, dns.dmarc_present |
signal_value | JSONB; convention: {"value": <native>, …metadata} |
severity | NULL or one of `info |
observed_at | TIMESTAMPTZ; partition column on the hypertable |
ttl_seconds | NULL = sticky |
Postgres CHECK constraints validate every regex so a typo surfaces
immediately as 23514; signals.Emitter.validate() runs the same check in
Go so the error lands at the call site.
Go interface
import "github.com/gotrust/monsys/hub/api/signals"
emitter := signals.NewSQLEmitter(pool)
err := emitter.Emit(ctx, tenantID, signals.Signal{ Source: "my_check", SubjectType: "agent", SubjectID: agentID.String(), Key: "my.finding", Value: map[string]any{"value": 42, "extra": "context"}, Severity: signals.SeverityHigh, ObservedAt: time.Now().UTC(),})EmitBatch uses pgx.CopyFrom; all-or-nothing: one invalid signal
rejects the whole batch (see signals/emitter.go rationale).
Agent-emitted signals
The agent can emit signals via a dedicated agent_signals payload type
(Phase 1.4–1.6). The hub validates:
source∈ allowlist (backup_check,clock_check,endpoint_posture)subject_type∈ allowlist (agent)subject_idoverwritten with the caller agent_id whensubject_type=agent
A compromised agent can never claim findings for another agent in its tenant.
Source → Trust Score category
Mapping in hub/api/trust_score.SourceToCategory. Adding a new source =
one entry; without that entry the source counts zero points in Trust
Score.
RLS
signal_streams has ENABLE ROW LEVEL SECURITY with
USING tenant_id = current_setting('app.current_tenant', true)::UUID.
Defense in depth. Every hub-side query MUST still include an explicit
WHERE tenant_id = $1 — set_config(..., true) does not survive pgxpool’s
connection recycling. See docs/internal/decisions/2026-Q2-connected-dashboards.md D-0003.
Compression is not enabled on signal_streams (Timescale 2.18+ refuses
the RLS+columnstore combination). Retention via drop_chunks, default 180d.