secretnat

package
v0.2.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

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

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

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

func NewGenerator(mode Mode) *Generator

NewGenerator returns a generator for the given mode.

func (*Generator) Mode

func (g *Generator) Mode() Mode

Mode returns the generator's mode.

func (*Generator) Synthetic

func (g *Generator) Synthetic(m Match, seq int) string

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

type KnownSecret struct {
	Name  string
	Value string
}

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.

func (*KnownSet) Size

func (ks *KnownSet) Size() int

Size returns the number of usable known secrets.

func (*KnownSet) Tokens

func (ks *KnownSet) Tokens() []string

Tokens returns the "${NAME}" token for every known secret.

func (*KnownSet) ValueFor

func (ks *KnownSet) ValueFor(name string) (string, bool)

ValueFor returns the real value registered under name, or ("", false). Used only by the explicit, local-only "##snat get ${NAME}" reveal.

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

func NewManager(opts Options) (*Manager, error)

NewManager builds a Manager from options. Custom detector patterns are compiled eagerly; an invalid pattern fails construction.

func (*Manager) CloseSession

func (m *Manager) CloseSession(convID string)

CloseSession drops convID's mapping table (pane closed). Real values in the table become unreachable immediately.

func (*Manager) DetectorNames

func (m *Manager) DetectorNames() []string

DetectorNames lists active detectors (##snat status).

func (*Manager) Enabled

func (m *Manager) Enabled() bool

Enabled returns the session-wide default.

func (*Manager) Mode

func (m *Manager) Mode() Mode

Mode returns the current token mode.

func (*Manager) RestoreDisplay

func (m *Manager) RestoreDisplay() bool

RestoreDisplay reports whether displayed output should be restored.

func (*Manager) Session

func (m *Manager) Session(convID string) *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

func (m *Manager) SetEnabled(v bool)

SetEnabled flips the session-wide default (##snat on|off --session).

func (*Manager) SetMode

func (m *Manager) SetMode(mode Mode)

SetMode switches token style for subsequently minted tokens.

func (*Manager) Stats

func (m *Manager) Stats() ManagerStats

Stats aggregates value-free counters across sessions.

func (*Manager) SweepExpired

func (m *Manager) SweepExpired() int

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

type MappingEntry struct {
	Token    string
	Detector string
	Hits     int
}

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 NewMappingTable

func NewMappingTable() *MappingTable

NewMappingTable returns an empty table.

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) Size

func (t *MappingTable) Size() int

Size returns the number of mappings.

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"
)

func ParseMode

func ParseMode(s string) Mode

ParseMode normalizes a config string into a Mode, defaulting to semantic.

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

func (n *NATProvider) Complete(ctx context.Context, prompt string) (string, error)

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

func NewRegistry(detectors ...Detector) *Registry

NewRegistry builds a registry from an explicit detector list (primarily for tests and embedders). Most callers want NewDefaultRegistry.

func (*Registry) DetectAll

func (r *Registry) DetectAll(text string) []Match

DetectAll runs every detector over text and merges the results using leftmost-longest-wins overlap resolution (equal spans: higher confidence, then registry order). The returned matches are sorted by Start and are pairwise non-overlapping.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the detector names in registry order.

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

func (s *Session) Enabled() bool

Enabled reports whether translation is active for this session: the per-session override when set, else the manager default.

func (*Session) ID

func (s *Session) ID() string

ID returns the conversation id this session serves.

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

func (s *Session) Restore(text string) string

Restore replaces synthetic tokens in text with real values. Identity when disabled.

func (*Session) RestoreDisplay

func (s *Session) RestoreDisplay() bool

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

func (s *Session) Reveal(token string) (value, source string, ok bool)

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

func (s *Session) Sanitize(text string) string

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

func (s *Session) SetOverride(v *bool)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL