Help us shape the future of Go! Take the 2026 Go Developer Survey

secretnat

package
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 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