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

proxy

package
v0.2.11 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package proxy implements the Universal Agent Governance Proxy (design 001): a loopback HTTP proxy that intercepts the LLM provider traffic of third-party agent CLIs (Claude Code, Codex, aider, …) running inside panes and applies rysh governance to it — SecretNAT redaction on request bodies, token/cost metering into the usage ledger (design 003), and audit — by base-URL environment injection, NOT TLS MITM.

Index

Constants

View Source
const DefaultTenantHeader = "X-Rysh-Tenant"

DefaultTenantHeader is the header a caller may use to name its tenant.

View Source
const GraceWindow = 20 * time.Second

GraceWindow is how long a known CLI may run without a governed request before rysh says so. Long enough that a cold start is not accused, short enough that an escaped CLI is reported while the user is still watching.

Variables

This section is empty.

Functions

func Ceilings added in v0.2.3

func Ceilings(cfg config.ProxyTenantConfig) map[string]int64

Ceilings maps every configured tenant to its ceiling, for seeding the ledger.

func DefaultUpstreams

func DefaultUpstreams() map[string]string

DefaultUpstreams maps a dialect to its real provider base URL, as declared by each registered Dialect.

func Endpoint

func Endpoint() string

Endpoint returns the running proxy's base URL, or "" if none is running. PaneActor injects <endpoint>/<dialect>/<paneID> as ANTHROPIC_BASE_URL etc. only when this is non-empty, so a disabled proxy leaves pane env untouched.

func FailoverSummary added in v0.2.3

func FailoverSummary(c config.ProxyFailoverConfig) string

FailoverSummary renders the configured upstream chain for ##proxy status.

func HasTenantKeys added in v0.2.3

func HasTenantKeys(cfg config.ProxyTenantConfig) bool

HasTenantKeys reports whether any tenant selects its own credential, for ##proxy status. A credential boundary the operator cannot see is one they cannot trust.

func IdentifyCLI added in v0.2.11

func IdentifyCLI(comm, execPath string, argv []string) (string, bool)

IdentifyCLI names the known agent CLI a process is, from what the OS says about it: comm is the kernel's accounting name (darwin p_comm, Linux /proc/<pid>/comm), execPath the executable's path and argv its arguments. It returns the profile's own binary name, so every notice names the CLI the way `##proxy check` expects it.

comm alone is not enough (F-15). The native installer lays `claude` out as

~/.local/bin/claude -> ~/.local/share/claude/versions/<version>

so the executable's own basename IS the version, and comm reads "2.1.288". What still says "claude" is argv[0], and on darwin the exec path as invoked (the symlink, unresolved). Linux's /proc/<pid>/exe is already resolved to versions/<version>, so the `<cli>/versions/<anything>` layout is recognised too. Walking symlinks would be the wrong direction: it lands on the version.

A script CLI run through an interpreter (`node …/bin/claude`, `python3 …/aider`) reports the interpreter as comm and argv[0], so argv[1] is consulted when argv[0] is one.

ok is false when nothing identifies a known CLI. That is the gate design 022 §4.4 asked for and F-13 found missing: only a known agent CLI is watched.

func KnownCLIs added in v0.2.3

func KnownCLIs() []string

KnownCLIs lists the binaries rysh recognises, sorted for stable output.

func SetEndpoint

func SetEndpoint(base string)

SetEndpoint records the running proxy's loopback base URL (or "" to clear).

func StrictBlockNotice added in v0.2.3

func StrictBlockNotice(binary string) string

StrictBlockNotice is the pane line for a CLI that strict mode is about to stop. It names what is being stopped, why, and how to get the deterministic answer — because strict mode acts on negative evidence and will occasionally stop something innocent.

func UnproxiedWarning added in v0.2.3

func UnproxiedWarning(binary string) string

UnproxiedWarning is the pane line for a CLI that looks ungoverned. It says what was observed and what to do, and is explicit that this is a suspicion — an idle CLI looks the same from here.

Types

type AuditLine

type AuditLine struct {
	TS            time.Time
	PaneID        string
	Dialect       string
	Model         string
	Endpoint      string
	ReqBytes      int
	RedactionHits int
	Status        int
	InTokens      int
	OutTokens     int
	// Upstream is the base URL that actually served the request, and Attempts
	// how many it took (1 = no failover). Both matter only once failover is on,
	// and both are what makes a silent fallback visible after the fact.
	Upstream string
	Attempts int
	// Tenant attributes the request to a customer (design 022 §4.3).
	Tenant string
	// EchoHits counts plaintext secrets the RESPONSE carried back (001 §3
	// step 8). Non-zero means a live credential reached the pane; rysh reports
	// it and does not rewrite the body (echo.go).
	EchoHits int
}

AuditLine is a metadata-only record of one proxied request (never content).

func (AuditLine) String

func (a AuditLine) String() string

type CLIProfile added in v0.2.3

type CLIProfile struct {
	// Binary is the executable name as it appears in the process table.
	Binary string
	// Dialect is which proxy dialect this CLI's traffic speaks.
	Dialect string
	// BaseURLEnv is the environment variable it reads its endpoint from.
	BaseURLEnv string
	// KeyEnv is the environment variable it reads its credential from.
	KeyEnv string
	// EnvIsEnough reports whether exporting BaseURLEnv actually governs this
	// CLI. False means the CLI needs an adapter (see AdaptSpawn) and that
	// base-URL injection alone will silently leave it ungoverned.
	EnvIsEnough bool
	// Note explains a false EnvIsEnough, and is what the pane warning says.
	Note string
	// contains filtered or unexported fields
}

CLIProfile is what rysh knows about one third-party agent CLI.

func ProfileFor added in v0.2.3

func ProfileFor(binary string) (CLIProfile, bool)

ProfileFor returns what rysh knows about a binary. The lookup tolerates a path and a .exe suffix, because a process table reports whatever was exec'd.

func (CLIProfile) AdaptSpawn added in v0.2.3

func (p CLIProfile) AdaptSpawn(baseURL, dir string) ([]string, error)

AdaptSpawn returns the extra environment that forces this CLI through baseURL at pane-spawn time, where rysh controls the environment but NOT the argv.

dir is a rysh-owned directory the adapter may write into. Returns nil when the CLI needs no adaptation.

func (CLIProfile) ProbeCommand added in v0.2.3

func (p CLIProfile) ProbeCommand(bin, baseURL, prompt, scratch, dummyKey string) ([]string, []string, error)

ProbeCommand builds the argv and environment for a one-shot run of this CLI against baseURL. Used by `##proxy check` and by cmd/wire-harness, so the answer to "how do you force this CLI through a base URL" has ONE home.

dummyKey is what lands in the CLI's credential variable: the probe must never need a working key, since the point is to observe the wire, not to complete.

type Dialect

type Dialect interface {
	// Name is the routing key in /{dialect}/{paneID}/... paths.
	Name() string
	// Upstream is the default provider base URL (overridable via config).
	Upstream() string
	// ApplyAuth attaches key using the provider's convention. key == "" means
	// no key is configured: the caller's own credentials (already copied onto
	// req) must pass through untouched.
	ApplyAuth(req *http.Request, key string)
	// ExtractUsage pulls token usage and model out of a response body
	// (JSON or SSE).
	ExtractUsage(body []byte) Usage
	// WriteRateLimited writes a synthetic 429 in the provider's native error
	// JSON shape, so the wrapped CLI's own error handling recognises it as a
	// rate limit and exits cleanly rather than treating an unfamiliar body as
	// a crash.
	//
	// It serves both refusals rysh can issue — the token ceiling (design 001
	// §4.4) and the request-rate limiter (design 022 §4.2) — because on the
	// wire they are the same thing: a rate limit. They differ only in the
	// message and in how long the caller should wait, which is why retryAfter
	// is the caller's to supply. A ceiling is a daily figure and answers
	// coarsely; a token bucket knows its delay exactly.
	WriteRateLimited(w http.ResponseWriter, message string, retryAfter time.Duration)
	// ErrorBody renders message as this provider's native error JSON for an
	// arbitrary status. Same reasoning as WriteRateLimited: every refusal rysh
	// issues — an unknown pane in the path, a model the allowlist forbids —
	// lands in a wrapped CLI's error handling, and a body it cannot parse turns
	// a clean refusal into a crash.
	ErrorBody(status int, message string) []byte
	// AdaptRequestBody may rewrite the (already redacted) request body before it
	// is forwarded, for the provider-specific adjustment metering requires —
	// today only OpenAI's `stream_options.include_usage` (design 001 §4.2).
	// path is the upstream path being called, so an adapter can scope itself to
	// the endpoints it understands. Returning body unchanged is the norm.
	AdaptRequestBody(path string, body []byte) []byte
}

Dialect abstracts one provider wire format (design 001 §4.2): the default upstream its traffic goes to, how the real API key is attached to a request, how token usage is read out of a response, and what its native rate-limit error looks like — so the budget 429 the proxy synthesizes is indistinguishable in shape from the provider's own.

type LeaseGate added in v0.2.3

type LeaseGate interface {
	// Allow reports whether the tenant may spend, and a short machine-readable
	// reason it may not.
	Allow(tenant string) (bool, string)
	// Spend records tokens against the current lease after a request meters.
	Spend(tenant string, tokens int64)
}

LeaseGate is the seam design 023 §4.2 hangs the org-wide ceiling on.

The contract is the whole point: both methods are IN-PROCESS, non-blocking reads/writes of local numbers. The daemon leases a slice of the workspace or tenant allowance out of band and enforces against that slice here, so the hot path never makes a network call — a per-request WAN round trip would put provider-latency-scale delay in front of every request and make a blip look like a provider outage.

nil ⇒ no org-wide governance, which is the default and the OSS behaviour.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the loopback governance proxy. It runs on its own HTTP goroutines (data plane). The mutex guards only the small audit ring buffer — a documented exception to the no-mutex rule, at the HTTP/actor boundary.

func Current added in v0.2.3

func Current() *Server

Current returns the running proxy server, or nil when none is running.

func New

func New(pub *msg.NATSPublisher, snat *secretnat.Manager, keys map[string]string, upstreams map[string]string, auditContent bool) *Server

New builds a proxy server. upstreams nil ⇒ DefaultUpstreams.

keys maps a dialect (anthropic|openai|gemini) to the real upstream API key. A dialect absent from the map is NOT rewritten — the caller's own credential passes through. Passing one key for every dialect is what previously sent an Anthropic key as the OpenAI bearer and broke wrapped OpenAI CLIs.

func (*Server) BaseURL

func (s *Server) BaseURL() string

BaseURL returns the running base URL ("" if not started).

func (*Server) DueWarning added in v0.2.3

func (s *Server) DueWarning(paneID string) (string, bool)

DueWarning returns the binary a pane should be warned about, once per run. Empty when there is nothing to say.

func (*Server) ForgetPane added in v0.2.3

func (s *Server) ForgetPane(paneID string)

ForgetPane drops every trace of a pane when it closes, so a long-lived daemon does not accumulate state per pane it has ever seen — and so a recycled pane ID starts from nothing rather than inheriting the last tenant's verdicts.

Called from PaneActor's Stopping handler; it had no callers at all until then, which meant none of this was ever released.

func (*Server) NoteForeground added in v0.2.3

func (s *Server) NoteForeground(paneID, binary string)

NoteForeground records which program a pane is running (design 022 §4.4), so a known agent CLI that never reaches the proxy can be reported. Pass "" when the pane returns to its shell. A binary with no CLI profile is not watched; pass the name IdentifyCLI returns, not the kernel's process name.

func (*Server) NotePane added in v0.2.3

func (s *Server) NotePane(paneID string)

NotePane registers a pane as a legitimate attribution target. Called where rysh injects the pane's base URL, so the set of panes the proxy accepts is exactly the set it pointed at itself.

func (*Server) PaneGovernanceState added in v0.2.3

func (s *Server) PaneGovernanceState(paneID string) string

PaneGovernanceState is the ##proxy status column: governed | starting | unproxied? | idle. The question mark is deliberate — this is inferred from the absence of traffic, and an idle CLI looks identical to an escaped one.

func (*Server) Port

func (s *Server) Port() int

Port returns the loopback port the proxy bound, or 0 if not started. Used to record the governed endpoint in the session registry (design 001).

func (*Server) RecentAudits

func (s *Server) RecentAudits(n int) []AuditLine

RecentAudits returns up to n most-recent audit lines (newest last).

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the per-request pipeline: parse → SNAT → forward → stream back → usage/audit.

func (*Server) SetAuditDir

func (s *Server) SetAuditDir(dir string)

SetAuditDir sets the directory redacted request bodies are written to when audit_content is enabled (design 001 §4.5). Call before Start. With no dir set, audit_content records metadata only — the body sink stays inert.

func (*Server) SetFailover added in v0.2.3

func (s *Server) SetFailover(cfg config.ProxyFailoverConfig)

SetFailover installs the upstream-retry policy (design 022 §4.1). Call before Start. Disabled ⇒ one attempt per request, exactly as before.

HeaderTimeout is applied to the shared transport only when explicitly set. It is off by default because a non-streaming completion can legitimately send no bytes for minutes while the model generates: any default here would eventually reclassify a slow-but-working request as a dead upstream and retry it, paying for the same generation twice.

func (*Server) SetLeaseGate added in v0.2.3

func (s *Server) SetLeaseGate(g LeaseGate)

SetLeaseGate installs the org-wide ceiling (design 023 §4.2). Call before Start. nil ⇒ per-machine ceilings only, exactly as today.

func (*Server) SetModelRules added in v0.2.3

func (s *Server) SetModelRules(cfg config.ProxyModelRules)

SetModelRules installs the model allowlist (design 001 §4.7). Call before Start. Empty ⇒ every model is allowed, as before.

func (*Server) SetRateLimit added in v0.2.3

func (s *Server) SetRateLimit(cfg config.ProxyRateLimitConfig)

SetRateLimit installs the request-rate rules (design 022 §4.2). Call before Start. An empty config leaves the limiter nil, which is the unlimited default.

func (*Server) SetStrict added in v0.2.3

func (s *Server) SetStrict(v bool)

SetStrict turns the ungoverned-CLI warning into a block (design 022 §8.2). Call before Start. It is read by the pane side, which owns the only thing that can act on it: the process in the pane.

The proxy holds the flag rather than each pane re-deriving it, because it can come from either config or policy and the two must not drift.

func (*Server) SetTenants added in v0.2.3

func (s *Server) SetTenants(cfg config.ProxyTenantConfig)

SetTenants installs per-customer policy (design 022 §4.3). Call before Start. Empty ⇒ untenanted, and every control keys off the pane as before.

func (*Server) Start

func (s *Server) Start(addr string) (string, error)

Start binds a loopback listener (ephemeral port unless addr specifies one), serves in the background, and publishes the endpoint for pane env injection. Returns the base URL (http://127.0.0.1:PORT).

func (*Server) StartPrivate added in v0.2.3

func (s *Server) StartPrivate(addr string) (string, error)

StartPrivate is Start without the process-global publication: the server runs and serves, but proxy.Endpoint() and proxy.Current() keep pointing at whatever was already there.

This exists because `##proxy check` (wirecheck) starts a second, throwaway proxy INSIDE the live daemon. With the publishing Start, that probe overwrote the session's endpoint on the way in and cleared it on the way out — after which every pane spawned got no base-URL injection and was silently ungoverned, while ##proxy status still reported ON.

func (*Server) Stop

func (s *Server) Stop()

Stop shuts the server down, and clears the endpoint only if THIS server is the one that published it. A private probe proxy therefore cannot unpublish the session's real endpoint, and neither can a stale Stop after a restart.

func (*Server) Strict added in v0.2.3

func (s *Server) Strict() bool

Strict reports whether an ungoverned CLI should be stopped rather than warned about.

type Usage

type Usage struct {
	InTokens   int
	OutTokens  int
	CacheRead  int
	CacheWrite int
	Model      string
}

Usage is the token accounting a Dialect extracts from one response.

Directories

Path Synopsis
Package wirecheck answers one question about one CLI: if rysh points it at the governance proxy, does its provider traffic actually go there?
Package wirecheck answers one question about one CLI: if rysh points it at the governance proxy, does its provider traffic actually go there?

Jump to

Keyboard shortcuts

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