Changelog¶
Single source of truth
This page mirrors CHANGELOG.md at the root of the repository.
Changelog¶
All notable changes to signet are documented here.
The format is based on Keep a Changelog. Versioning follows SemVer, with the understanding that pre-1.0 minor versions may break the API.
Unreleased¶
0.1.4 — 2026-05-03¶
Documentation accuracy pass¶
This is a doc-only release so the README on PyPI's project page reflects what v0.1.3 actually shipped. No code changes.
- README "Honest scope" section rewritten. Removed stale "roadmap for v0.2" claims about ed25519 receipts, RFC 3161 anchoring, multi-process audit writers, and embeddings/completions — all shipped in v0.1.3. Restructured into "architectural boundaries" (things signet doesn't do because they belong elsewhere) + "When you need more than the OSS" (the legitimate Pro/Thornveil call for production-tuned LLM-judge calibration, behavioral fingerprinting, HSM integrations, compliance attestation, custom check development).
- README built-in-checks table updated to reflect the v0.1.3 PromptInjection improvements (NFKC, confusables fold, multi- encoding decoders).
SECURITY.mdthreat model updated: tamper-evidence now points at the actually-shippedRfc3161Anchor; receipt symmetry now points at the actually-shippedEd25519ReceiptSigner; multi- process writer warning now points at the actually-shippedFileLockingJsonlBackend. Stale "v0.2 supply-chain roadmap" removed.docs/architecture.md"two limits" section reframed as "two architectural choices" — both choices are now operator-config decisions, not future work.- 7 new check pages:
loopback_trust,rate_limit,regex_content,prompt_injection,token_budget,scope_drift,continuing_consent,tool_call_inspector. Each documents what the check does, configuration patterns, audit-row shapes, and known false-positive surface. The mkdocs nav now lists all 10 built-in checks.
0.1.3 — 2026-05-03¶
Added — Phase 1: bulletproof OSS¶
- Asymmetric receipt signing (ed25519) via
signet.server.receipt.Ed25519ReceiptSignerandsignet keys generate-ed25519CLI command. Verifiers hold only the public key and cannot forge. Optional deppip install signet-sign[ed25519]. - External anchor backends for tamper-proof audit chains.
signet.audit.anchor.AnchorBackendProtocol +NoopAnchor(default, byte-compat) +Rfc3161Anchor(FreeTSA / any free public RFC 3161 TSA, requires no extra deps). Anchor receipt embedded in entry metadata BEFORE the chain HMAC is computed, so the HMAC binds the receipt to the entry. - Multi-process safe audit writer via
signet.audit.backend.FileLockingJsonlBackend(fcntl on POSIX, msvcrt on Windows). Pair withHmacChain(cache_prev=False)to run uvicorn--workers N>1safely. - Endpoint coverage:
/v1/embeddingsand/v1/completionsare now gated through the full pipeline./v1/audio/*and/v1/images/*remain explicit 404s with a roadmap note (their non-JSON request shapes need their own check protocols). - PromptInjection obfuscation hardening: Unicode NFKC
normalization, Cyrillic / Greek / Cherokee confusables fold,
zero-width / bidi character stripping, "stretched" letter-spaced
text collapse (
i g n o r e→ignore), wider encoding decoders (URL-safe base64, base32, hex, ROT13). Module docstring documents the bypass surface still left open and points at production-tuned LLM-judge layer for the rest. - Auth integration recipes at
docs/integrations/auth.md— three concrete patterns (nginx + mTLS, FastAPI middleware + JWT, oauth2-proxy + OIDC) for putting real authentication in front of signet's owner-resolution gate.
Added — Phase 2: production-grade ops¶
/metricsPrometheus endpoint with counters:signet_requests_total{path},signet_pipeline_decisions_total{check, decision},signet_audit_chain_appends_total,signet_audit_anchor_failures_total{backend}, plus asignet_uptime_secondsgauge. No external dep — exposition format written manually.- CORS support via
ServerConfig.cors_allowed_origins(+ methods / headers / credentials / preflight-cache fields). Skipped entirely when origins is empty (default), so non-browser deployments incur zero overhead. - Per-check timeout via
Check.timeout_seconds. Pipeline wraps each hook call inasyncio.wait_for; timeout fails closed (BLOCK with a clear reason). Bounds external dependencies (LLM-judge calls, sandbox runners) so a stuck dependency cannot halt the proxy. - Graceful shutdown: lifespan exit waits up to
ServerConfig.shutdown_grace_seconds(default 10s) for in-flight streaming responses to drain before tearing down the upstream client. Audit rows for abandoned streams still write via the generator's finally block. signet audit count/signet audit tailCLI subcommands.audit count --by check|owner|decision|owner_type|stagefor incident-response counts;audit tail -n 50 --filter decision=blockfor log inspection. Both support--jsonfor scripting.signet audit verify --jsonmachine-readable output mode for CI cron consumers.- Redis-backed state stores:
signet.server.redis_session_store.RedisSessionStoreandsignet.checks.redis_rate_limit_state.RedisRateLimitState— drop-in replacements for the in-memory defaults when running multiple replicas. Optional deppip install signet-sign[redis]. --log-format jsonflag onsignet serveswitches stdlib logging output to structlog's JSON renderer (one JSON object per line). Wire to Loki/Datadog/ELK without changing signet code.
Documentation polish¶
- Docs site nav keeps Contributing / Security / Changelog
inside the site (mkdocs
pymdownx.snippetsmirrors the canonical root files; GitHub keeps auto-discovering the originals). README gets explicit "📚 Documentation: jeranaias.github.io/signet" link with PyPI / docs / license / Python badges at the top. CONTRIBUTING.mdinternal links upgraded to absolute GitHub URLs so they resolve cleanly in both the repo browser and the included docs-site page.- GitHub repo description corrected —
signetis a proper noun, not preceded by an article.
Tooling¶
pyproject.tomlextras:[ed25519],[redis],[prometheus].[all]includes them all.[dev]pulls them for dev-environment coverage.ruffper-file-ignores for the deliberately-non-ASCII confusables table inprompt_injection.py+ the obfuscation-attack tests.- Cross-platform mypy fix: file-locking implementation selected at
module import time via
sys.platformnarrowing — keeps mypy clean on both Linux and Windows.
Tests¶
- 242 unit + adversarial green. mypy
--strictclean. ruff lint clean. mkdocs--strictbuild clean. - New tests: ed25519 sign/verify roundtrip + verify-only-cannot-sign + alg-downgrade rejection + PEM-roundtrip; anchor receipt binding to chain HMAC + failing-anchor-recorded-as-failure + require_anchor_success raises; FileLockingJsonlBackend basic + two-instance shared-file safety; PromptInjection obfuscation- busting (5 homoglyph variants + ROT13 + URL-safe base64); embeddings/completions endpoint round-trips; Redis adapters (sessions + rate-limit) end-to-end via fakeredis.
0.1.2 — 2026-05-03¶
Added — UX polish from first-user smoketest¶
signet serve --devshorthand. Bundles--allow-ephemeral-key,--audit-log audit.jsonl, and--config pipeline.pyinto one flag. Each is only set if not otherwise specified. The most common local invocation drops from five flags to one.signet doctorcommand. Prints versions and probes endpoints:--upstream <url>checks the LLM upstream is reachable;--self <url>hits a running signet's/health,/version, and sends a no-owner refusal probe to confirm the gate is enforcing. Exits non-zero on any probe failure.signet audit show <entry-id>is the new (honest) name for whatsignet replaydid. Thereplayalias still works but prints a deprecation warning; it will be removed in v0.2 alongside the actual pipeline-replay feature.signet initwrites aclient_example.pyalongside the pipeline scaffold. New users no longer have to read the README to find out how to call signet from Python — both raw httpx andwrap_openaipatterns are demonstrated.signet serveprints the ephemeral HMAC key on startup when--allow-ephemeral-keyis in effect. Lets the user save it externally if they want to verify the audit log later instead of losing the key on shutdown.OwnerResolutionCheckrefusal hint now lists three concrete header examples (X-Commit-Owner: human:alice@example.com,X-Agent-Id: agent:nightly-syncer,X-Policy-Name: acme-default) instead of just naming the field shapes. Refusal payload also carries anexamplesarray.X-Signet-UpstreamandX-Signet-Upstream-Statusresponse headers on every reply so callers can finger-point upstream errors vs. signet errors at a glance. Configurable label via--upstream-label/SIGNET_UPSTREAM_LABEL(defaults to the upstream URL host).ServerConfig.upstream_labelfield exposed for embedded use.
Documentation¶
- README rewritten to lead with the "why you need this now" pitch: three concrete attack scenarios, then the architecture, then the honest-scope section. Designed so a CEO can read the first three paragraphs and a CTO can read the rest.
docs/architecture.mdopens with a one-paragraph layman summary that names the junior-employee analogy before diving into the technical section. Trust model and out-of-scope list cleaned up.docs/index.mdmatches the README's framing for the docs site landing page.docs/checks/owner_resolution.mdcorrected —X-Agent-Idbare values are no longer accepted (theagent:prefix is required, fixed during pre-release security review).
0.1.1 — 2026-05-03¶
Fixed¶
signet servestartup banner used→(U+2192) which crashes Python's default cp1252 stdout on Windows withUnicodeEncodeError. Caught immediately on the first post-publish smoketest. Replaced with->for portability across console code pages.
0.1.0 — 2026-05-03¶
First public release. Apache-2.0 OSS prior art for the gate-pattern thesis.
Hardened — pre-release adversarial review¶
Five rounds of self-review against a fresh "hater" lens before tagging, producing ~50 fixes across the audit chain, receipt format, proxy semantics, owner resolution, memory safety, and documentation honesty. None are wire-format breaking; some change response semantics in ways that match the documented intent.
Audit chain¶
HmacChain.appendnow holds an internalthreading.Lock; concurrent FastAPI requests can no longer fork the chain by reading the sameprev_hmactwice. Multi-process workers still need a custom backend (documented)._serialize_for_signingrejectsNaN/Infinity(allow_nan=False) and usesensure_ascii=Falseso the canonical form is deterministic.JsonlBackend.appendcallsos.fsyncafter every write by default (fsync_after_append=True); a crash mid-handler can no longer leave the chain shorter than the responses already returned.Key.__repr__redacts the secret. Earlier the default dataclass repr would print HMAC bytes into any log line that touched aKey.- Verifier docstring lists all four break kinds (
MISSING_KEY_IDwas missing from the module-level summary).
Receipts¶
- Receipt format now carries
alg=hmac-sha256. The verifier rejects receipts whosealgdoes not match the configured signer, blocking downgrade attacks against future ed25519. ReceiptSigneris now aProtocol; concreteHmacReceiptSignerships as the v0.1 default. Callers can pass their own signer toSignetApp(receipt_signer=...)for ed25519 or HSM-backed primitives.- Receipt symmetry caveat called out explicitly in the module docstring,
SECURITY.md, anddocs/architecture.md.
Proxy semantics¶
REDACTresults from ADMISSION now actually modify the request body before forwarding (multimodal vision content preserved) instead of returning 403.ESCALATEresults return202 Acceptedwith anaudit_entry_idfield instead of 403.- Audit
Decisionreflects the actual outcome (BLOCK / REDACT / ESCALATE / ALLOW one-to-one). Earlier any non-allow collapsed to BLOCK in the chain. - Inbound bodies are streamed with a configurable cap
(
SIGNET_MAX_REQUEST_BODY_BYTES, default 4 MiB). Anything larger gets a 413 before signet attempts JSON parsing. - Empty body returns explicit 400 instead of forwarding
{}and producing an opaque upstream 400. - Pipeline crashes at admission or forward write a synthetic audit row before returning 500/502; earlier the chain was silent on the most security-relevant events.
- Streaming generator wraps in try/finally and writes a terminal audit
row with
finish_reason="client_disconnect"when the caller bails mid-stream. RECORD-stage non-allow results now persist as their own audit rows instead of being silently discarded.- Sessions are now actually loaded —
_handle_chatcallssession_store.get_or_create + touchon every request that assertsX-Signet-Sessionand stashes theSessiononRequestContext.scratch["_session"]. Earlier the store was wired but nothing populated from it. - Unsupported OpenAI endpoints (
/v1/embeddings,/v1/completions,/v1/audio/*,/v1/images/*) return an explicit 404 with a note, not FastAPI's generic body. _extract_sse_contentcoalesces multi-linedata:per the WHATWG EventSource spec (consecutivedata:lines join with\n, blank line dispatches the event). Matters for OpenAI-compatible upstreams that stream multi-line events (LiteLLM, vLLM with prompt-streaming).
Owner resolution¶
- Header lookup is case-insensitive and strips whitespace.
- Precedence is documented and deterministic: already-resolved →
X-Commit-Owner→X-Agent-Id→X-Policy-Name. Both human and agent present? Human wins. - Empty principal after a
human:/agent:prefix is rejected.
Memory¶
InMemorySessionStoreandInMemoryRateLimitStateare now LRU-bounded (defaults: 10k sessions, 50k owner buckets). An attacker rotating session IDs / owner identities can no longer grow the stores without bound.ResponseContext.accumulated_textis bounded (default 1 MiB) viaextend_text. Long streams setaccumulated_text_truncatedand stop appending instead of doing O(N²) string growth on multi-MB responses.
Audit row contents¶
request_fingerprintis now populated (sha256 over raw request bytes, computed before any redaction). All audit rows from the same request share this value, letting downstream tools group entries without inventing a request_id.accumulated_text_truncatedandchunk_countpropagate intopipeline.completerows so consumers can flag entries where INSPECTION saw only a prefix.
Checks¶
ScopeDriftCheckmarkers are now overrideable via constructor; empty dict disables classification drift entirely. Default false-positive surface documented.PromptInjectionCheckdocstring lists the bypass surface explicitly: non-English, homoglyph (Cyrillic-lookalike), whitespace obfuscation, alt encodings, cross-turn attacks, adversarial suffixes — all known gaps.SandboxResult.is_safe()keyword list explicitly marked as a placeholder; users should pass a realpolicy=callable.TribunalCheck.inspect_tool_callswitches togather(return_exceptions=True)so one judge crashing no longer cancels the other mid-flight (which leaked the surviving HTTP connection and discarded a usable verdict). Reuses onehttpx.AsyncClientacross calls.
CLI¶
signet initwrites a.gitignore(.env,.env.*,*.jsonl) alongside the scaffold so first-time users do not commit their HMAC secret or audit log on push. Existing.gitignoreis left alone.signet serve --config <path>prints a yellow warning that the flag executes arbitrary Python from the file. Function docstring also names it explicitly.signet replayhelp text is honest: it reads + prints the audit row but does NOT re-execute the pipeline. Replay against historical traffic is roadmap.signet replay <UUID>is now case-insensitive on the entry ID.signet serveprints the pipeline checks loaded at startup so operators can verify the configuration without reading the file._parse_hex_secretgives a clear message whenSIGNET_HMAC_SECRETis malformed: names the env var, accepts an optional0xprefix and surrounding whitespace, suggestsopenssl rand -hex 32, refuses secrets shorter than 16 bytes loudly.
SDK adapters¶
wrap_openai/wrap_anthropicraiseTypeErrorloudly when the SDK client lacks a writablebase_url, instead of silently leaving the original endpoint in place.
Hygiene¶
_iter_entry_pointsloses the pre-3.10 fallback (project pins>=3.11).
Threat model and supply chain¶
SECURITY.mdanddocs/architecture.mdcall out two real limits up front: owner identity is caller-asserted (not authenticated) and the HMAC chain is tamper-evident (not write-once). Both have v0.2 roadmap notes (asymmetric receipts, anchor backends).SECURITY.mdreporting channel is now GitHub private security advisory with the gmail backed up as fallback only.SECURITY.mdhardening list adds explicitchmod 0600guidance for the audit-log file (signet creates it with the OS umask).publish.ymlgenerates a CycloneDX SBOM and attaches it to each GitHub Release.SECURITY.mdcommits to v0.2 sigstore + SLSA.- README softens NIST 800-53 claim from "compatible" to "aligned with"
and is honest about endpoint coverage (only
/v1/chat/completionsin v0.1).
New tests¶
- 25-thread concurrent append confirms chain stays linked.
- NaN-in-metadata confirms loud rejection.
- Custom-marker
ScopeDriftCheckconfirms override path. - Receipt downgrade-attack confirms
algmismatch is rejected. - Body too large returns 413; empty body returns 400.
- Pipeline exception writes a synthetic audit row.
- ESCALATE returns 202 with
audit_entry_id. - REDACT preserves vision-style image parts.
- Audit rows from the same request share
request_fingerprint. accumulated_texttruncates and flags at the cap.- LRU eviction caps
RateLimitCheckmemory. - Lowercase / mixed-case headers, human-wins-over-agent precedence, whitespace-stripped values, empty-principal blocked.
- Multi-line SSE coalescing and
[DONE]handling. _parse_hex_secretrejects bad hex with a useful message; accepts whitespace + 0x prefix.- Session loaded into
ctx.scratch["_session"]on every request. signet initwrites.gitignoreand does not overwrite existing one.
Test count: 220 unit + adversarial green. mypy clean. ruff clean.
Added — core abstractions¶
signet.core.owner.Owner+OwnerTypeenum (human / agent / policy / unresolved)signet.core.audit.AuditEntry+Decisionenum (allow / block / redact / escalate)signet.core.check.CheckABC +CheckResultwith four hook timings (pre_request,inspect_response_chunk,inspect_tool_call,post_complete)signet.core.stage.Stagefour-tier hierarchy: ADMISSION / INSPECTION / COMMITMENT / RECORDsignet.core.contextrequest / response / tool-call context dataclassessignet.core.pipeline.Pipelinefail-closed sequenced executor
Added — HMAC audit chain¶
signet.audit.chain.HmacChain— append-and-sign coordinator with cached prev_hmacsignet.audit.verifier.ChainVerifier— distinguishes SELF_MISMATCH / LINK_MISMATCH / UNKNOWN_KEY / MISSING_KEY_ID break kindssignet.audit.keyring.KeyRing— multi-era key management for verification across rotationssignet.audit.backend.JsonlBackend— default append-only JSONL storage- Tamper-detection tests covering modify, delete, reorder, forge-insert, key rotation
Added — 10 built-in checks¶
OwnerResolutionCheck(ADMISSION) — refuse if no resolvable commit ownerLoopbackTrustCheck(ADMISSION) — auto-resolve owner for loopback + Tailscale CGNATRateLimitCheck(ADMISSION) — per-owner token bucket with pluggable state backendRegexContentCheck(ADMISSION) +RegexOutputCheck(INSPECTION) — block / redact patternsClassificationGateCheck(ADMISSION) — 5-level UNCLASS → TS/SCI gatePromptInjectionCheck(ADMISSION) — pattern + heuristic + base64-decoded scanTokenBudgetCheck(ADMISSION + RECORD) — per-owner output-token quota with reconciliationScopeDriftCheck(INSPECTION) — token / character / classification-marker driftContinuingConsentCheck(INSPECTION) — periodic mid-stream owner-authority revalidationToolCallInspectorCheck(COMMITMENT) — risk-tier gating + tool allowlist
Added — HTTP proxy¶
signet.server.app.SignetApp— FastAPI proxy with /health, /version, POST /v1/chat/completions- SSE streaming with mid-stream abort + trailer event on INSPECTION block
signet.server.config.ServerConfig— env-loadable runtime configsignet.server.session.Session+SessionStore— cross-request statesignet.server.receipt.ReceiptSigner—X-Signet-ReceiptHMAC-signed decision summary, offline-verifiable by callers
Added — SDK adapters¶
signet.adapters.openai.wrap_openai— in-place reconfigure of openai SDK clientssignet.adapters.anthropic.wrap_anthropic— same shape for Anthropic SDKsignet.adapters.langchain.SignetCallbackHandler— LangChain-shaped observer surfacing receipts and refusal payloads- 3 runnable example scripts in
examples/
Added — plugin interface¶
signet.plugins.discover+load_by_name— entry-point-based discovery (group:signet.checks)signet.plugins.tribunal.TribunalCheck— reference dual-judge dissent (caller supplies judge endpoints)signet.plugins.sandbox.SandboxPreviewCheck— reference preview-before-commit (caller supplies sandbox runner)
Added — CLI¶
signet serve— run the FastAPI proxysignet audit verify— walk an HMAC-chained log and report tamperingsignet replay— display the audit row for a given entry IDsignet init— scaffold a starter pipeline + .env
Added — tests¶
- ~250 unit tests covering all 10 checks, the chain, the proxy, the adapters, the plugins, and the CLI
- ~25 adversarial bypass tests across 6 attack categories
- Integration suite targeting local Ollama + remote RigRun (skipped when endpoints not reachable)
Added — docs¶
- README with one-paragraph what-it-is + quickstart
docs/architecture.mdcovering 4-stage hierarchy, continuing-consent + scope-drift patterns, trust model, what is intentionally not in scopedocs/checks/owner_resolution.md,docs/checks/classification_gate.mddocs/plugin_dev.mdwalkthrough- CONTRIBUTING + CODE_OF_CONDUCT + SECURITY (with explicit threat model)
Added — CI¶
- Ruff lint + format + mypy --strict on every push
- Test matrix: Python 3.11 / 3.12 / 3.13 × Linux / macOS / Windows = 9 jobs per push
- mkdocs-material site builds + deploys to GitHub Pages