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
- func Ceilings(cfg config.ProxyTenantConfig) map[string]int64
- func DefaultUpstreams() map[string]string
- func Endpoint() string
- func FailoverSummary(c config.ProxyFailoverConfig) string
- func HasTenantKeys(cfg config.ProxyTenantConfig) bool
- func IdentifyCLI(comm, execPath string, argv []string) (string, bool)
- func KnownCLIs() []string
- func SetEndpoint(base string)
- func StrictBlockNotice(binary string) string
- func UnproxiedWarning(binary string) string
- type AuditLine
- type CLIProfile
- type Dialect
- type LeaseGate
- type Server
- func (s *Server) BaseURL() string
- func (s *Server) DueWarning(paneID string) (string, bool)
- func (s *Server) ForgetPane(paneID string)
- func (s *Server) NoteForeground(paneID, binary string)
- func (s *Server) NotePane(paneID string)
- func (s *Server) PaneGovernanceState(paneID string) string
- func (s *Server) Port() int
- func (s *Server) RecentAudits(n int) []AuditLine
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) SetAuditDir(dir string)
- func (s *Server) SetFailover(cfg config.ProxyFailoverConfig)
- func (s *Server) SetLeaseGate(g LeaseGate)
- func (s *Server) SetModelRules(cfg config.ProxyModelRules)
- func (s *Server) SetRateLimit(cfg config.ProxyRateLimitConfig)
- func (s *Server) SetStrict(v bool)
- func (s *Server) SetTenants(cfg config.ProxyTenantConfig)
- func (s *Server) Start(addr string) (string, error)
- func (s *Server) StartPrivate(addr string) (string, error)
- func (s *Server) Stop()
- func (s *Server) Strict() bool
- type Usage
Constants ¶
const DefaultTenantHeader = "X-Rysh-Tenant"
DefaultTenantHeader is the header a caller may use to name its tenant.
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 ¶
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
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
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
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).
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) DueWarning ¶ added in v0.2.3
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
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
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
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
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 ¶
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 ¶
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 ¶
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
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
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 ¶
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
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.
Source Files
¶
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? |