Documentation
¶
Overview ¶
Package runtime hosts the request-time dispatcher and the plugin runtime interfaces. The architecture mirrors unclaw's plugin runtime: endpoint plugins own protocol decoding, credential plugins own secret injection, approver plugins own arbitration. Built-in plugins satisfy these interfaces directly; a future distribution layer would slot in behind the same shapes.
Index ¶
- Constants
- Variables
- func AcceptCredentialRuntime(check func(*config.Plugin) bool)
- func CredentialMismatchReason(policy *config.CompiledPolicy, profile string, ep *config.CompiledEndpoint, ...) string
- func HITLApprovalMessage(state HITLOperationState, effect HITLApprovalEffect, upstreamCalled bool) string
- func HITLEndpointLabel(req ApproveRequest) string
- func HITLQueryLabel(ep *config.CompiledEndpoint) string
- func HITLTitle(method, endpoint string) string
- func HostEndpoint(policy *config.CompiledPolicy, profile, host string) *config.CompiledEndpoint
- func MatchRequest(ep *config.CompiledEndpoint, req *match.Request) *config.CompiledRule
- func MatchRequestFailClosed(ep *config.CompiledEndpoint, req *match.Request) *config.CompiledRule
- func NormalizeHITLPendingApproval(p *HITLPending)
- func ResolveCredential(policy *config.CompiledPolicy, profile string, ep *config.CompiledEndpoint, ...) *config.CompiledCredential
- type ApproveCallRequest
- type ApproveRequest
- type ApproveVerdict
- type ApproverRuntime
- type BlobStore
- type ClickhouseAuthCredential
- type ConnEndpointRuntime
- type ConnEvent
- type ConnHandle
- type ConnIndex
- type ConnRouter
- type EKSBearerMinter
- type EndpointTLSConfigurer
- type EnvSecretStore
- type HITLApprovalEffect
- type HITLClassifier
- type HITLDecision
- type HITLHumanCredentialer
- type HITLMessageUpdate
- type HITLMessageUpdateSink
- type HITLMessageUpdater
- type HITLNotifier
- type HITLOperationState
- type HITLPending
- type HITLPendingMessageUpdateSink
- type HITLPool
- type HITLPoolAsyncGrantResolver
- type HITLPoolCanceler
- type HITLPoolDecider
- type HITLPoolUpdater
- type HITLResolveResult
- type HITLState
- type HITLSummary
- type HITLTarget
- type HTTPCredentialRedactionProvider
- type HTTPCredentialRuntime
- type HTTPRequestRewriter
- type HTTPRequestSigner
- type HTTPSyntheticResponder
- type PlaceholderDetector
- type PostgresAuthCredential
- type PostgresCredentialRuntime
- type PostgresStartup
- type Request
- type SQLParser
- type Secret
- type SecretStore
- type TLSCredentialRuntime
- type Tunnel
- type TunnelCredential
- type TunnelCredentialNamer
- type TunnelDisconnector
- type TunnelHost
- type TunnelReconciler
- type TunnelRuntime
- type TunnelSharing
- type WebSocketCredentialRuntime
- type WebhookCtx
- type WebhookHandler
- type WebhookProvider
- type WebhookRoute
Constants ¶
const ApproveDecisionAsyncPending = "async_pending"
ApproveDecisionAsyncPending is the ApproveVerdict.Decision value an approver returns when no synchronous verdict was reached and the operation is being recorded for async resolution.
Variables ¶
var ErrTunnelUnsupported = errors.New("tunnel plugin not available in this build")
ErrTunnelUnsupported is returned by a tunnel plugin's Open when the binary lacks the build tag (or the host lacks the platform support) needed to bring the tunnel up. The manager translates this into a clear "rebuild with -tags X" log entry and fails the inbound conn with a 503-equivalent rather than panicking the plugin's init() at policy load.
var ErrUnsupported = errors.New("plugin runtime not implemented")
ErrUnsupported is returned by a plugin's runtime hook when the requested operation isn't implemented for that plugin yet (e.g. clickhouse_native endpoints have schema only). The dispatcher translates this into a clear "endpoint runtime not implemented" log entry and a 503 to the agent.
Functions ¶
func AcceptCredentialRuntime ¶
AcceptCredentialRuntime registers an additional credential runtime-interface check. checkRuntime accepts a credential plugin when ANY registered check (or built-in interface) matches its Runtime — out-of-tree credential families plug in here from their own init() rather than editing this file.
func CredentialMismatchReason ¶ added in v0.2.2
func CredentialMismatchReason(policy *config.CompiledPolicy, profile string, ep *config.CompiledEndpoint, req *match.Request) string
CredentialMismatchReason builds a one-line explanation of why ResolveCredential just returned nil for (profile, ep, req) despite the binding existing in policy. Useful for surfacing actionable connection errors to agents (e.g. postgres' SCRAM error path) so the operator sees "user 'none' doesn't match" instead of a flat "no credential bound". Returns "" when the profile-endpoint pair has no bound credentials at all — the caller falls back to its generic "no credential" message in that case.
func HITLApprovalMessage ¶
func HITLApprovalMessage(state HITLOperationState, effect HITLApprovalEffect, upstreamCalled bool) string
HITLApprovalMessage returns the operator-facing description of what approving the pending request will do, given the operation's current state, the precomputed approval effect, and whether the upstream call already started.
func HITLEndpointLabel ¶
func HITLEndpointLabel(req ApproveRequest) string
HITLEndpointLabel returns the most concrete endpoint identifier for a HITL prompt. Whether the request's Host is informative on its own (HTTPS hostname like api.anthropic.com) or merely a wire address (SQL / k8s VIP) is a per-facet property: the facet runtime declares HostIsResource() and the dashboard / Slack renderer asks instead of carving out family strings here.
func HITLQueryLabel ¶
func HITLQueryLabel(ep *config.CompiledEndpoint) string
HITLQueryLabel picks a family-appropriate label for the body of a HITL prompt by asking the facet that owns the endpoint's family. Falls back to "Path" for unknown families or endpoints without a registered facet.
func HITLTitle ¶
HITLTitle builds the Slack header / dashboard title: "Approve <verb> · <endpoint>". Either half may be empty; an empty input is dropped so we never emit "Approve · ".
func HostEndpoint ¶
func HostEndpoint(policy *config.CompiledPolicy, profile, host string) *config.CompiledEndpoint
HostEndpoint resolves a profile + SNI/authority host to the endpoint that owns it. Compile populates HostIndex with exact declared hosts plus bare-host aliases for HTTPS-family default-port declarations, so a TLS SNI value like "api.example.com" can match an endpoint declared as "api.example.com:443" without a runtime scan. DNS hostnames are case-insensitive; the lookup key is lowercased to match the lowercase keys Compile inserts.
When the exact lookup misses we walk HostPatterns — the profile's wildcard declarations (`hosts = ["*.foo.com"]`) — in longest-suffix-first order. Exact matches always win over wildcards for the same name. Returns nil when nothing matches; the caller then applies the defaults.unknown_host policy.
func MatchRequest ¶
func MatchRequest(ep *config.CompiledEndpoint, req *match.Request) *config.CompiledRule
MatchRequest walks an endpoint's priority-sorted rule list and returns the first rule whose matcher accepts req. Disabled rules are skipped. nil is returned when no rule fires — the caller then applies the defaults.unknown_host policy (or the endpoint plugin's own default).
Unevaluable fail-close: a matcher returns three-valued Decisions. When a rule's condition cannot be evaluated honestly — its outcome depends on a facet value the gateway doesn't have (bytes capped at the inspection buffer, parser-refused SQL fields, both surfaced as viral CEL unknowns), or evaluation errored at runtime — the rule is fired with a synthesized deny verdict instead of being silently skipped (skipping would let a deny rule fail open). The returned CompiledRule keeps the original rule's identity (name / priority) so logs still attribute the deny to the rule whose contract broke.
Because the unknowns are value-level, a rule that merely mentions a truncated / unparseable facet still resolves honestly when its outcome doesn't depend on it: `sql.verb == 'select' && sql.database == 'x'` on an unparseable request with database 'y' evaluates `unknown && false == false` and cleanly falls through to the next rule. Rules keyed only on always-available facets (credential, peer IP, sql.statement on an unparseable query) are unaffected.
func MatchRequestFailClosed ¶ added in v0.2.5
func MatchRequestFailClosed(ep *config.CompiledEndpoint, req *match.Request) *config.CompiledRule
MatchRequestFailClosed wraps MatchRequest for wire frontends whose request bytes the gateway could not fully inspect (Truncated / Unparseable). On such a request, "no rule matched" may simply mean every rule's outcome was independent of the missing bytes (absorption) — not that the operator intended the bytes to flow. When the endpoint declares at least one enabled rule and none matched, this returns a synthesized catch-all deny instead of nil, so an uninspectable payload never rides the implicit-allow default past a rule set that exists. Endpoints with no rules at all keep their legacy pass-through default, and fully-inspected requests behave exactly like MatchRequest.
SQL frontends (postgres, clickhouse_native) route their statement evaluation through this; the HTTPS path deliberately does NOT — rule-less LLM endpoints routinely forward >cap request bodies, and inspectable header/method facets remain matchable there.
func NormalizeHITLPendingApproval ¶
func NormalizeHITLPendingApproval(p *HITLPending)
NormalizeHITLPendingApproval fills safety copy defaults for a pending HITL prompt. Existing synchronous human-approval prompts start in sync_waiting: approving while the original request is still held sends the request upstream immediately. Async grant code can override the state/effect/message when sync_wait_timeout has already returned 202.
func ResolveCredential ¶
func ResolveCredential(policy *config.CompiledPolicy, profile string, ep *config.CompiledEndpoint, req *match.Request) *config.CompiledCredential
ResolveCredential picks the credential entry that applies to req for the given profile and endpoint.
The profile's per-endpoint credential list (CompiledProfile. EndpointCredentials) carries the dispatch entries — each pairs a credential with a merged disambiguator map (block-side body fields + framework-peeled `placeholder`, overlaid with the profile's inline `{ credential = X, <field> = "..." }` entries). Field names are conventionally:
- "placeholder" — the dispatcher asks the endpoint plugin's PlaceholderDetector to extract the agent-sent placeholder string from the request body / headers.
- "user" — matched against req.User (postgres / clickhouse_native populate this from the wire-protocol StartupMessage / Hello).
- "database" — matched against req.Database.
An entry matches iff every constraint it declares is satisfied. Among matching entries the most-specific (highest number of declared constraints) wins; compile-time validation guarantees signature uniqueness per (profile, endpoint) so equal-specificity ties are impossible at runtime — but if one ever shows up we return nil rather than silently mis-routing.
Single-entry bindings short-circuit unconditionally: with only one credential on a (profile, endpoint), there's nothing to disambiguate. The credential's body fields (postgres_credential's `user`, clickhouse's `database`, etc.) still drive upstream auth, but the agent doesn't need to mirror those values in its StartupMessage / Hello to make the dispatch pick the credential. Multi-entry endpoints still go through the disambiguator filter below so placeholder-driven `pg_ro` / `pg_rw` patterns keep working.
Returns nil when the profile is unknown OR the profile binds no credential to ep — the caller (endpoint plugin) decides whether to default-deny vs. forward-unauthenticated.
Types ¶
type ApproveCallRequest ¶
type ApproveCallRequest struct {
Stages []config.ApproveStage
Verb string // SQL verb / k8s verb / etc., for the dashboard
Summary string // one-liner the operator sees in the HITL prompt
// Rule is the matched compiled rule (carries Reason for the
// dashboard's "why is this gated" line).
Rule *config.CompiledRule
}
ApproveCallRequest is what a ConnEndpointRuntime hands to ConnHandle.Approve when a matched rule has an approve = [...] chain. Verb / Summary populate the dashboard's HITL request card; Stages drives which approvers fire in which order.
type ApproveRequest ¶
type ApproveRequest struct {
Stage config.ApproveStage
Endpoint *config.CompiledEndpoint
Rule *config.CompiledRule
Request *match.Request
// ApproverName is the bare name from the stage — also the key the
// approver should use against Pool / Secrets when it needs to
// disambiguate per-approver state.
ApproverName string
// AgentIP is the WireGuard source IP of the originating peer.
// Used as the HITLPending.AgentIP key and as a log identifier.
AgentIP string
// Profile is the tenant profile the originating peer is bound to
// (e.g. "dev2"). Informational — approvers use it as a
// human-readable label in slack cards / log lines / message
// templates; it carries no credential-lookup meaning.
Profile string
// Method / Host / Path / UA / BodySample carry the request shape
// for HITL prompts. Endpoint plugins fill these so approvers
// don't have to know the family-specific Request internals.
Method string
Host string
Path string
UA string
BodySample string
Reason string
// ThreadTS, when set, asks HITL notifiers to post the approval
// prompt as a reply in this Slack thread rather than top-level.
// Populated from the X-HITL-Thread-TS request header.
ThreadTS string
// NotifyChannel, when set, overrides the static channel declared in
// the human_approver HCL block. Populated from X-HITL-Channel header.
// Lets agents route approvals into their own Slack session thread.
NotifyChannel string
// AsyncOperationID binds the prompt to a durable async operation.
// If AsyncPendingOnSyncTimeout is true and ctx hits DeadlineExceeded,
// human_approver leaves the prompt pending and returns async_pending.
AsyncOperationID string
AsyncPendingOnSyncTimeout bool
// Pool exposes the gateway's shared pending-approval list — the
// dashboard / Slack approvers use it to publish a pending entry
// and block until a decision arrives. Synchronous approvers
// (LLM) leave it nil-handled.
Pool HITLPool
// Secrets fetches the bot token / API key the approver needs to
// post a notification or call an LLM judge.
Secrets SecretStore
// DashboardURL is the operator-facing dashboard origin used for
// deep links in Slack messages and similar notifications.
DashboardURL string
// Policy gives approvers access to the full compiled policy —
// HumanApprover uses it to look up its referenced credential
// entity and dispatch via HITLNotifier.
Policy *config.CompiledPolicy
// MessageUpdateSink records channel message references for async HITL
// operation updates after notifiers successfully post prompts.
MessageUpdateSink HITLMessageUpdateSink
// PendingMessageUpdateSink records channel message references for live
// synchronous HITL prompts so terminal timeout/disconnect states can be
// reflected back to the original operator-facing message.
PendingMessageUpdateSink HITLPendingMessageUpdateSink
}
ApproveRequest is the bundle handed to ApproverRuntime.Approve. Plugins read whatever they need (a Slack-targeted human approver reads only the summary; an LLM approver reads the full body).
type ApproveVerdict ¶
type ApproveVerdict struct {
Decision string // "allow" | "deny" | "async_pending" | ""
Reason string
By string // who decided ("dashboard:<user>" / "slack:#chan" / "llm:<model>")
ApproverName string // HCL block name, e.g. "pg-staging-secret-columns-judge"
ApproverType string // plugin type, e.g. "llm_approver" / "human_approver" / "dashboard"
}
ApproveVerdict is what an approver returns. "" Decision means the approver couldn't decide (timeout / error) — the caller falls back to the configured fail mode.
ApproverName / ApproverType identify which approver entity produced the verdict. The dispatcher reads these to label the resulting dashboard event with the deciding approver's kind (human / llm / dashboard) and id (the HCL block name) — operators looking at a `denied` row see *why* without having to drill into the rule.
type ApproverRuntime ¶
type ApproverRuntime interface {
Approve(ctx context.Context, req ApproveRequest) (ApproveVerdict, error)
}
ApproverRuntime evaluates one stage of an approve = [...] chain. Built-in approvers (dashboard, human, llm) implement it; out-of-tree plugins ship their own approver type and runtime via the same interface. Return Verdict + reason or surface a timeout.
Implementations live on the approver plugin's decoded body so the dispatcher can type-assert and invoke per-approver logic without new wiring per type.
type BlobStore ¶
type BlobStore interface {
Get(kind, name string) ([]byte, bool, error)
Put(kind, name string, data []byte) error
}
BlobStore is the plugin-facing persistent byte store. Endpoint / credential plugins that need to remember opaque bytes across boots — SSH endpoint host keys, OAuth signing material — call into this instead of touching the filesystem directly.
The host (clawpatrol gateway) backs it with a sqlite table; tests can substitute an in-memory map. Plugins address rows by (kind, name): kind is the plugin's stable namespace ("ssh_host_key", "codex_jwt_keys"), name is whatever sub-key the plugin needs (the endpoint name, or empty string for plugin- singleton blobs).
Why a small typed interface rather than the SecretStore: secrets are credential-bound, owned-by-profile, and resolved through a lookup chain (dashboard / OAuth / env). Blobs are plugin-internal state with no owner concept — separating the contracts keeps each one simple and lets plugin authors pick the right one without stretching SecretStore semantics.
type ClickhouseAuthCredential ¶
ClickhouseAuthCredential is what the clickhouse_native endpoint runtime needs from a credential plugin to inject secrets into the agent's Hello packet. The credential returns its (user, password); the runtime swaps placeholder bytes the agent embedded in the Hello's username / password slots before forwarding the packet upstream. Same shape as PostgresAuth — kept distinct so the checker can confirm a credential is wired for the right protocol.
type ConnEndpointRuntime ¶
type ConnEndpointRuntime interface {
HandleConn(ctx context.Context, ch *ConnHandle) error
}
ConnEndpointRuntime owns request-time handling for protocols that don't fit the http.Request model — postgres, clickhouse_native, any future binary wire protocol. The plugin receives the agent connection (post TLS termination if applicable) plus a connect callback to dial the upstream, walks the compiled rule list with a family-appropriate match.Request, and forwards / denies / pauses for approval per the rule's Outcome.
HandleConn returns when the session ends; errors are logged by the dispatcher.
type ConnEvent ¶
type ConnEvent struct {
Action string // "allow" | "deny" | "approved" | "denied" | "error"
Reason string
Verb string // SQL verb / k8s verb / etc.
Summary string // human-readable one-liner for the event log
Bytes int64 // approximate request size for billing / quotas
Facets map[string]any
// Rule is the matched CompiledRule.Name, "" when no rule fired.
// The host's Emit closure copies it onto the dashboard Event so
// the action-fixture exporter can pin a downloaded action to a
// specific rule (site/doc/clawpatrol-test.md).
Rule string
// Approver* mirror ApproveVerdict — set for Action=="approved" /
// "denied" so the dashboard can show which approver (and what
// kind: human / llm / dashboard) produced the verdict.
Approver string
ApproverType string
ApproverBy string
// ID / Phase give a plugin action the same start→end lifecycle the
// built-in HTTP path uses: the adapter emits Phase "start" (in-flight)
// when an action is evaluated and "end" (carrying Status + result
// facets) when the plugin reports its outcome via ActionResult, keyed
// by the same ID so the dashboard merges them. Empty Phase means a
// single terminal event (the legacy behavior, e.g. a denial).
ID string
Phase string
// Status is the action's outcome — lifted from the result schema's
// title field (e.g. an AWS error code, "200"). Set on the end event.
Status string
// ResultFacets is the after-the-fact report payload (values keyed by
// the facet's result_fields). Set on the end event alongside Status.
ResultFacets map[string]any
// RespBody is the plugin-reported, gateway-capped response sample —
// the after-the-fact counterpart of a request-side body. The plugin
// offers the body as a FACET_STREAM result field; the gateway pulls it
// up to its body-storage cap, appends the truncation marker when the
// body overran the cap, and cancels the stream. Set on the end event,
// it lands on Event.RespBody and renders in the request detail page's
// "Response body" section exactly like the built-in HTTP path's sample.
RespBody string
// RespSha is the hex SHA-256 of the captured response-body sample (at
// most cap+1 bytes), NOT the full upstream body. Unlike the built-in HTTP
// sampler — which hashes every byte — the plugin path deliberately stops
// pulling at the body cap and cancels the stream, so the gateway never
// sees the bytes past the cap and cannot hash them. Empty when the plugin
// reported no body. Set on the end event alongside RespBody.
RespSha string
}
ConnEvent is the wire-protocol-agnostic event shape conn-family plugins emit per request / query.
Facets carries the per-family report payload the host writes to Event.Facets — the result of calling the family's facet.Runtime Report hook against the matched request. Conn plugins populate it when they have the parsed metadata in scope (postgres / clickhouse build it from the *sqlfacet.Meta stashed on mreq.Meta) so the dashboard doesn't have to round-trip through the legacy Verb / Summary squashing.
type ConnHandle ¶
type ConnHandle struct {
Conn net.Conn
Endpoint *config.CompiledEndpoint
Policy *config.CompiledPolicy
// Profile is the device's profile name, looked up from peer IP
// before dispatch. Informational — the host uses it for logging
// and exposes it to external plugins; it is no longer keyed into
// credential lookups.
Profile string
// PeerIP is the agent's source IP. Informational identifier of
// the originating peer.
PeerIP string
// Secrets is the host's SecretStore; plugins use it to fetch
// credential material at session-start time (postgres) or per
// query (rare).
Secrets SecretStore
// DialUpstream connects to the upstream host:port over plain
// TCP. Postgres MITM uses this for the upstream socket.
DialUpstream func(ctx context.Context, network, addr string) (net.Conn, error)
// DialUpstreamTLS dials addr and completes an upstream TLS
// handshake the way the gateway's own HTTPS path does: system
// roots plus the endpoint's EndpointTLSConfigurer and any
// TLSCredentialRuntime client certs. serverName drives SNI and
// certificate verification. nil when the host doesn't support
// it; callers fall back to DialUpstream + their own TLS.
DialUpstreamTLS func(ctx context.Context, network, addr, serverName string) (net.Conn, error)
// Sink is an opaque event-sink callback. Plugins emit per-query
// events; the gateway funnels them to the dashboard SSE +
// JSONL log.
Emit func(ev ConnEvent)
// Approve runs an approve = [...] chain through the host's HITL
// infrastructure. Plugins call it when a matched rule's
// Outcome.Approve is non-empty; the host wraps its
// existing approver registry (dashboard / Slack / LLM) and
// returns the verdict synchronously. nil when the host doesn't
// support HITL for this conn family — plugins must default to
// deny in that case.
Approve func(req ApproveCallRequest) ApproveVerdict
// StateDir is the gateway's persistent state root (matches
// cfg.StateDir). Kept on the handle for tunnel plugins
// (Tailscale's tsnet state dir is derived from it). Endpoint
// plugins should persist material through Blobs instead — see
// ConnHandle.Blobs.
StateDir string
// Blobs is the gateway's plugin-blob store. Endpoint plugins
// that need persistent bytes (SSH host keys, JWT signing keys)
// read / write through it instead of touching the filesystem.
// The host backs it with a sqlite table; tests pass a fake.
Blobs BlobStore
// DstPort is the destination port the agent connection arrived
// on (post-VIP / direct dial). Endpoints whose host strings
// carry a non-default port (`hosts = ["x.com:22222"]`) consult
// this to pick which Hosts[i] the connection corresponds to.
DstPort uint16
// UpstreamHost is the hostname the agent dialed, resolved from
// the VIP table at dispatch time. Populated for VIP-routed conns
// only; empty for direct-IP / fixed-port dispatches (postgres).
// Plugins use it to (a) pass a real hostname to DialUpstream so
// the gateway's host network can resolve it, and (b) drive SNI /
// SAN matching when the protocol layers TLS on top.
UpstreamHost string
// MintCert returns a leaf certificate signed by the gateway CA
// for the given hostname (or IP literal). Plugins that
// TLS-terminate inbound traffic — clickhouse_native with
// `tls = true`, future binary protocols — call this from a
// `tls.Config.GetCertificate` callback so the SAN matches the
// SNI the client sent. nil when the dispatcher can't mint
// (gateway has no CA).
MintCert func(host string) (*tls.Certificate, error)
// BodyStorageCap is the most response-body the gateway buffers when a
// plugin offers a FACET_STREAM result field (cfg.BodyStorageLimit()).
// It matches the cap the built-in HTTP sampler uses, so plugin and
// built-in response samples are bounded identically. Zero means use the
// default (config.DefaultBodyStorageLimit).
BodyStorageCap int
}
ConnHandle bundles everything a ConnEndpointRuntime needs to service one inbound connection. Kept narrow so plugins don't need to import the gateway package.
type ConnIndex ¶
type ConnIndex struct {
// contains filtered or unexported fields
}
ConnIndex maps the WG forwarder's dstIP back to the endpoint(s) that own it. Multiple endpoints can share an IP (e.g. pg-writer / pg-readonly pointing at the same RDS host); the caller filters by profile to pick the one the device should use.
func BuildConnIndex ¶
func BuildConnIndex(policy *config.CompiledPolicy) *ConnIndex
BuildConnIndex walks every endpoint whose body implements ConnRouter and resolves each declared host. DNS failures are logged + skipped; the endpoint stays in the policy and a subsequent resolution attempt (e.g. on policy reload) can still index it. Resolution is best-effort with a short timeout to avoid stalling boot when an upstream is unreachable.
type ConnRouter ¶
type ConnRouter interface {
ConnRouteHosts() []string
}
ConnRouter is the optional interface an endpoint plugin's body implements when its traffic arrives as raw conns (not via SNI). The returned strings are the host[:port] tuples the endpoint claims.
type EKSBearerMinter ¶
type EKSBearerMinter interface {
MintEKSBearer(ctx context.Context, sec Secret, region, cluster string) (string, error)
}
EKSBearerMinter is the credential-plugin contract for minting a k8s-aws-v1.<…> bearer scoped to one (region, cluster) pair. The kubernetes_port_forward tunnel uses this to materialise a temp kubeconfig at Open time, so operators can drop the external awscli + kubeconfig + KUBECONFIG-in-systemd glue: HCL declares the cluster shape (server + ca_cert + cluster_name + region) and binds an aws_credential that satisfies this interface; the tunnel mints, writes, and cleans up the per-tunnel kubeconfig itself.
The HTTPRequestSigner path (kubernetes endpoint, per-request HTTP auth) calls the same underlying STS presigner — both shapes route through one mint helper so signature semantics stay in sync.
type EndpointTLSConfigurer ¶
EndpointTLSConfigurer is the optional hook an endpoint plugin implements when it carries upstream-TLS state that doesn't belong on a credential — most commonly a cluster-scoped CA pin. The kubernetes endpoint implements it to apply its ca_cert HCL field to cfg.RootCAs before the upstream dial: EKS apiservers present per-cluster CAs that no system trust store knows about, and the aws_credential plugin handles bearer auth, not root verification.
Called by dialUpstream before any credential's ConfigureUpstreamTLS, so a credential can still override RootCAs / append Certificates when it has good reason to.
type EnvSecretStore ¶
type EnvSecretStore struct{}
EnvSecretStore reads secret material from process env vars. Lookup key: CLAWPATROL_SECRET_<UPPER_NAME> with hyphens normalized to underscores. Returns an empty Secret (no error) when the var isn't set so the dispatcher can decide between fail-closed and passthrough at the policy level.
type HITLApprovalEffect ¶
type HITLApprovalEffect string
HITLApprovalEffect tells an operator what clicking approve does for the current state. This is separate from OperationState so the UI can render a stable affordance even while async operation wiring evolves.
const ( HITLApprovalEffectExecuteUpstream HITLApprovalEffect = "execute_upstream" HITLApprovalEffectCreateRetryGrant HITLApprovalEffect = "create_retry_grant" )
HITLApprovalEffect values: what clicking approve will do given the current HITLOperationState.
func HITLApprovalEffectForOperationState ¶
func HITLApprovalEffectForOperationState(state HITLOperationState) HITLApprovalEffect
HITLApprovalEffectForOperationState returns the effect that clicking approve will have given the supplied operation state.
type HITLClassifier ¶
type HITLClassifier interface {
Summarize(ctx context.Context, req ApproveRequest) (*HITLSummary, error)
}
HITLClassifier is the optional interface an approver plugin implements to generate a HITLSummary before the HITL notification is sent.
type HITLDecision ¶
HITLDecision is what the pool delivers when an operator approves or denies a pending entry.
type HITLHumanCredentialer ¶
type HITLHumanCredentialer interface {
HumanApproverCredential() string
}
HITLHumanCredentialer is implemented by approvers that route human HITL prompts through a named credential notifier.
type HITLMessageUpdate ¶
type HITLMessageUpdate struct {
MessageRef string
OperationID string
State HITLOperationState
Method string
Host string
Path string
Profile string
UpstreamCalled bool
LastError string
}
HITLMessageUpdate is the payload an approver receives when a HITL decision lands. Credential plugins (e.g. Slack) use it to edit the originating interactive message with the final state.
type HITLMessageUpdateSink ¶
HITLMessageUpdateSink records a channel-specific message reference (for example Slack channel/ts) after a notifier posts a prompt. Implementations must not store tokens or other secrets in ref.
type HITLMessageUpdater ¶
type HITLMessageUpdater interface {
UpdateHITLMessage(ctx context.Context, secrets SecretStore, update HITLMessageUpdate) error
}
HITLMessageUpdater is optionally implemented by notifier credentials that can update an already-posted HITL prompt as the durable operation moves through async states.
type HITLNotifier ¶
type HITLNotifier interface {
NotifyHITL(ctx context.Context, req ApproveRequest, target HITLTarget) error
}
HITLNotifier is the optional interface a credential plugin implements when it can deliver a HITL approval prompt to a human (Slack chat.postMessage, Discord webhook, Telegram sendMessage, SMTP, PagerDuty alert, etc.). HumanApprover dispatches to its configured credential's notifier — adding a new channel = implementing this on a new credential plugin, no main-package changes.
type HITLOperationState ¶
type HITLOperationState string
HITLOperationState is the durable async-HITL operation state copied into human-facing pending prompts. It intentionally lives in runtime (instead of main) so dashboard and channel notifiers can share the same wire labels without importing the gateway package.
const ( HITLOperationStateSyncWaiting HITLOperationState = "sync_waiting" HITLOperationStatePendingApproval HITLOperationState = "pending_approval" HITLOperationStateApprovedWaitingForRetry HITLOperationState = "approved_waiting_for_retry" HITLOperationStateDenied HITLOperationState = "denied" HITLOperationStateExpired HITLOperationState = "expired" HITLOperationStateExecutingUpstream HITLOperationState = "executing_upstream" HITLOperationStateUpstreamSucceeded HITLOperationState = "upstream_succeeded" HITLOperationStateUpstreamFailed HITLOperationState = "upstream_failed" HITLOperationStateClientDisconnected HITLOperationState = "client_disconnected" )
HITLOperationState values: durable lifecycle states for async-HITL operations.
type HITLPending ¶
type HITLPending struct {
ID string `json:"id"`
OperationID string `json:"operation_id,omitempty"`
AgentIP string `json:"agent_ip"`
Host string `json:"host"`
Method string `json:"method"`
Path string `json:"path"`
// OperationState and ApprovalEffect are safety-boundary display
// metadata. They let dashboard/Slack copy distinguish the initial
// synchronous wait (approval forwards upstream immediately) from async
// fallback (approval only grants one matching client retry).
OperationState HITLOperationState `json:"operation_state,omitempty"`
ApprovalEffect HITLApprovalEffect `json:"approval_effect,omitempty"`
UpstreamCalled bool `json:"upstream_called"`
ApprovalMessage string `json:"approval_message,omitempty"`
// Endpoint is the operator-readable identifier for what's being
// called. HITLEndpointLabel-derived: hostname for HTTPS, resource
// name for SQL / k8s where Host is a virtual IP.
Endpoint string `json:"endpoint,omitempty"`
// Family is the endpoint family ("http" | "sql" | "k8s") so the
// dashboard can pick a matching label for Path ("Query" /
// "Resource" / "Path"). Empty when no endpoint metadata is set.
Family string `json:"family,omitempty"`
UA string `json:"ua,omitempty"`
BodySample string `json:"body_sample,omitempty"`
Reason string `json:"reason,omitempty"`
Approvers []string `json:"approvers,omitempty"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
HITLPending mirrors the dashboard's pending-approval shape. Stays here (vs main package) so approver plugins can construct it. JSON tags match the dashboard's existing field names — that endpoint is public API to the in-tree React UI.
type HITLPendingMessageUpdateSink ¶
HITLPendingMessageUpdateSink records a channel-specific message reference against a live synchronous pending HITL request. Implementations must not store tokens or other secrets in ref.
type HITLPool ¶
type HITLPool interface {
// Add publishes a pending entry. Returns the assigned id (used
// by Decide) and a channel that fires exactly once when the
// pool gets a verdict. Caller must select on ctx.Done() too;
// when ctx fires first, prefer HITLPoolCanceler.Cancel so stale
// prompts can explain why the upstream request was not sent.
Add(p HITLPending) (id string, decision <-chan HITLDecision)
// Discard drops a pending entry without recording terminal state.
// Use only for compatibility or best-effort cleanup after resolve.
Discard(id string)
// Decide resolves a pending entry — used by webhook handlers
// (Slack interactive callback, future Discord etc.) to forward
// a side-channel verdict into the same pool the dashboard's
// /api/hitl/decide writes to. Returns false when the id is
// unknown (already resolved or expired).
Decide(id string, d HITLDecision) bool
}
HITLPool is the shared pending-approval surface the dashboard presents to operators. Approver runtimes that need human input (dashboard, Slack human-approver, etc.) call Add to publish an entry and block on the returned channel until the dashboard's PUT /api/hitl/decide signals back.
The pool implementation lives in the gateway main package; runtime only declares the contract so approver plugins can satisfy ApproverRuntime without depending on main.
type HITLPoolAsyncGrantResolver ¶
type HITLPoolAsyncGrantResolver interface {
ResolveAsyncHITLGrant(operationID string, d HITLDecision) HITLResolveResult
}
HITLPoolAsyncGrantResolver lets pools bind a pending human decision to a durable async operation. Implementations must not execute upstream here; approval only moves the operation to a retry-grant state.
type HITLPoolCanceler ¶
type HITLPoolCanceler interface {
Cancel(id string, state HITLState, reason string) HITLResolveResult
}
HITLPoolCanceler is implemented by pools that preserve a short-lived terminal state when a synchronous HITL request ends without a human decision (timeout, client disconnect, gateway cancellation).
type HITLPoolDecider ¶
type HITLPoolDecider interface {
DecideWithResult(id string, d HITLDecision) HITLResolveResult
}
HITLPoolDecider is implemented by pools that can explain why a decision was accepted or rejected. Callers should prefer this over Decide when rendering operator-facing stale-click messages.
type HITLPoolUpdater ¶
type HITLPoolUpdater interface {
Update(id string, mutate func(*HITLPending)) bool
}
HITLPoolUpdater is implemented by pools that can update operator-facing safety-copy metadata for an existing pending prompt without resolving it.
type HITLResolveResult ¶
type HITLResolveResult struct {
OK bool `json:"ok"`
State HITLState `json:"state"`
Reason string `json:"reason,omitempty"`
}
HITLResolveResult is returned by structured HITL resolution APIs. OK means this call transitioned an active pending request. State and Reason are still populated for stale/duplicate decisions so Slack and the dashboard can distinguish timed-out, disconnected, and already decided prompts instead of showing a generic expired message.
type HITLState ¶
type HITLState string
HITLState names the lifecycle state of a human approval prompt.
const ( HITLStatePending HITLState = "pending" HITLStateApproved HITLState = "approved" HITLStateDenied HITLState = "denied" HITLStateTimedOut HITLState = "timed_out" HITLStateClientDisconnected HITLState = "client_disconnected" HITLStateCanceled HITLState = "canceled" HITLStateUnknown HITLState = "unknown" )
HITL terminal states exposed to dashboard and Slack stale-click handlers.
type HITLSummary ¶
type HITLSummary struct {
Subject string `json:"subject"`
Label string `json:"label"`
Confidence int `json:"confidence"` // 0-100; 0 = not provided
Summary string `json:"summary"`
}
HITLSummary is optional pre-computed request context from a classifier LLM. When set on HITLTarget, notifiers build a richer approval card instead of the generic method/path display.
type HITLTarget ¶
type HITLTarget struct {
CredentialName string // bare name — for SecretStore.Get
Channel string // routing target (#chan / chat_id / email)
Interactive bool // approve/deny buttons vs. dashboard-only
PendingID string // pool's pending entry id
DashboardURL string // for fallback dashboard link in non-interactive mode
ThreadTS string // if set, post as a reply in this Slack thread
// OperationState / ApprovalEffect / UpstreamCalled / ApprovalMessage
// mirror HITLPending's safety copy so channel notifiers can tell
// operators whether an approval executes upstream immediately or only
// authorizes a one-shot retry grant.
OperationState HITLOperationState
ApprovalEffect HITLApprovalEffect
UpstreamCalled bool
ApprovalMessage string
// Summary is optional pre-computed request context. When non-nil,
// notifiers render a richer card instead of the generic method/path display.
Summary *HITLSummary
// Message is an optional pre-expanded template string. When non-empty,
// notifiers use it as the section text, overriding the default path
// display and the Summary card.
Message string
// MessageUpdateSink records a notifier-specific, non-secret message ref
// for async HITL operation status updates.
MessageUpdateSink HITLMessageUpdateSink
// PendingMessageUpdateSink records a notifier-specific, non-secret
// message ref for synchronous pending HITL terminal updates.
PendingMessageUpdateSink HITLPendingMessageUpdateSink
}
HITLTarget is the per-approver config the notifier needs: where to send the prompt, whether to render interactive buttons, and the pending entry's id (for action_id payload encoding).
type HTTPCredentialRedactionProvider ¶ added in v0.2.10
type HTTPCredentialRedactionProvider interface {
ConsumeHTTPRedactions(req *http.Request) []string
}
HTTPCredentialRedactionProvider is an optional companion to HTTPCredentialRuntime for injectors that derive additional sensitive values at request time. The gateway calls ConsumeHTTPRedactions after InjectHTTP and adds the returned values to request audit redaction. Implementations should forget the values they return for req.
type HTTPCredentialRuntime ¶
type HTTPCredentialRuntime interface {
InjectHTTP(ctx context.Context, req *http.Request, sec Secret) error
}
HTTPCredentialRuntime is the credential-plugin contract for HTTP auth shapes (bearer / cookie / header / mtls / OAuth-with-bearer). Inject mutates req.Header (and maybe req.URL if cookies involve a path); the secret string is fetched out-of-band by the host (e.g. via clawpatrol's existing OAuthRegistry) and passed in as Secret.
Implementations live next to their config plugin so the schema and runtime stay co-located, mirroring unclaw's plugin layout.
Callers MUST check whether the injector also implements HTTPCredentialRedactionProvider and, if so, call ConsumeHTTPRedactions(req) after InjectHTTP returns — including on inject error. Skipping the consume strands the injector's per-request redaction state for the lifetime of the credential instance.
type HTTPRequestRewriter ¶ added in v0.3.0
type HTTPRequestRewriter interface {
RewritesHTTPRequest() bool
}
HTTPRequestRewriter is an optional marker on an HTTPCredentialRuntime whose InjectHTTP may rewrite the request beyond headers — its URL or body — and, in doing so, consume the request body. When such an injector's InjectHTTP returns an error the request is left unusable (the body was streamed away), so the gateway MUST fail closed rather than forward it. Header-only injectors do not implement this; their failed injection leaves the request untouched and forwarding the agent's placeholder is safe.
type HTTPRequestSigner ¶
type HTTPRequestSigner interface {
SignHTTPRequest(ctx context.Context, req *http.Request, sec Secret, endpoint any) error
}
HTTPRequestSigner is the credential-plugin contract for HTTP auth shapes whose signature spans the *whole request* (method, URL, headers, body) and need parameters that live on the endpoint, not the credential. AWS SigV4 is the canonical example: the signature depends on the service + region declared on the endpoint, plus a hash of the request body. HTTPCredentialRuntime.InjectHTTP only gets the request, not the endpoint, so signing schemes that need endpoint context implement this instead.
SignHTTPRequest may consume and replace req.Body (typically reading it in full to hash, then restoring with io.NopCloser); callers must not assume the body is preserved verbatim.
The endpoint argument is the endpoint plugin's decoded Body (the same value as CompiledEndpoint.Body). The signer type-asserts it against an interface that exposes the parameters it needs (e.g. `AWSSigningParams() (service, region string)`).
type HTTPSyntheticResponder ¶
type HTTPSyntheticResponder interface {
RespondHTTP(ctx context.Context, req *http.Request) (*http.Response, bool, error)
}
HTTPSyntheticResponder is the optional contract an endpoint plugin's runtime implements when it needs to short-circuit certain matched requests and return a synthetic response without forwarding upstream. The openai_codex_https endpoint uses it to serve a clawpatrol-controlled JWKS at chatgpt.com's agent-identity URL (so a JWT we minted ourselves validates) and to stub out the agent-task registration POST.
RespondHTTP is called by mitmHTTPS before credential injection. Returning (resp, true, nil) writes resp to the agent and skips forwarding; returning (_, false, nil) falls through to the normal inject + proxy path. Errors are logged and the request still forwards verbatim.
The hook lives on the endpoint plugin (not the credential) so the behavior is bound to the protocol surface, not to whichever bearer happens to be configured for it.
type PlaceholderDetector ¶
PlaceholderDetector is the optional contract an endpoint plugin's runtime implements so the multi-credential dispatch logic can ask it: "given this incoming request and these candidate placeholders, which one (if any) did the agent send?"
The returned string must be one of `candidates` exactly, or "" if no placeholder matched (the caller then falls back to the no-placeholder credential entry, when one exists).
Why an endpoint-plugin method rather than a callback handed to ResolveCredential: each protocol family hides placeholders in a different slot. HTTPS scans the Authorization header. Postgres reads the StartupMessage password. Putting the extraction logic on the endpoint plugin keeps the dispatcher protocol-agnostic.
Endpoints with only singular `credential = X` bindings don't need to implement this — ResolveCredential short-circuits before calling it.
type PostgresAuthCredential ¶
PostgresAuthCredential is what the postgres endpoint runtime needs from a credential plugin to terminate upstream auth. The credential returns its (user, password) — runtime drives the SCRAM / cleartext handshake itself, so the agent never sees an auth challenge.
type PostgresCredentialRuntime ¶
type PostgresCredentialRuntime interface {
InjectPostgres(ctx context.Context, startup *PostgresStartup, sec Secret) error
}
PostgresCredentialRuntime swaps the agent's StartupMessage password for the real one before the upstream connect. The wire-protocol front-end calls this once per session.
type PostgresStartup ¶
PostgresStartup is the view a postgres credential plugin sees of the StartupMessage it must rewrite. The wire-protocol front-end fills it; the credential plugin updates Password + optionally User.
type Request ¶
Request is re-exported here so callers don't have to import config/match for the placeholder-detector signature.
type SQLParser ¶
SQLParser is the optional contract a SQL-family endpoint plugin's runtime implements so a host that received a raw SQL string (rather than a live wire-protocol frame) can populate `match.Request.Meta` using the same parser the live dispatch path uses. The fixture loader behind `clawpatrol test` reads only `"statement": "..."` from each fixture and calls this to recover verb / tables / functions before running rule matching, so the format stays operator-friendly (site/doc/clawpatrol-test.md).
Implementations return the per-family `*sqlfacet.Meta` value the SQL matcher expects on `match.Request.Meta`, plus a bool reporting whether the parser refused the input. When the bool is true the fixture loader sets `match.Request.Unparseable` so the unevaluable fail-close (parser facets become CEL unknowns) runs against the test rule set, mirroring live wire-frame dispatch. Endpoints whose runtime doesn't implement this aren't usable as SQL test fixtures.
type Secret ¶
type Secret struct {
Kind string
Bytes []byte
// Extras is plugin-specific. mTLS passes cert / key / chain;
// postgres passes user; OAuth passes refresh token + expiry.
Extras map[string]string
}
Secret is what credential plugins receive at injection time. The Bytes are the actual secret material; Kind disambiguates what shape the credential expects (bearer / api-key / cookie / mTLS bundle / postgres password / ...). The host (clawpatrol) fetches the value from its existing oauth.go store before calling the plugin.
type SecretStore ¶
SecretStore returns the secret material a credential plugin's InjectHTTP / InjectPostgres needs at request time. Lookup key is the credential's bare name (e.g. "github"). A credential declared in HCL maps to exactly one secret — operators wanting distinct material for two callers should declare two credentials and bind each to its own endpoint.
Implementations live outside the config package because the secret store is a host concern, not a policy concern. The default env-var store is lightweight enough for development; the gateway wires the existing OAuthRegistry behind this interface for OAuth-flow credentials (anthropic / codex / notion / etc.) so refresh + token persistence flow through the same path.
type TLSCredentialRuntime ¶
TLSCredentialRuntime customizes the upstream TLS configuration before the dial. mTLS credentials use this to add a client cert (Certificates) and an optional custom root pool (RootCAs); other shapes — pinned-cert, ALPN-twiddling — extend via the same hook without changing the call site.
Implementations mutate cfg in place. Secret.Extras typically holds "cert" / "key" / "ca" PEM blobs; the env-var store populates them from CLAWPATROL_SECRET_<NAME>_{CERT,KEY,CA} (with @path/to/file shorthand for reading PEM bundles off disk).
type Tunnel ¶
type Tunnel interface {
// Dial opens an upstream connection through the tunnel. addr
// is what the endpoint asks for ("host:port", typically the
// hostname recovered from the VIP table). Tunnels that own
// the upstream addressing (cloud_sql_proxy's local listener,
// kubectl port-forward's ephemeral port) ignore addr and dial
// their own configured target.
Dial(ctx context.Context, network, addr string) (net.Conn, error)
// Close tears down the underlying transport. The manager calls
// it exactly once when refcount reaches 0 and the configured
// keepalive window has elapsed.
Close() error
}
Tunnel is the live runtime handle the manager hands to the dispatcher. The dispatcher calls Dial to open an upstream connection for one inbound conn; Close runs when the manager decides this instance has been idle long enough.
type TunnelCredential ¶
TunnelCredential is the slice of a credential entity a tunnel plugin needs to drive secret lookup and runtime behaviour. The host populates it from the credential symbol resolved at compile time, so plugins don't have to reach back into the registry.
Body is the credential plugin's decoded HCL struct (same value stored on Entity.Body). Plugins type-assert it to their expected shape — e.g. ssh_port_forward asserts to the ssh credential's runtime interface to read username + private-key handling.
type TunnelCredentialNamer ¶
type TunnelCredentialNamer interface {
// CredentialName returns the bare credential name that owns this
// tunnel's identity, or "" if the tunnel wasn't credential-driven.
CredentialName() string
}
TunnelCredentialNamer is an optional interface a Tunnel implements when its identity is owned by a named credential. The TunnelManager uses it to route DisconnectCredential to matching live instances.
type TunnelDisconnector ¶
TunnelDisconnector is an optional interface a Tunnel implements when it can perform a clean upstream sign-off (e.g. tsnet's LocalClient Logout deregisters the node from the control plane) before the manager calls Close. The manager invokes Disconnect first, then force-evicts the entry. A Disconnect error is non-fatal — the caller proceeds with the eviction so a stuck upstream doesn't pin local state.
type TunnelHost ¶
type TunnelHost struct {
// Name is the tunnel's HCL name, useful for log line prefixes.
Name string
// SecretStore lets a tunnel plugin fetch its credential's
// secret bytes by name (the credential ref resolves at compile
// time; the actual material lives in the host's secret store).
SecretStore SecretStore
// Credential is the resolved credential entity for this tunnel,
// or nil if the HCL didn't specify one. The plugin reads
// Credential.Name to feed SecretStore.Get and type-asserts
// Credential.Body to its expected runtime interface (e.g. ssh
// credentials expose a known shape).
Credential *TunnelCredential
// StateDir is the gateway's persistent state root (cfg.StateDir).
// Tunnel plugins that need to persist material between policy
// reloads — e.g. tailscale tsnet's state directory — derive
// paths from this. Always non-empty (the host resolves a default
// when no explicit StateDir is configured).
StateDir string
// Logger is a per-tunnel logger pre-tagged with the tunnel name.
// Plugins should prefer it over package-level log.Printf so the
// dashboard's log surface attributes lines correctly.
Logger *log.Logger
}
TunnelHost bundles host-side dependencies a tunnel plugin's Open callback may need. Kept narrow so plugin packages don't have to import the gateway main package.
Open is invoked as a method on the plugin's decoded body (the same struct the loader populated from HCL), so the plugin reads its own configuration directly off `t` — TunnelHost only carries the host- side bits (logger, secret store, etc.) that aren't expressible in HCL.
type TunnelReconciler ¶ added in v0.2.5
type TunnelReconciler interface {
ReconcileOrphans(ctx context.Context, host TunnelHost) error
}
TunnelReconciler is an optional interface a tunnel plugin implements when it leaves out-of-band cluster state (e.g. a kubectl-created jump pod) that a daemon crash can orphan. The host calls ReconcileOrphans once at startup, after config load and before serving, for every declared tunnel — the plugin sweeps resources it owns that no live tunnel references. Best-effort: a returned error is logged, not fatal.
Only safe to call at startup, when no tunnel instance is live: a running gateway may legitimately own pods a sweep would otherwise delete.
type TunnelRuntime ¶
type TunnelRuntime interface {
// Sharing reports the plugin's preferred sharing model when the
// operator hasn't set `share = ...` on the block. The manager
// honours the HCL override when present.
Sharing() TunnelSharing
// Open builds a fresh Tunnel instance. The manager calls this
// exactly once per (tunnel, sharing-key) regardless of refcount
// — refcounting is the manager's job, not the plugin's.
//
// For chained tunnels, `via` is the underlying Tunnel handle
// (already Acquire'd by the manager and ref-attached to the
// child); the plugin uses `via.Dial(...)` instead of
// `net.Dial(...)` to bring up its transport. nil when the
// HCL has no `via = ...`.
Open(ctx context.Context, host TunnelHost, via Tunnel) (Tunnel, error)
}
TunnelRuntime is the request-time contract a tunnel plugin's body implements. The TunnelManager (host-side, in the main package) owns the lifecycle: refcounting, idle teardown, the `via` chain. The plugin owns one job — bringing up an underlying transport in Open and exposing Dial / Close on the returned Tunnel handle.
Plugins that need to fan out one HCL declaration into multiple runtime instances (per_endpoint or per_conn sharing) don't have to do anything special: the manager calls Open once per distinct (tunnel-name, sharing-key) tuple and refcounts each instance independently.
type TunnelSharing ¶
type TunnelSharing = string
TunnelSharing controls how the manager keys runtime instances of a tunnel against repeat Acquire calls. Aliased to string so the compile pass (in config/) can type-assert `Sharing() string` on a plugin's runtime without taking a dependency on this package and inviting an import cycle.
const ( // All endpoints, all connections, share the same Tunnel. Idle // teardown is shared too. Right default for tailscale (one node // in the tailnet) and singleton local listeners (cloud_sql_proxy). TunnelShareSingleton TunnelSharing = "singleton" // endpoint) pair. Right default for kubernetes_port_forward, // where each endpoint allocates its own ephemeral local port // and two endpoints can't share the same forwarder. TunnelSharePerEndpoint TunnelSharing = "per_endpoint" // torn down on conn close. Niche but useful for stateless // "tunnels" whose lifetime should track a single request. TunnelSharePerConn TunnelSharing = "per_conn" )
type WebSocketCredentialRuntime ¶
type WebSocketCredentialRuntime interface {
RewriteWebSocketPayload(ctx context.Context, payload []byte, sec Secret) ([]byte, bool, error)
}
WebSocketCredentialRuntime is the credential-plugin contract for server-bound WebSocket text payloads that carry token placeholders. The gateway calls this after decoding/unmasking a complete text frame but before forwarding it upstream; implementations must return a new plaintext payload and indicate whether the frame must be rebuilt. Inspectors still receive the original placeholder-bearing payload so real secrets are not emitted to logs or the dashboard.
type WebhookCtx ¶
type WebhookCtx struct {
CredentialName string
Secrets SecretStore
HITL HITLPool
Policy *config.CompiledPolicy
}
WebhookCtx is everything a credential's webhook handler may need from the gateway runtime. The credential's name is included so a shared handler factory can identify which credential it's serving.
type WebhookHandler ¶
type WebhookHandler func(ctx WebhookCtx, w http.ResponseWriter, r *http.Request)
WebhookHandler is the request handler — same as http.HandlerFunc plus a WebhookCtx with the runtime services the credential plugin needs (its own secret, the HITL pool, the policy snapshot).
type WebhookProvider ¶
type WebhookProvider interface {
WebhookRoutes() []WebhookRoute
}
WebhookProvider is the optional interface a credential body implements when it wants to receive callbacks from the upstream service.
type WebhookRoute ¶
type WebhookRoute struct {
Path string
Handler WebhookHandler
}
WebhookRoute is one mount under the credential's subtree. Path MUST start with "/" and gets joined under /api/cred/<credName>.