Documentation
¶
Overview ¶
Package secretnat implements SecretNAT — also known by its alias ReSet (Reversible Secret Translation) — a transparent, reversible secret translation layer that sits between rysh and the LLM provider.
Outbound text (prompts, conversation history, tool inputs/outputs, system prompts) is scanned for secrets; every real secret is replaced with a format-preserving synthetic token (e.g. "sk_live_SNAT000001" or "${NAME}"). Inbound tokens are mapped back to real values only where a real value is required locally (tool execution). The LLM provider never sees a real secret, and the mapping table lives exclusively in memory — it is never persisted, logged, or exported.
Two tiers of secrets exist:
- Known tier: secrets registered in the host's secret store are replaced with stable "${NAME}" tokens (the same grammar as the store's Expand), restorable across process restarts.
- Detected tier: pattern-matched secrets get "<prefix>SNAT<%06d>" synthetic values whose mapping dies with the process.
Index ¶
- func Restore(text string, known *KnownSet, table *MappingTable) (string, int)
- func Sanitize(text string, known *KnownSet, reg *Registry, table *MappingTable, ...) (string, int)
- func Wrap(p provider.AgenticProvider, s SessionHandle) provider.AgenticProvider
- type CustomDetector
- type Detector
- type Generator
- type KnownSecret
- type KnownSet
- type Manager
- func (m *Manager) CloseSession(convID string)
- func (m *Manager) DetectorNames() []string
- func (m *Manager) Enabled() bool
- func (m *Manager) Mode() Mode
- func (m *Manager) RestoreDisplay() bool
- func (m *Manager) Session(convID string) *Session
- func (m *Manager) SetEnabled(v bool)
- func (m *Manager) SetMode(mode Mode)
- func (m *Manager) Stats() ManagerStats
- func (m *Manager) SweepExpired() int
- func (m *Manager) UpdateKnownSecrets(secrets []KnownSecret)
- type ManagerStats
- type MappingEntry
- type MappingTable
- func (t *MappingTable) Entries() []MappingEntry
- func (t *MappingTable) IsToken(s string) bool
- func (t *MappingTable) LastUsed() time.Time
- func (t *MappingTable) MarshalJSON() ([]byte, error)
- func (t *MappingTable) MaxTokenLen() int
- func (t *MappingTable) PerDetector() map[string]int
- func (t *MappingTable) RestoreAll(text string) (string, int)
- func (t *MappingTable) RestoredCount() int
- func (t *MappingTable) RevealToken(token string) (string, bool)
- func (t *MappingTable) Size() int
- func (t *MappingTable) TokenFor(value, detector string, mint func(seq int) string) string
- func (t *MappingTable) Tokens() []string
- type Match
- type Mode
- type NATProvider
- func (n *NATProvider) Complete(ctx context.Context, prompt string) (string, error)
- func (n *NATProvider) CompleteWithTools(ctx context.Context, conversation []provider.ConversationTurn, ...) (*provider.AgenticResponse, error)
- func (n *NATProvider) Name() string
- func (n *NATProvider) Unwrap() provider.AgenticProvider
- func (n *NATProvider) WithMaxTokens(maxTokens int) provider.AgenticProvider
- func (n *NATProvider) WithModelEffort(model, effort string) provider.AgenticProvider
- type Options
- type Registry
- type Session
- func (s *Session) Enabled() bool
- func (s *Session) ID() string
- func (s *Session) NewStreamRestorer() *StreamRestorer
- func (s *Session) Restore(text string) string
- func (s *Session) RestoreDisplay() bool
- func (s *Session) RestoreJSON(raw json.RawMessage) json.RawMessage
- func (s *Session) Reveal(token string) (value, source string, ok bool)
- func (s *Session) Sanitize(text string) string
- func (s *Session) SanitizeJSON(raw json.RawMessage) json.RawMessage
- func (s *Session) SetOverride(v *bool)
- func (s *Session) Stats() SessionStats
- type SessionHandle
- type SessionStats
- type StreamRestorer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Restore ¶
func Restore(text string, known *KnownSet, table *MappingTable) (string, int)
Restore replaces synthetic tokens in text with real values:
- minted detected-tier tokens (exact table hits, longest first);
- "${NAME}" references whose NAME exists in the known set — a model-written "${HOME}" stays untouched unless HOME is actually a registered secret.
Returns the restored text and the number of replacements.
func Sanitize ¶
func Sanitize(text string, known *KnownSet, reg *Registry, table *MappingTable, gen *Generator) (string, int)
Sanitize translates every secret in text into its synthetic token:
pass 1 — known tier: registered secret values → "${NAME}", longest
value first;
pass 2 — detected tier: registry matches → minted tokens from the
mapping table (deterministic per value).
Sanitize is idempotent: tokens produced by either pass are never re-translated (Sanitize(Sanitize(x)) == Sanitize(x)), which also keeps repeated sanitization byte-stable for prompt caching. Returns the sanitized text and the number of replacements.
func Wrap ¶
func Wrap(p provider.AgenticProvider, s SessionHandle) provider.AgenticProvider
Wrap decorates p with s. Identity when p or s is nil. Re-wraps (never stacks) when p is already a NATProvider.
Types ¶
type CustomDetector ¶
type CustomDetector struct {
Name string `yaml:"name" json:"name"`
Pattern string `yaml:"pattern" json:"pattern"`
// Prefix is used for the synthetic token in semantic mode.
Prefix string `yaml:"prefix" json:"prefix"`
}
CustomDetector is the user-facing config shape for plugging in an extra regex detector without writing code (config: snat.custom_detectors).
type Detector ¶
type Detector interface {
// Name identifies the detector (stable, lowercase, e.g. "github").
Name() string
// Detect returns all secret spans in text. Spans must not overlap
// within a single detector's result.
Detect(text string) []Match
// SyntheticPrefix is the default format-preserving prefix for tokens
// minted from this detector's matches ("" when not applicable).
SyntheticPrefix() string
// Validate post-filters a candidate secret value (entropy / length /
// shape checks). Candidates failing Validate are not translated.
Validate(candidate string) bool
}
Detector is the plugin interface for secret detection. Implementations must be safe for concurrent use and should detect in linear time (RE2 regexps satisfy this).
type Generator ¶
type Generator struct {
// contains filtered or unexported fields
}
Generator mints synthetic tokens for detected-tier secrets. Tokens embed the fixed "SNAT" marker plus a fixed-width sequence number: distinctive enough that no built-in detector re-matches its own output (idempotent sanitize) and no real-world text collides with it.
func NewGenerator ¶
NewGenerator returns a generator for the given mode.
func (*Generator) Synthetic ¶
Synthetic mints the token for sequence number seq from a match's shape. In semantic mode the match's own Synthetic template wins (e.g. JWT's three-segment shape), then its format-preserving prefix, then a detector-name fallback. Private mode always yields SECRET_TOKEN_<n>.
type KnownSecret ¶
KnownSecret is one (name, value) pair from the host's secret store.
type KnownSet ¶
type KnownSet struct {
// contains filtered or unexported fields
}
KnownSet is an immutable snapshot of the known-tier secrets. Rebuild (via NewKnownSet) and atomically swap on every secret-store mutation.
func NewKnownSet ¶
func NewKnownSet(secrets []KnownSecret) *KnownSet
NewKnownSet builds a snapshot from the given pairs, dropping entries with empty names or values too short to translate safely.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the process-wide SecretNAT state: options, detector registry, the known-secret snapshot, and one Session per conversation (pane / agent / humanoid). Safe for concurrent use.
func NewManager ¶
NewManager builds a Manager from options. Custom detector patterns are compiled eagerly; an invalid pattern fails construction.
func (*Manager) CloseSession ¶
CloseSession drops convID's mapping table (pane closed). Real values in the table become unreachable immediately.
func (*Manager) DetectorNames ¶
DetectorNames lists active detectors (##snat status).
func (*Manager) RestoreDisplay ¶
RestoreDisplay reports whether displayed output should be restored.
func (*Manager) Session ¶
Session returns the session for convID, creating it on first use. Each conversation's mapping table is isolated; mappings never leak across sessions. Session creation opportunistically sweeps TTL-expired sessions.
func (*Manager) SetEnabled ¶
SetEnabled flips the session-wide default (##snat on|off --session).
func (*Manager) Stats ¶
func (m *Manager) Stats() ManagerStats
Stats aggregates value-free counters across sessions.
func (*Manager) SweepExpired ¶
SweepExpired drops sessions idle longer than MappingTTL. No-op when TTL is 0. Returns the number of sessions dropped.
func (*Manager) UpdateKnownSecrets ¶
func (m *Manager) UpdateKnownSecrets(secrets []KnownSecret)
UpdateKnownSecrets atomically swaps the known-tier snapshot. Call from the secret store's owner on every mutation (values are held only in memory, inside the snapshot).
type ManagerStats ¶
type ManagerStats struct {
Enabled bool
Mode Mode
RestoreDisplay bool
KnownSecrets int
Sessions int
Detected int
Restored int
PerDetector map[string]int
}
ManagerStats is the value-free aggregate metrics view (##snat status).
type MappingEntry ¶
MappingEntry is the value-free view of one translation, safe to display (##snat list) and log: it carries the synthetic token and metadata but NEVER the real value.
type MappingTable ¶
type MappingTable struct {
// contains filtered or unexported fields
}
MappingTable is the in-memory, per-conversation bidirectional map between real secret values and synthetic tokens.
Security invariants:
- lives only in memory; MarshalJSON fails loudly so the table can never ride along an accidentally-serialized struct into KV or logs;
- real values are reachable only via RestoreAll / lookups, never via Entries()/stats.
func (*MappingTable) Entries ¶
func (t *MappingTable) Entries() []MappingEntry
Entries returns the value-free listing for display, ordered by token.
func (*MappingTable) IsToken ¶
func (t *MappingTable) IsToken(s string) bool
IsToken reports whether s is a token minted by this table.
func (*MappingTable) LastUsed ¶
func (t *MappingTable) LastUsed() time.Time
LastUsed returns the last time the table translated or restored anything.
func (*MappingTable) MarshalJSON ¶
func (t *MappingTable) MarshalJSON() ([]byte, error)
MarshalJSON always fails: the mapping table must never be serialized. A loud error beats a silent secret leak into JetStream KV or a log line.
func (*MappingTable) MaxTokenLen ¶
func (t *MappingTable) MaxTokenLen() int
MaxTokenLen returns the longest minted token's length (streaming restorers use it to bound their hold-back window).
func (*MappingTable) PerDetector ¶
func (t *MappingTable) PerDetector() map[string]int
PerDetector returns detection hit counts keyed by detector name.
func (*MappingTable) RestoreAll ¶
func (t *MappingTable) RestoreAll(text string) (string, int)
RestoreAll replaces every minted token in text with its real value, longest-token-first so no token that is a substring of another can cause a partial replacement. Returns the restored text and the number of replacements made.
func (*MappingTable) RestoredCount ¶
func (t *MappingTable) RestoredCount() int
RestoredCount returns the number of token→value replacements performed.
func (*MappingTable) RevealToken ¶
func (t *MappingTable) RevealToken(token string) (string, bool)
RevealToken returns the real value mapped to a detected-tier token, or ("", false). This is the ONLY value-returning accessor; it exists for the explicit, local-only "##snat get" reveal and must never be used on any outbound / persistence path.
func (*MappingTable) TokenFor ¶
func (t *MappingTable) TokenFor(value, detector string, mint func(seq int) string) string
TokenFor returns the synthetic token for value, minting one via mint on first sight. Deterministic within a table: the same value always yields the same token. mint receives the next sequence number.
func (*MappingTable) Tokens ¶
func (t *MappingTable) Tokens() []string
Tokens returns all minted tokens (no values), longest first.
type Match ¶
type Match struct {
// Start/End delimit the secret VALUE itself (not the surrounding
// context a detector may have matched, e.g. the "PASSWORD=" key of an
// env-style line or the "user:" part of a database URL).
Start, End int
// Type is the detector name that produced this match (e.g. "stripe").
Type string
// Confidence in [0,1]; when overlapping matches conflict, the leftmost-
// longest span wins first, then higher confidence.
Confidence float64
// Prefix is the format-preserving prefix carried into the synthetic
// token in semantic mode (e.g. "sk_live_", "ghp_"). Empty when the
// secret has no meaningful prefix (passwords, env values).
Prefix string
// Synthetic optionally overrides synthetic-token generation for this
// match (used e.g. by the JWT detector to preserve the three-segment
// shape). When nil the Generator's default format applies.
Synthetic func(seq int) string
}
Match is one detected secret span within a scanned text.
type Mode ¶
type Mode string
Mode selects the synthetic-token style.
const ( // ModeSemantic preserves the credential's visible type so the LLM keeps // full context: "sk_live_SNAT000042", "ghp_SNAT000007". ModeSemantic Mode = "semantic" // ModePrivate hides even the provider/type: "SECRET_TOKEN_042". Better // privacy, less context for the LLM. ModePrivate Mode = "private" )
type NATProvider ¶
type NATProvider struct {
// contains filtered or unexported fields
}
NATProvider decorates an AgenticProvider with outbound sanitization: every request's conversation, tool inputs, and system prompt are deep-sanitized (on copies — the caller's conversation is never mutated) before reaching the wire. It performs NO inbound restore: synthetic tokens deliberately stay in the assistant turns so real values never enter the persisted conversation. The orchestrator restores just-in-time at tool execution and (optionally) for display.
Sanitize is idempotent, so text already sanitized at source (tool results, user turns) passes through byte-identical — safe for prompt caching.
func (*NATProvider) Complete ¶
Complete sanitizes the single-turn prompt outbound. The response is returned as-is (tokens intact).
func (*NATProvider) CompleteWithTools ¶
func (n *NATProvider) CompleteWithTools( ctx context.Context, conversation []provider.ConversationTurn, tools []provider.ToolSpec, systemPrompt string, ) (*provider.AgenticResponse, error)
CompleteWithTools sanitizes a copy of the conversation and the system prompt, then delegates.
func (*NATProvider) Name ¶
func (n *NATProvider) Name() string
Name reports the inner provider's name (the decorator is transparent).
func (*NATProvider) Unwrap ¶
func (n *NATProvider) Unwrap() provider.AgenticProvider
Unwrap returns the decorated provider.
func (*NATProvider) WithMaxTokens ¶
func (n *NATProvider) WithMaxTokens(maxTokens int) provider.AgenticProvider
WithMaxTokens forwards the per-request max-tokens cap to the inner provider and re-wraps the result, so a capped seat can never unwrap the NAT layer. Unlike WithModelEffort there is NO silent fallback: when the inner provider lacks the seam, nil is returned per the MaxTokensOverridable contract so the caller fails loudly instead of sending a request with the cap silently dropped.
func (*NATProvider) WithModelEffort ¶
func (n *NATProvider) WithModelEffort(model, effort string) provider.AgenticProvider
WithModelEffort delegates the override to the inner provider and re-wraps the result, so per-run model/effort seats can never unwrap the NAT layer. When the inner provider doesn't support overrides, the receiver is returned unchanged (mirroring the orchestrator's fallback semantics).
type Options ¶
type Options struct {
// Enabled is the session-wide default; individual sessions may override
// (##snat on|off on a pane).
Enabled bool
// Mode selects semantic (type-preserving) or private tokens.
Mode Mode
// RestoreDisplay controls whether displayed LLM output has tokens
// restored to real values. Default false: the pane display buffer is
// persisted and forwarded to listeners, so restoring would re-leak.
RestoreDisplay bool
// MappingTTL expires idle per-conversation mappings; 0 = conversation
// lifetime (mappings die when the session closes or the process exits).
MappingTTL time.Duration
// DisabledDetectors removes built-in detectors by name.
DisabledDetectors []string
// CustomDetectors adds user-defined regex detectors.
CustomDetectors []CustomDetector
}
Options configures a Manager. Zero value = disabled.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds an ordered, fixed set of detectors. Order is significant: it is the deterministic tiebreak for equal-length overlapping matches, so a Registry must not be mutated after construction.
func NewDefaultRegistry ¶
func NewDefaultRegistry(disabled []string, custom []CustomDetector) (*Registry, error)
NewDefaultRegistry returns the built-in detector set, minus any names in disabled, plus compiled custom detectors. Invalid custom patterns are reported as an error rather than silently dropped.
func NewRegistry ¶
NewRegistry builds a registry from an explicit detector list (primarily for tests and embedders). Most callers want NewDefaultRegistry.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one conversation's translation context: the manager's shared detector/known state plus a private mapping table. All methods are safe on a nil receiver (no-ops), for callers that thread the handle unconditionally.
func (*Session) Enabled ¶
Enabled reports whether translation is active for this session: the per-session override when set, else the manager default.
func (*Session) NewStreamRestorer ¶
func (s *Session) NewStreamRestorer() *StreamRestorer
NewStreamRestorer returns a restorer bound to this session's live token set (the mapping table can grow mid-stream).
func (*Session) Restore ¶
Restore replaces synthetic tokens in text with real values. Identity when disabled.
func (*Session) RestoreDisplay ¶
RestoreDisplay reports whether displayed output should be restored.
func (*Session) RestoreJSON ¶
func (s *Session) RestoreJSON(raw json.RawMessage) json.RawMessage
RestoreJSON restores tokens in every string leaf of raw.
func (*Session) Reveal ¶
Reveal resolves a token back to its real value LOCALLY — a detected-tier mapping token (e.g. "sk_live_SNAT000001"), a known-tier "${NAME}" reference, or a bare registered NAME. Returns (value, tier, ok). This is the explicit "##snat get" escape hatch: the value is printed to the owner's own pane and is NEVER placed on the outbound path. source is "detected" or "known".
func (*Session) Sanitize ¶
Sanitize translates secrets in text to synthetic tokens. Identity when disabled.
func (*Session) SanitizeJSON ¶
func (s *Session) SanitizeJSON(raw json.RawMessage) json.RawMessage
SanitizeJSON translates secrets in every string leaf of raw.
func (*Session) SetOverride ¶
SetOverride sets (or clears, with nil) the per-session enable override.
func (*Session) Stats ¶
func (s *Session) Stats() SessionStats
Stats returns the session's value-free counters.
type SessionHandle ¶
type SessionHandle interface {
Enabled() bool
Sanitize(text string) string
SanitizeJSON(raw json.RawMessage) json.RawMessage
Restore(text string) string
RestoreJSON(raw json.RawMessage) json.RawMessage
RestoreDisplay() bool
NewStreamRestorer() *StreamRestorer
Stats() SessionStats
}
SessionHandle is the interface the agentic layer consumes. All methods are safe on a nil *Session and cheap no-ops when disabled, so callers can thread it unconditionally.
type SessionStats ¶
type SessionStats struct {
Detected int // total value→token replacements (both tiers)
Restored int // total token→value replacements
Mappings int // distinct detected-tier mappings alive
PerDetector map[string]int // hit counts per detector
Entries []MappingEntry // token/detector/hits — never values
Override *bool // per-session enable override (nil = default)
}
SessionStats is the value-free per-session metrics view.
type StreamRestorer ¶
type StreamRestorer struct {
// contains filtered or unexported fields
}
StreamRestorer restores synthetic tokens in a streamed text where a token may straddle chunk boundaries. It emits text as soon as it provably cannot be the beginning of a restorable token, holding back at most one max-token-length tail — O(1) memory, imperceptible latency.
Usage: out := r.Feed(delta) per chunk, then out := r.Flush() at stream end (message_stop) to drain the held-back tail.
func NewStreamRestorer ¶
func NewStreamRestorer(restore func(string) string, candidates func() []string) *StreamRestorer
NewStreamRestorer builds a restorer from a restore function and a token candidate provider. Both must be safe for concurrent use with the underlying session.
func (*StreamRestorer) Feed ¶
func (r *StreamRestorer) Feed(chunk string) string
Feed appends chunk to the pending text and returns the longest prefix that is safe to emit, restored. The suffix that could still be the start of a token is held back for the next Feed/Flush.
func (*StreamRestorer) Flush ¶
func (r *StreamRestorer) Flush() string
Flush drains and restores whatever is still held back. Call exactly once, after the final chunk.