Documentation
¶
Overview ¶
Package config loads and validates clawpatrol's HCL gateway config.
The config has two layers. Operational fields (listen / state_dir / tailscale {} / ...) live at the top of the file and decode via gohcl into the Gateway struct. Policy blocks (defaults / approver / credential / endpoint / rule / profile) are dispatched to plugins by their first label; each plugin owns its body schema, the in-memory record it builds, and (later) the runtime that handles requests for it.
Index ¶
- Constants
- func AddPluginChecker(fn func(*Plugin) []string)
- func BodyBufferLimitFromString(s string) (int64, error)
- func BodyStorageLimitFromString(s string) (int64, error)
- func CredentialEndpointTargets(ent *Entity) []string
- func DashboardSessionTTLFromString(s string) (time.Duration, error)
- func Emit(gw *Gateway) ([]byte, error)
- func MatchDashboardOperator(login string, allowlist []string) bool
- func ParseSize(s string) (int64, error)
- func Register(p *Plugin)
- func SetIdent(b *hclwrite.Body, name, ident string)
- func SetIdentList(b *hclwrite.Body, name string, idents []string)
- func SetPluginLoader(p PluginLoader)
- func StringListVal(xs []string) cty.Value
- func TraversalTokens(s string) hclwrite.Tokens
- func Types(kind Kind) []string
- func ValidateDashboardOperatorEntry(entry string) error
- func ValidateHITLAsyncGrant(name string, syncWaitTimeout string, grant *HITLAsyncGrantConfig) hcl.Diagnostics
- type ApproveStage
- type BuildCtx
- type CompiledCredential
- type CompiledEndpoint
- type CompiledPolicy
- type CompiledProfile
- type CompiledRule
- type CompiledTunnel
- type CredentialDisambiguatorBody
- type Defaults
- type Entity
- type EnvPushdownProvider
- type EnvVar
- type FileIncludable
- type FileIncludeField
- type FrameworkAttrSpec
- type FrameworkAttrs
- type Gateway
- func (g *Gateway) ActionsKeep() string
- func (g *Gateway) BodyBufferLimit() int
- func (g *Gateway) BodyStorageLimit() int
- func (g *Gateway) DashboardConfigWrites() bool
- func (g *Gateway) DashboardListen() string
- func (g *Gateway) DashboardSessionTTL() string
- func (g *Gateway) Dump() ([]byte, error)
- func (g *Gateway) Funnel() bool
- func (g *Gateway) GenAITelemetryEnabled() bool
- func (g *Gateway) GenAITelemetryIncludeContent() bool
- func (g *Gateway) IsTailscaleEnabled() bool
- func (g *Gateway) IsWireGuardEnabled() bool
- func (g *Gateway) Join() JoinConfig
- func (g *Gateway) LogPath() string
- func (g *Gateway) Operators() []string
- func (g *Gateway) PublicURL() string
- func (g *Gateway) ResolvedStateDir() string
- func (g *Gateway) Resolver() string
- func (g *Gateway) SessionKeep() string
- func (g *Gateway) SetPublicURL(s string)
- func (g *Gateway) StateDir() string
- func (g *Gateway) Telemetry() *bool
- type GatewaySettings
- type GenAITelemetryBlock
- type HITLAsyncGrantConfig
- type HITLAsyncGrantEnabler
- type HostPattern
- type JoinConfig
- type Kind
- type LimitsBlock
- type NonInjectingCredential
- type OAuthConfig
- type OAuthFlowProvider
- type OAuthIntegration
- type OptionalScope
- type OptionalScopeGroup
- type Outcome
- type Plugin
- type PluginLoader
- type PluginSource
- type Policy
- type Profile
- type RefIndex
- type RefSpec
- type Refs
- type SecretSlot
- type SecretSlotsProvider
- type Symbol
- type SymbolTable
- type TailscaleBlock
- type TunnelCommon
- type TunnelCommonRead
- type WireGuardBlock
Constants ¶
const ( // MinSchemaVersion is the oldest config grammar this binary still // decodes. 0 covers unversioned legacy configs (no top-level // `schema_version` attribute); they load with a deprecation // warning. Bump this only at a major when ancient syntax is // finally dropped. MinSchemaVersion = 0 // MaxSchemaVersion is the newest grammar this binary understands. // Bump it whenever a breaking grammar change ships. Configs // declaring a higher version are rejected with an "upgrade // clawpatrol" error rather than a wall of decode noise. MaxSchemaVersion = 1 )
const ( // HITLAsyncDefaultApprovedRetryTTL is the default lifetime of the // one-shot retry grant after a human approval. HITLAsyncDefaultApprovedRetryTTL = 5 * time.Minute // HITLAsyncDefaultMaxBodyBytes is the default body size limit for // raw-body request fingerprinting. HITLAsyncDefaultMaxBodyBytes int64 = 1 << 20 // HITLAsyncHardMaxBodyBytes caps v1 raw-body fingerprinting so an // approver config cannot make Claw Patrol buffer unbounded bodies. HITLAsyncHardMaxBodyBytes int64 = 8 << 20 // HITLAsyncFingerprintRawBody is the only v1 fingerprint mode. HITLAsyncFingerprintRawBody = "raw" )
const ( // DefaultBodyBufferLimit is the fallback for // gateway.limits.body_buffer: the most request/response body the // gateway buffers before handing it to the rules engine. 1 MiB // preserves the historical hardcoded maxHTTPMatchBody value. DefaultBodyBufferLimit int64 = 1 << 20 // DefaultBodyStorageLimit is the fallback for // gateway.limits.body_storage: the most body the gateway keeps // when persisting an action's request/response sample to the audit // log. 4 KiB preserves the historical hardcoded sampler cap. DefaultBodyStorageLimit int64 = 4096 )
const DefaultDashboardSessionTTL = 24 * time.Hour
DefaultDashboardSessionTTL is the fallback when gateway.hcl omits dashboard_session_ttl. 24 hours is short enough that a stolen cookie self-expires within a working day while long enough that operators don't re-type the password between coffee breaks.
const KeepaliveAlwaysSentinel = time.Duration(-1)
KeepaliveAlwaysSentinel is the duration value that means "pin the tunnel up for the lifetime of the policy that declared it". Plugins / loaders set CompiledTunnel.KeepaliveAlways=true rather than picking a magic duration; this constant exists so callers have a name for the "no idle teardown" case when they need to log it.
Variables ¶
This section is empty.
Functions ¶
func AddPluginChecker ¶
AddPluginChecker installs fn as a validator that runs at every future Register and retroactively against every already-registered plugin. Validators that return a non-empty []string panic the registration so plugin bugs surface at init time, not at first request.
Used by config/runtime to enforce that Plugin.Runtime, when set, satisfies the expected interface for its Kind. Living as a callback here (rather than a config-package import of runtime) avoids a cycle: runtime already imports config; config doesn't depend on runtime.
func BodyBufferLimitFromString ¶ added in v0.2.5
BodyBufferLimitFromString parses the body buffer cap, falling back to DefaultBodyBufferLimit when the string is empty.
func BodyStorageLimitFromString ¶ added in v0.2.5
BodyStorageLimitFromString parses the body storage cap, falling back to DefaultBodyStorageLimit when the string is empty.
func CredentialEndpointTargets ¶ added in v0.2.3
CredentialEndpointTargets returns the endpoint names a credential binds. Reads either the singular `endpoint` framework attr or the list-form `endpoints`; cross-credential validation has already rejected the both-set case.
This is the canonical "credentials bind endpoints" direction. CompiledEndpoint.Credentials is the inverted index built from these targets — callers asking "which endpoints does credential C declare?" should use this function, not walk every endpoint's Credentials list.
func DashboardSessionTTLFromString ¶
DashboardSessionTTLFromString parses a string like "24h" / "30m" into a positive duration. Empty input returns DefaultDashboardSessionTTL. Used by the validator (to surface bad input at load) and by the gateway at runtime (to convert to a concrete time.Duration before minting sessions).
func Emit ¶
Emit serializes a loaded *Gateway back to HCL. The output is deterministic (operational fields first, then kind-grouped policy blocks in source order) and re-parsable by Load — round-tripping fixtures through Emit + Load produces a structurally identical *Gateway, modulo comment loss (hclwrite can't preserve operator comments through gohcl decode).
Per-block emission delegates to the plugin's Emit hook so each plugin owns its own body shape — credential bindings, match objects, family-specific endpoint fields all live next to the schema they correspond to.
func MatchDashboardOperator ¶
MatchDashboardOperator reports whether login matches any entry in allowlist. Entries are either:
- "user@domain" — exact match against the whois login.
- "*@domain" — matches any login ending in "@domain".
Empty login never matches (a missing whois identity must not be papered over). Empty allowlist never matches.
func ParseSize ¶ added in v0.2.5
ParseSize parses a human-readable byte-size string into a count of bytes. It accepts an optional unit suffix (case-insensitive): bare / "B" = bytes, "KiB"/"K", "MiB"/"M", "GiB"/"G" — all binary (1 KiB = 1024 bytes). A bare integer is interpreted as bytes. Zero and negative values are rejected.
func Register ¶
func Register(p *Plugin)
Register installs a plugin. Called from each plugin package's init(). Duplicate (Kind, Type) pairs panic — they always indicate a build-time mistake.
func SetIdent ¶
SetIdent writes `name = a.b` where the value is a dotted traversal (e.g. `credential = header_token.github`). The ident string may be a single identifier or a dotted traversal — splitting on '.' yields the token sequence.
func SetIdentList ¶
SetIdentList writes `name = [a.x, b.y, c.z]` where each element is a dotted traversal expression. Used for typed ref lists like `endpoints = [https.github, slack_tokens.dev]`. Pass each entry as its fully-qualified traversal string (use RefIndex.Ref to build).
Exported so plugin Emit hooks can use it for fields like a rule's `endpoints = [...]` ref list.
func SetPluginLoader ¶
func SetPluginLoader(p PluginLoader)
SetPluginLoader installs the loader used by every subsequent Load / LoadBytes call. Pass nil to revert to the default no-op.
func StringListVal ¶
StringListVal lifts a Go []string into a cty.ListVal. Exported so plugin Emit hooks can use it for `hosts = [...]` style attributes. cty.ListValEmpty is required for the empty case because cty.ListVal(nil) panics — gocty inference can't pick the element type from an empty slice.
func TraversalTokens ¶
TraversalTokens is the exported form of traversalTokens, for plugin Emit hooks that build attribute token lists by hand (e.g. the rule plugin's `approve = [a, b, c]` list).
func Types ¶
Types returns every registered Type for the given kind, sorted. Used to render "unknown <kind> type \"X\" — known types: ..." hints.
func ValidateDashboardOperatorEntry ¶
ValidateDashboardOperatorEntry checks the shape of a single allowlist entry. Returns nil for "user@domain" or "*@domain", an error otherwise.
func ValidateHITLAsyncGrant ¶
func ValidateHITLAsyncGrant(name string, syncWaitTimeout string, grant *HITLAsyncGrantConfig) hcl.Diagnostics
ValidateHITLAsyncGrant returns HCL diagnostics for an async-grant config block. It checks that, when the grant is enabled, the grant's TTLs are well-formed. sync_wait_timeout is optional: when set it must be a positive duration, but omitting it is valid — the request is then parked synchronously for the full approval window with no early 202 hand-back, and the async retry-grant path simply never activates.
Types ¶
type ApproveStage ¶
type ApproveStage struct {
Name string `json:"name"`
}
ApproveStage is one node in an approve = [...] chain — a bare-name reference to an approver block. LLM policy text and cache TTL ride on the approver block itself (see LLMApprover), so the use site stays a single bare name. Lives here so runtime callers don't need to import the rules plugin package.
type BuildCtx ¶
type BuildCtx struct {
Refs *Refs
Symbols *SymbolTable
Block *hcl.Block // for diagnostic ranges when nothing more precise is available
}
BuildCtx is what the loader hands to Validate and Build. It bundles the standard pre-resolved Refs (from RefSpec entries) with the symbol table, so plugins can resolve names embedded in shapes that don't fit the RefSpec.Path mini-DSL — most notably bare-name fields inside `match = { credential = X }` cty.Value attributes.
type CompiledCredential ¶
CompiledCredential expands a credential's `endpoint = X` / `endpoints = [...]` binding into a per-endpoint entry. The Disambiguators map carries the merged dispatch discriminators (block-side values from the credential body's CredentialDisambiguatorBody + framework-peeled `placeholder`, overlaid with profile-side values from `{ credential = X, <field> = "..." }` inline entries). An empty / nil map marks the no-constraint catchall entry. Dispatch picks the most-specific matching entry per request — see runtime.ResolveCredential.
type CompiledEndpoint ¶
type CompiledEndpoint struct {
Name string
Family string // "http" | "sql" | "k8s"
Plugin *Plugin
Body any
Hosts []string
Credentials []*Entity // credentials globally bound to this endpoint
Rules []*CompiledRule // sorted by priority desc
// Description is the operator-supplied free-text note from the
// block's `description = "..."` framework attr, or "" if unset.
// Surfaced in the discovery manifest to orient agents.
Description string
// Retention is the raw `retention = "..."` framework attr, or "" if
// unset. When set it overrides gateway.actions_keep for this
// endpoint's rows in the action-log sweeper. time.ParseDuration
// format; "0" / "off" (or any zero-valued duration like "0s") keeps
// this endpoint's rows forever. Validated at compile.
Retention string
// InspectsTruncatable is true when any rule on this endpoint reads a
// facet whose bytes a wire frontend buffers under a cap (for ssh:
// ssh.stdin; the http/sql bodies are buffered by their own frontends
// on a coarser heuristic). The ssh runtime reads it to keep the
// channel splice byte-for-byte unchanged when no rule needs stdin —
// inspection is opt-in per endpoint, not paid by every connection.
// Computed once at compile time from the per-matcher
// InspectsTruncatableFacet() bool.
InspectsTruncatable bool
// Tunnel is the resolved tunnel this endpoint dials through, or
// nil for endpoints reached over the gateway's plain dialer.
// Populated from the endpoint plugin's optional EndpointTunnel()
// accessor.
Tunnel *CompiledTunnel
}
CompiledEndpoint flattens an endpoint plus the rules that target it. Body is whatever the endpoint plugin's Build returned (e.g. *endpoints.HTTPSEndpoint) — runtime callers type-assert based on Family.
Credentials is the global set of credential Entities whose `endpoint` / `endpoints` framework attr names this endpoint. The list is profile-agnostic — used by code that inspects an endpoint's binding (TLS cert lookup, dashboard cards). Per-request credential dispatch reads CompiledProfile.EndpointCredentials instead, because the placeholder discriminator lives on the profile in v15.
func MatchHostPattern ¶
func MatchHostPattern(patterns []HostPattern, hostname string) *CompiledEndpoint
MatchHostPattern is the matcher used at dispatch time. Returns the first endpoint whose pattern matches hostname (patterns must already be sorted by descending pattern length). Hostname must already be lowercased; patterns are stored lowercased by Compile.
func (*CompiledEndpoint) RequiresVIP ¶
func (ce *CompiledEndpoint) RequiresVIP() bool
RequiresVIP reports whether DNS-VIP allocation should claim this endpoint's hostnames. True when the body opts in via the dnsvip.RequiresVIP marker OR when the endpoint dials through a tunnel — tunneled upstreams can't be resolved by the agent's resolver, so VIP-routing is the only way to recover the hostname → endpoint mapping at conn-accept time.
Implemented as a method (rather than a stored bool) so the dnsvip package — which already type-asserts on a `RequiresVIP() bool` shape — keeps working unchanged once it's pointed at the CompiledEndpoint instead of the body.
type CompiledPolicy ¶
type CompiledPolicy struct {
// Policy fallbacks (mirrored from the top-level Gateway fields).
UnknownHost string
LLMFailMode string
LLMCacheTTL int
HumanTimeout int
HumanOnTimeout string
// DashboardURL mirrors gateway.public_url — the canonical URL where
// an operator reaches the dashboard to configure device profiles.
// Surfaced here so the discovery manifest can point an agent (or its
// human) at the dashboard when its profile grants nothing. Empty when
// public_url is unset.
DashboardURL string
// Profiles indexed by name. Each holds a per-endpoint rule list,
// already family-tagged and priority-sorted.
Profiles map[string]*CompiledProfile
// Endpoints contains every declared endpoint, keyed by name.
// Useful for callers that don't care about profile scoping
// (status pages, dashboard listings).
Endpoints map[string]*CompiledEndpoint
// Tunnels contains every declared tunnel, keyed by name. The
// TunnelManager (host-side) walks this on policy reload to diff
// old vs new tunnels. Endpoints store a *CompiledTunnel pointer
// in their Tunnel field — same instance as the entry here.
Tunnels map[string]*CompiledTunnel
// Approvers / Credentials surface the same entities from the
// Policy struct under a runtime-friendly typed alias — they're
// pointers into the same Entity records, no copies.
Approvers map[string]*Entity
Credentials map[string]*Entity
}
CompiledPolicy is the runtime-friendly view of a loaded gateway: per-profile maps that the request handler walks at dispatch time. Build with Compile after Load.
func Compile ¶
func Compile(gw *Gateway) (*CompiledPolicy, error)
Compile lowers a *Gateway into a *CompiledPolicy. Errors surface as Go errors (not hcl.Diagnostics) — semantic validation has already run at Load time; Compile only fails when a plugin's match map is shaped in a way the matcher can't compile (e.g. malformed regex).
type CompiledProfile ¶
type CompiledProfile struct {
Name string
Credentials []*Entity
Endpoints map[string]*CompiledEndpoint
HostIndex map[string]*CompiledEndpoint
HostPatterns []HostPattern
EndpointCredentials map[string][]*CompiledCredential
HITLAsyncGrants bool
}
CompiledProfile binds an identity to the endpoint set its requests dispatch against. Endpoints map by name; HostIndex maps exact declared hosts plus bare-host aliases for HTTPS-family default-port declarations to the endpoint that owns them for fast SNI / authority lookup. HostPatterns captures wildcard `*.suffix` declarations the dispatcher walks when HostIndex misses. CompiledEndpoint.Hosts keeps the operator-declared strings unchanged.
Endpoint membership is the transitive closure profile → credentials → endpoints: a profile names credentials, and each credential names the endpoint(s) it authenticates against. Credentials surfaces the raw HCL list (one entry per name as written); Endpoints surfaces the resolved set, deduplicated when multiple listed credentials bind the same endpoint.
EndpointCredentials is the profile-scoped credential dispatch table: for each endpoint reached via this profile, the *CompiledCredential entries that the request-time placeholder detector matches against. The Placeholder string on each entry comes from the profile's inline `{ placeholder = "...", credential = ... }` entries, so two profiles binding the same endpoint with the same credentials may carry different (or no) placeholders depending on whether the binding is ambiguous in that profile.
type CompiledRule ¶
type CompiledRule struct {
Name string
Priority int
Disabled bool
Condition string
Credential string
Matcher match.Matcher
Outcome Outcome
}
CompiledRule is one priority-sorted rule attached to an endpoint.
Condition is the original CEL source the matcher was built from, kept alongside Matcher for dashboard / diagnostic consumers that want to inspect the predicate without re-walking the rule plugin's Body. Credential, when set, is the bare-name reference the runtime checks against the dispatching credential before evaluating the matcher.
type CompiledTunnel ¶
type CompiledTunnel struct {
Name string
Plugin *Plugin
// Body is whatever the tunnel plugin's Build returned — runtime
// callers type-assert it to TunnelRuntime (the runtime contract)
// to call Open / Sharing.
Body any
// Sharing is the resolved sharing model after applying any
// `share = ...` HCL override on top of the plugin's default.
Sharing string
// Keepalive is the idle window after refcount==0 before the
// manager calls Close on the tunnel instance. Zero means tear
// down immediately; KeepaliveAlways means never tear down on
// idle (the manager pins refcount).
Keepalive time.Duration
KeepaliveAlways bool
// Via is the underlying tunnel this one chains through, or nil
// for top-level tunnels. The manager Acquires the via tunnel
// before opening this one and releases on teardown.
Via *CompiledTunnel
// Credential is the resolved credential entity this tunnel
// drives its auth from, or nil if the HCL didn't declare one.
// Plugins fetch the secret bytes via SecretStore.Get keyed on
// Credential.Symbol.Name.
Credential *Entity
// Fingerprint is a stable hash of the compiled tunnel definition,
// including its credential definition and via chain. Runtime
// managers use it to distinguish same-name tunnels across policy
// reloads without retaining or logging raw config/secret-bearing
// fields.
Fingerprint string `json:"-"`
}
CompiledTunnel is the runtime-friendly view of one tunnel block. The TunnelManager walks these to spawn / refcount / tear down runtime instances; endpoint dispatch holds a pointer into this list via CompiledEndpoint.Tunnel.
type CredentialDisambiguatorBody ¶
CredentialDisambiguatorBody is implemented by a credential plugin's decoded body when one or more of its struct fields double as dispatch discriminators (e.g. postgres_credential's `user`, clickhouse_credential's `database` / `user`). The returned map is field-name → value; empty values are dropped. Compile merges this with the framework-peeled `placeholder` attr and the profile-inline override map to produce the per- (profile, endpoint) CompiledCredential dispatch entry.
type Defaults ¶
type Defaults struct {
// UnknownHost controls traffic whose destination does not match
// any endpoint. "passthrough" relays it; "deny" closes it.
UnknownHost string `hcl:"unknown_host,optional"`
// LLMFailMode controls requests guarded by LLM approvers when
// the model call errors or times out. "closed" denies; "open"
// allows.
LLMFailMode string `hcl:"llm_fail_mode,optional"`
// LLMCacheTTL is the LLM decision cache lifetime in seconds.
LLMCacheTTL int `hcl:"llm_cache_ttl,optional"`
// HumanTimeout is the default human-approval timeout in seconds.
HumanTimeout int `hcl:"human_timeout,optional"`
// HumanOnTimeout is the default outcome when a human approver
// does not answer before timeout. "deny" or "allow".
HumanOnTimeout string `hcl:"human_on_timeout,optional"`
}
Defaults is the body of the optional top-level `defaults { ... }` block. Every field has a built-in default; the block is only needed to override one.
type Entity ¶
type Entity struct {
Symbol *Symbol
Plugin *Plugin
Body any
Refs *Refs
// Framework holds the resolved values of framework-level attrs
// (the FrameworkAttrSpec entries declared for this kind). The
// loader extracts these from the block body via
// body.PartialContent before invoking the plugin's gohcl
// decode, so plugin authors get cross-cutting features
// (`tunnel = X` on every endpoint) without per-plugin schema
// boilerplate.
Framework FrameworkAttrs
}
Entity is a successfully-loaded named entity for one of the plugin-dispatched kinds. The Body field is whatever the plugin's Build returned — the canonical record the runtime reads.
type EnvPushdownProvider ¶
type EnvPushdownProvider interface {
EnvVars() []EnvVar
}
EnvPushdownProvider is the optional interface a credential plugin implements when an agent CLI expects to read its credential out of a process environment variable. `clawpatrol env` walks every registered credential plugin's EnvVars() and prints the union as shell `export ...` lines.
Plugins that don't have a CLI integration story (mtls / generic bearer / generic header) leave this unimplemented; they show up only in the dashboard.
type EnvVar ¶
type EnvVar struct {
Name string
Value string
Description string // shown as a `# comment` line above the export
}
EnvVar is one shell variable `clawpatrol env` exports for the operator to source into their agent CLI's process environment. The Value is a placeholder that LOOKS like a real token (so the agent CLI's startup validation passes) — the gateway swaps in the real secret at MITM time via the credential plugin's InjectHTTP.
type FileIncludable ¶
type FileIncludable interface {
FileIncludeFields() []FileIncludeField
}
FileIncludable is the optional interface a plugin's body type implements to expose string fields the loader should scan for `<<file:NAME>>` markers. Only fields whose value the user might reasonably write as a file path (CA cert PEMs, certificate chains, large prompts) need it; the rest stay literal.
type FileIncludeField ¶
FileIncludeField is a getter / setter pair pointing at one string field on a plugin body. Plugins return a slice so multiple fields (e.g. kubernetes endpoint's ca_cert + client_cert) can opt in.
type FrameworkAttrSpec ¶
type FrameworkAttrSpec struct {
Name string
Kind Kind // ref kind; "" for primitive string attrs
Optional bool
List bool // when true (and Kind != ""), value is a list of bare names
}
FrameworkAttrSpec declares an HCL attribute that the loader peels off a block body BEFORE the plugin's gohcl decode runs. The plugin never sees these attrs in its struct or schema — the framework owns them. Mirrors Terraform's `lifecycle {}` shape: every resource accepts `lifecycle` without each provider having to implement it.
Three shapes:
- Kind != "" && !List : singular bare-name ref → FrameworkAttrs.Refs
- Kind != "" && List : list of bare-name refs → FrameworkAttrs.RefLists
- Kind == "" : primitive string → FrameworkAttrs.Strings
type FrameworkAttrs ¶
type FrameworkAttrs struct {
Refs map[string]string
RefLists map[string][]string
Strings map[string]string
}
FrameworkAttrs is the per-Entity bag of framework-level attr values. Keyed by FrameworkAttrSpec.Name. Refs holds singular bare-name references, RefLists holds list-of-bare-name references, Strings holds primitive (non-ref) string values.
func (FrameworkAttrs) Ref ¶
func (f FrameworkAttrs) Ref(name string) string
Ref returns the resolved reference for the named framework attr, or "" if unset.
func (FrameworkAttrs) RefList ¶
func (f FrameworkAttrs) RefList(name string) []string
RefList returns the resolved bare-name list for the named framework attr, or nil if unset.
func (FrameworkAttrs) Str ¶
func (f FrameworkAttrs) Str(name string) string
Str returns the primitive string value for the named framework attr, or "" if unset.
type Gateway ¶
type Gateway struct {
// SchemaVersion is the config grammar this file targets. The
// gateway accepts a range of versions and rejects anything newer
// than it understands with an upgrade error. Omitting it loads as
// legacy grammar (version 0) with a deprecation warning.
SchemaVersion int `hcl:"schema_version,optional"`
// Settings carries every operational scalar and the two transport
// sub-blocks. Required: configs missing the block fail to load.
Settings *GatewaySettings `hcl:"gateway,block"`
// Defaults holds the optional `defaults { ... }` block with the
// policy defaults (unknown_host, llm_*, human_*). nil when the
// block is absent — every field has a built-in default.
Defaults *Defaults `hcl:"defaults,block"`
// Plugins lists every `plugin "<name>" { source = "..." }` block
// at the top of the file. The loader spawns each subprocess
// (and registers its declared types) before running pass-1
// symbol building, so plugin-supplied (kind, type) pairs are
// available by the time policy blocks are dispatched.
Plugins []PluginSource `hcl:"plugin,block"`
// Policy holds the v14-grammar block contents. Populated after
// the operational decode by Load's pass-1 / pass-2 walk. Set to
// a non-nil empty value if the file declared no policy blocks.
Policy *Policy `hcl:"-"`
// Remain is the part of the file gohcl didn't consume — i.e.
// every policy block. Pass-2 reads from this. Not exposed in
// the public API but kept on the struct so gohcl knows to
// preserve it.
Remain hcl.Body `hcl:",remain"`
}
Gateway is the fully-loaded clawpatrol gateway config: a single `gateway { ... }` block of operational settings (with nested `wireguard { ... }` / `tailscale { ... }` transport sub-blocks), an optional top-level `defaults { ... }` block of policy defaults, and the labeled policy blocks the plugins dispatch on.
func Load ¶
func Load(path string) (*Gateway, hcl.Diagnostics)
Load parses, validates, and resolves the gateway config at path. Returns a populated *Gateway plus any diagnostics. Callers should check diagnostics first — a non-nil Gateway can still carry errors (some recovery is best-effort).
If path is a regular file, it's parsed as a single HCL document. If path is a directory, every `*.hcl` file in it (non-recursive) is parsed and merged in lexicographic filename order — the Terraform-style "module is a directory of files" model. See `doc/multi-file-config.md` for the contract.
Plugin loading goes through the package-global PluginLoader installed via SetPluginLoader. main.go installs the real *extplugin.Manager once at startup; the default is a no-op so non-plugin configs and tests need no setup.
func LoadBytes ¶
func LoadBytes(src []byte, filename string) (*Gateway, hcl.Diagnostics)
LoadBytes is Load over an in-memory buffer. Used by tests so fixtures don't need to round-trip through the filesystem.
func LoadDir ¶ added in v0.2.4
func LoadDir(dir string) (*Gateway, hcl.Diagnostics)
LoadDir parses every `*.hcl` file in dir (non-recursive), merges them in lexicographic filename order, and resolves the result as a single gateway config — the Terraform-style mental model where a "module" is a directory whose files are joined.
Discovery rules:
- Only direct children of dir are read (no recursion); subdirectories are ignored.
- Files whose name ends in `.hcl` are included. Files starting with `.` (dotfiles like `.swp`) are skipped so editor temporaries don't poison the load.
- Order is lexicographic by filename (`filepath.Base`). It must not affect semantics — reference resolution runs against the merged symbol table — but it does decide the deterministic order diagnostics, dumps, and emit traverse.
- Top-level singleton blocks (`gateway`, `defaults`) must appear in exactly one file; duplicates are rejected with the standard gohcl "Duplicate X block" diagnostic.
- Top-level repeatable blocks (`plugin "...", every policy entity) can appear in any file, but each named entity is still unique across the whole module — duplicate names across files surface as `Duplicate <kind> name` from pass-1.
- References (`endpoint = X`, `approve = [Y]`) resolve against the merged symbol table, so the order files were read in doesn't determine whether a reference is reachable.
`<<file:NAME>>` markers resolve relative to dir (the directory passed here), not relative to the individual file the marker appears in. In practice that's the same path since all merged files share a parent, but it's worth flagging if we ever extend multi-file to subdirectories.
func (*Gateway) ActionsKeep ¶ added in v0.5.5
ActionsKeep returns the raw global actions-retention string, or empty.
func (*Gateway) BodyBufferLimit ¶ added in v0.2.5
BodyBufferLimit returns the configured body buffer limit in bytes, or DefaultBodyBufferLimit when unset/unparseable. Bad input is surfaced as a load-time error by validateOperational; this runtime accessor falls back to the default so a degraded config still serves.
func (*Gateway) BodyStorageLimit ¶ added in v0.2.5
BodyStorageLimit returns the configured body storage limit in bytes, or DefaultBodyStorageLimit when unset/unparseable.
func (*Gateway) DashboardConfigWrites ¶ added in v0.2.5
DashboardConfigWrites reports whether dashboard-originated config mutations are enabled for this gateway.
func (*Gateway) DashboardListen ¶
DashboardListen returns the configured dashboard HTTP bind address, or empty string when unset.
func (*Gateway) DashboardSessionTTL ¶
DashboardSessionTTL returns the raw TTL string. Empty → default.
func (*Gateway) Dump ¶
Dump renders the loaded gateway as deterministic, indented JSON for golden-file tests. Maps marshal in sorted-key order; entity bodies are produced by plugin Build functions and are expected to be json-friendly (no cty.Value fields).
The output is NOT a stable serialization format. It changes when schema or plugins evolve and goldens regenerate via -update.
func (*Gateway) GenAITelemetryEnabled ¶ added in v0.3.0
GenAITelemetryEnabled reports whether OTel GenAI semantic-convention span export is opted in (the `genai_telemetry {}` block is present).
func (*Gateway) GenAITelemetryIncludeContent ¶ added in v0.3.0
GenAITelemetryIncludeContent reports whether message content (prompts/completions) should be captured on GenAI spans. Always false unless GenAI telemetry is enabled and the operator opted in.
func (*Gateway) IsTailscaleEnabled ¶
IsTailscaleEnabled reports whether the operator declared a `tailscale { ... }` sub-block.
func (*Gateway) IsWireGuardEnabled ¶
IsWireGuardEnabled reports whether the operator declared a `wireguard { ... }` sub-block. Block presence is the single switch — there is no `control` field. Both transports may be enabled at once.
func (*Gateway) Join ¶
func (g *Gateway) Join() JoinConfig
Join returns the join-related fields as a JoinConfig value bundle. Cheap to call — it's a small struct copy.
func (*Gateway) Operators ¶
Operators returns the tailnet-login allowlist from the `tailscale {}` block. Empty/nil disables the allowlist gate (and is the only value when no tailscale block is declared — without tsnet there's no whois identity to match against).
func (*Gateway) ResolvedStateDir ¶ added in v0.3.0
ResolvedStateDir returns the effective state directory: the configured state_dir, or ${HOME}/.clawpatrol when unset. This is the path the gateway actually uses at runtime, so it is what the plugin loader must guard read_paths against — the raw StateDir() is empty by default, which would let a read_paths grant of the default dir slip past the credential-store overlap check. Returns "" only when state_dir is unset and $HOME is unavailable.
func (*Gateway) SessionKeep ¶
SessionKeep returns the raw session-retention string, or empty.
func (*Gateway) SetPublicURL ¶
SetPublicURL overwrites the public URL. Used by the runtime when it auto-derives the URL from the tsnet cert domain after Funnel comes up.
type GatewaySettings ¶
type GatewaySettings struct {
// DashboardListen is the bind address for the dashboard + JSON
// API HTTP server. Default 127.0.0.1:8080. Set to a routable
// address to expose the dashboard off-host (the same mux is also
// served on the WG netstack / tsnet stack at this port).
DashboardListen string `hcl:"dashboard_listen,optional"`
// PublicURL is the canonical externally-reachable gateway URL.
// Used in generated control-plane links such as join targets, OAuth
// redirect URIs, and (when public_url has a host but wireguard.endpoint
// doesn't) the host clients dial for WireGuard.
PublicURL string `hcl:"public_url,optional"`
// StateDir is the directory holding clawpatrol.db (and anything
// a plugin persists to disk under it). Defaults to ${HOME}/.clawpatrol.
StateDir string `hcl:"state_dir,optional"`
// DashboardSessionTTL is how long a dashboard login session stays
// valid after the operator types the password. time.ParseDuration
// format ("24h", "30m"). Default 24h.
DashboardSessionTTL string `hcl:"dashboard_session_ttl,optional"`
// DashboardConfigWrites allows authenticated dashboard users to
// append generated config snippets to the gateway HCL. Default
// false: config remains read-only and changes happen out-of-band.
DashboardConfigWrites bool `hcl:"dashboard_config_writes,optional"`
// Resolver is the DNS resolver address the gateway uses for
// upstream lookups when the runtime needs an explicit resolver.
Resolver string `hcl:"resolver,optional"`
// LogPath is an optional file path for gateway log output.
LogPath string `hcl:"log_path,optional"`
// Telemetry opts in/out of the update-checker / anonymous usage
// ping (doc/telemetry.md). nil = default on; explicit `telemetry
// = false` silences the goroutine. Env vars CLAWPATROL_TELEMETRY=0
// and DO_NOT_TRACK=1 also work.
Telemetry *bool `hcl:"telemetry,optional"`
// SessionKeep is the hard retention floor for the sessions table.
// Sessions whose last_at is older than this get deleted by the
// background sweeper. Default 720h (30d), "0" / "off" disables.
// time.ParseDuration format.
SessionKeep string `hcl:"session_keep,optional"`
// ActionsKeep is the global default retention floor for the actions
// table (captured request/response logs — the gateway's largest
// table). Rows whose ts_ns is older than this are deleted by the
// background sweeper. Each endpoint may override this with its own
// `retention = "..."`. Default 720h (30d), "0" / "off" disables the
// default sweep (per-endpoint retention still applies).
// time.ParseDuration format.
ActionsKeep string `hcl:"actions_keep,optional"`
// Limits, if present, overrides the two gateway-wide body-size
// limits (rules-engine body buffer and persisted action body
// storage). nil uses the DefaultBody*Limit constants, which match
// today's hardcoded behavior.
Limits *LimitsBlock `hcl:"limits,block"`
// GenAITelemetry, if present, enables export of OpenTelemetry GenAI
// semantic-convention spans (gen_ai.*) for intercepted LLM
// requests. Requires the OTLP exporter to be configured
// (OTEL_EXPORTER_OTLP_ENDPOINT); without it there is nothing to
// export to. The block is opt-in: when absent, no gen_ai.* spans
// are emitted and there is no added per-request overhead.
GenAITelemetry *GenAITelemetryBlock `hcl:"genai_telemetry,block"`
// WireGuard, if present, enables the embedded userspace WireGuard
// server. Required block when running WG-mode deployments.
WireGuard *WireGuardBlock `hcl:"wireguard,block"`
// Tailscale, if present, enables the embedded tsnet node and the
// Tailscale control plane (OAuth key minting, exit-node routing).
// Both transports may be enabled simultaneously.
Tailscale *TailscaleBlock `hcl:"tailscale,block"`
}
GatewaySettings is the body of the top-level `gateway { ... }` block. Every operational scalar lives here; the two transport sub-blocks (`wireguard` / `tailscale`) are optional and select which transports the gateway exposes.
type GenAITelemetryBlock ¶ added in v0.3.0
type GenAITelemetryBlock struct {
// IncludeMessageContent additionally captures and exports the
// prompt/completion message content via the GenAI content
// convention (the gen_ai.input.messages, gen_ai.output.messages,
// and gen_ai.system_instructions span attributes), and enriches
// gen_ai.tool.definitions with each tool's description and JSON
// schema. Default false: message content can be large and
// sensitive, so it is only captured when an operator explicitly
// opts in. Independent of the base span export — the gen_ai.*
// attribute span emits regardless of this flag, including
// gen_ai.tool.definitions with each tool's name and type.
IncludeMessageContent bool `hcl:"include_message_content,optional"`
}
GenAITelemetryBlock is the body of the `genai_telemetry { ... }` sub-block inside `gateway { ... }`. Presence of the block enables emission of OpenTelemetry GenAI semantic-convention spans for intercepted LLM requests (gen_ai.* attributes — semconv v1.27.0).
type HITLAsyncGrantConfig ¶
type HITLAsyncGrantConfig struct {
// Enabled explicitly opts this approver into async retry-grant mode.
// The active profile must also set hitl_async_grants = true.
Enabled bool `hcl:"enabled,optional" json:"enabled,omitempty"`
// Post-approval retry grant lifetime for the client to retry.
ApprovedRetryTTL string `hcl:"approved_retry_ttl,optional" json:"approved_retry_ttl,omitempty"`
// Request-body fingerprinting mode. V1 supports only "raw".
FingerprintBody string `hcl:"fingerprint_body,optional" json:"fingerprint_body,omitempty"`
// Maximum request body size eligible for async raw-body fingerprinting.
MaxBodyBytes *int `hcl:"max_body_bytes,optional" json:"max_body_bytes,omitempty"`
}
HITLAsyncGrantConfig is the optional nested `async_grant { ... }` block shared by async-capable HITL approvers. It is schema-only here; runtime execution lives in the gateway and endpoint layers.
func (*HITLAsyncGrantConfig) ApprovedRetryTTLDuration ¶
func (g *HITLAsyncGrantConfig) ApprovedRetryTTLDuration() (time.Duration, error)
ApprovedRetryTTLDuration parses the approved-retry TTL, falling back to HITLAsyncDefaultApprovedRetryTTL when unset.
func (*HITLAsyncGrantConfig) FingerprintBodyValue ¶
func (g *HITLAsyncGrantConfig) FingerprintBodyValue() string
FingerprintBodyValue returns the configured fingerprint body mode, defaulting to HITLAsyncFingerprintRawBody when unset.
func (*HITLAsyncGrantConfig) MaxBodyBytesValue ¶
func (g *HITLAsyncGrantConfig) MaxBodyBytesValue() int64
MaxBodyBytesValue returns the configured max body cap (in bytes), defaulting to HITLAsyncDefaultMaxBodyBytes when unset.
type HITLAsyncGrantEnabler ¶
type HITLAsyncGrantEnabler interface {
HITLAsyncGrantEnabled() bool
}
HITLAsyncGrantEnabler is implemented by approver bodies that expose whether their config enables async retry-grant behavior. Keeping this small avoids a config -> approvers package import cycle.
type HostPattern ¶
type HostPattern struct {
Pattern string
Endpoint *CompiledEndpoint
}
HostPattern is one wildcard host binding inside a CompiledProfile. Pattern is the full lowercased `*.suffix` string; Endpoint is the CompiledEndpoint that declared it. The dispatcher walks HostPatterns when an exact HostIndex lookup misses; the slice is pre-sorted at compile time so longer (more specific) suffixes are tried first.
type JoinConfig ¶
type JoinConfig struct {
AuthKey string
ControlURL string
Hostname string
StateDir string
OAuthClientID string
OAuthClientSecret string
TailscaleTags []string
WGEnabled bool
WGInterface string
WGEndpoint string
WGListenPort int
WGServerPub string
WGSubnetCIDR string
TailscaleEnabled bool
// PublicURL is the operator-facing gateway URL. The WG onboarder
// uses its host as the default client dial target when
// WGEndpoint's host portion is wildcard / unset.
PublicURL string
}
JoinConfig is a parameter bundle of the join/transport-related settings. Not an HCL block — the fields are projected from the nested `gateway { wireguard {} tailscale {} }` shape. This struct exists purely so functions like StartWGServer / newOnboarder can take one argument instead of twelve.
type Kind ¶
type Kind string
Kind names a class of policy block. The plugin-dispatched two-label kinds — KindEndpoint, KindCredential, KindApprover, KindTunnel — read their type from the block's first label (e.g. `endpoint "https" "github-dev"` → Type="https").
KindRule and KindProfile are one-label blocks. KindRule has a single registered plugin (Type="") and infers its protocol family from the endpoints it targets at validate/build time. KindProfile has a fixed schema.
const ( KindEndpoint Kind = "endpoint" KindCredential Kind = "credential" KindRule Kind = "rule" KindApprover Kind = "approver" KindProfile Kind = "profile" KindTunnel Kind = "tunnel" )
Plugin kind constants enumerate supported config block kinds.
func (Kind) LabelCount ¶
LabelCount returns how many labels a block of this kind carries (excluding the kind keyword itself).
type LimitsBlock ¶ added in v0.2.5
type LimitsBlock struct {
BodyBuffer string `hcl:"body_buffer,optional"`
BodyStorage string `hcl:"body_storage,optional"`
}
LimitsBlock is the optional `limits { ... }` sub-block inside `gateway { ... }`. It exposes the two independent body-size limits as human-readable size strings (e.g. "256KiB", "1MiB"):
- BodyBuffer bounds the hot-path buffer the rules engine matches against. Trade-off: latency / memory vs. how much body rules see.
- BodyStorage bounds the cold-storage sample persisted per action. Trade-off: disk / db size vs. how useful the action details page is for debugging.
Both are independent: a deployment may log more (or less) than it rule-matches. Empty fields fall back to the DefaultBody*Limit constants, which equal today's hardcoded behavior.
type NonInjectingCredential ¶ added in v0.2.5
type NonInjectingCredential interface {
IsPassthrough() bool
}
NonInjectingCredential is the optional marker a credential plugin's decoded body implements to declare it injects nothing at request time — the `passthrough` type. Such a credential exists only to give the operator a handle that profiles can claim and bind to endpoints, so the existing credential→endpoint→profile claim path works for endpoints that simply don't need auth injection.
The request-time injection path already skips these for free: their body satisfies none of the runtime injection interfaces (HTTPCredentialRuntime / HTTPRequestSigner / WebSocketCredentialRuntime / PostgresCredentialRuntime / …), so the gateway forwards the request verbatim. This marker exists purely so the dashboard can surface a "no injection" indicator without importing the plugin package.
type OAuthConfig ¶
type OAuthConfig struct {
ClientID string
ClientSecret string
AuthURL string
TokenURL string
DeviceURL string // used by Flow="device"
RegisterURL string // RFC 7591 dynamic client registration (used by Flow="notion_mcp")
RedirectURI string
Scopes []string
RefreshToken string // bootstrap; per-owner tokens override
}
OAuthConfig is the per-provider OAuth client config — auth/token URLs, scopes, and the bootstrap refresh token. Per-owner access tokens are persisted by the gateway's OAuthRegistry and refresh through the usual oauth2.TokenSource path; this struct only carries the static flow definition that doesn't change per user.
type OAuthFlowProvider ¶
type OAuthFlowProvider interface {
OAuthFlow() *OAuthIntegration
}
OAuthFlowProvider is the optional interface a credential plugin's decoded body implements when it represents an OAuth-flow credential. registerOAuthCredentials walks every loaded credential, type-asserts to this, and registers the returned flow under the credential's bare name.
Drop-in for adding a new OAuth provider: ship a credential plugin type whose body implements OAuthFlow() and you're done — no host changes, no auxiliary maps to update.
type OAuthIntegration ¶
type OAuthIntegration struct {
ID string
Type string
Header string
Prefix string
Flow string // "auth_code" (default) | "device"
OAuth OAuthConfig
// OptionalScopes is the catalog of opt-in scopes the dashboard
// shows as a checklist before kicking off the OAuth flow. Plugins
// that don't surface a picker leave this empty; the connect modal
// then goes straight to the OAuth start without a picker step.
OptionalScopes []OptionalScopeGroup
}
OAuthIntegration packages an OAuthConfig with the header shape the runtime uses to inject the resulting access token. Owned by the credential plugin that ships the provider — anthropic_oauth_subscription returns the Anthropic flow, github_oauth returns the gh device flow, etc. The host's OAuthRegistry registers these by credential bare name at policy load.
type OptionalScope ¶
OptionalScope is one toggleable scope in the picker. ID is the literal scope value sent to the IdP (e.g. "admin:public_key"); Label is a short human description rendered next to the checkbox.
type OptionalScopeGroup ¶
type OptionalScopeGroup struct {
Title string `json:"title"`
Scopes []OptionalScope `json:"scopes"`
}
OptionalScopeGroup is one section of the connect-time scope picker (e.g. "ssh keys", "packages"). Each group holds zero or more OptionalScope rows that the user can individually toggle on top of OAuthConfig.Scopes.
type Outcome ¶
type Outcome struct {
Verdict string // "allow" | "deny"
Reason string
Approve []ApproveStage
}
Outcome captures a rule's verdict + (when applicable) approve chain. Exactly one of Verdict and Approve is set after Build's validation.
type Plugin ¶
type Plugin struct {
Kind Kind
Type string
// New returns a fresh pointer to the plugin's gohcl-tagged config
// struct. The loader passes the result to gohcl.DecodeBody, unless
// DecodeBody is set (see below).
New func() any
// DecodeBody, when set, replaces the loader's default
// gohcl.DecodeBody call. External plugins (config/extplugin) use
// this hook because their schema is only known at runtime — gohcl
// requires a statically-tagged Go struct, but an external plugin
// declares its attributes via a Manifest at startup. The hook
// receives the body that remains after framework-attr extraction
// and is responsible for populating target with the decoded
// attributes (typically by stashing a cty.Value). Built-in
// plugins leave this nil and rely on gohcl.
DecodeBody func(body hcl.Body, ctx *hcl.EvalContext, target any) hcl.Diagnostics
// Refs declares which fields on the decoded struct hold bare-name
// references that must be resolved against the symbol table.
Refs []RefSpec
// Family classifies an endpoint's protocol so rule plugins can
// constrain which endpoints they target. Set on KindEndpoint
// plugins ("http" | "sql" | "k8s"). KindRule, with a single
// unified plugin, leaves these empty — family is inferred from
// the rule's resolved endpoints at validate/build time.
Family string
Families []string
// Validate runs after gohcl decode and after Refs are resolved.
// It catches plugin-local invariants gohcl can't express
// (e.g. exactly-one-of credential / credentials) and may use the
// symbol table to resolve refs that the standard RefSpec path
// syntax can't reach (e.g. fields inside a cty.Value attribute).
Validate func(decoded any, name string, ctx *BuildCtx) hcl.Diagnostics
// Build returns the canonical in-memory record stored in
// Policy.Endpoints / .Credentials / etc. The runtime reads from
// these, never from the raw decoded struct.
Build func(decoded any, name string, ctx *BuildCtx) (any, hcl.Diagnostics)
// CompileRule lowers a rule plugin's Build output into a
// *CompiledRule + the list of endpoint names it attaches to.
// Only set on rule plugins; nil for other kinds. Defined as a
// callback so the lowering logic lives next to the rule plugin's
// schema (rather than in a generic compile pass that needs an
// interface escape hatch).
CompileRule func(body any, name string) (*CompiledRule, []string, error)
// Runtime is type-asserted by callers based on Kind:
// KindEndpoint → runtime.EndpointRuntime
// KindCredential → runtime.CredentialRuntime
// KindApprover → runtime.ApproverRuntime
// KindRule → runtime.RuleMatcherFactory
// KindTunnel → runtime.TunnelRuntime
// nil means "schema-only; runtime not implemented" — request-time
// dispatch reports a clear diagnostic when it tries to use one.
Runtime any
// Emit serializes a built entity back to HCL by populating an
// hclwrite block body. Required for every plugin — the framework
// has no generic reverse path that handles bare-name refs,
// heterogeneous list shapes (credentials with optional
// placeholders), or per-family match maps. Plugins that decode
// nothing (zero-attribute credentials) provide a no-op Emit.
Emit func(body any, name string, hb *hclwrite.Body)
// Disambiguators names the HCL attrs this credential plugin
// recognizes as dispatch discriminators when two credentials of
// the same type bind the same endpoint within a profile. Only
// meaningful for KindCredential plugins; left empty elsewhere.
//
// Each name corresponds to a string-valued attr the operator may
// set on either the credential block itself or inline in a
// profile's credentials list (`{ credential = X, <name> = "..." }`);
// profile-inline values override block-side values for the same
// `(credential, endpoint)` tuple.
//
// Conventional names per protocol family:
// - HTTP-auth (bearer_token, header_token, anthropic_*, …): "placeholder"
// - postgres_credential: "user" (postgres routes by user)
// - clickhouse_credential: "database", "user" (either or both)
// - ssh_credential: "user"
//
// The loader rejects any disambiguator attr (block-side or
// profile-side) whose name is not in this list — that catches
// e.g. `placeholder = "..."` set on a postgres credential.
Disambiguators []string
}
Plugin describes one (Kind, Type) pair — e.g. (endpoint, https) or (credential, bearer_token). Built-in plugins call Register at init time; the loader looks them up by (Kind, Type) when decoding blocks.
func AllPlugins ¶
AllPlugins returns every registered plugin of the given kind, sorted by Type. Used by `clawpatrol env` to walk credential plugins and ask each for its env-pushdown lines without having loaded a specific gateway.hcl.
type PluginLoader ¶
type PluginLoader interface {
// stateDir is the gateway's resolved state directory (where the
// secret store lives); the loader refuses a plugin read_paths
// grant that overlaps it.
LoadPlugins(specs []PluginSource, stateDir string) hcl.Diagnostics
}
PluginLoader is the gateway-side hook the loader calls before policy decoding. It spawns each declared plugin subprocess and registers the manifest types in the global plugin registry. Returning hcl.Diagnostics with errors aborts the load.
Implemented by *extplugin.Manager — the package-cycle-safe seam (config can't import extplugin since extplugin imports config).
type PluginSource ¶
type PluginSource struct {
Name string `hcl:"name,label"`
Source string `hcl:"source"`
// Version is a semver constraint (hashicorp/go-version syntax,
// e.g. "~> 1.2", ">= 1.0, < 2.0") applied to a GitHub source's
// release tags; the newest satisfying tag is selected. Empty
// selects the newest non-prerelease. It is an error to set Version
// on a local-path source.
Version string `hcl:"version,optional"`
// Provenance controls how a GitHub release's build-provenance
// attestation is treated. "warn" (the default) verifies an
// attestation when present and falls back to checksum-only with a
// warning when absent; "require" refuses a release that carries no
// (or an invalid) attestation; "off" skips the attestation check
// (checksum + lockfile pinning still apply). Only valid on a GitHub
// source. A present-but-invalid attestation always fails closed.
Provenance string `hcl:"provenance,optional"`
// Network grants the plugin direct network access. "none" (the
// default) confines the plugin to its gateway socket — endpoint,
// credential, and tunnel plugins all reach the network through the
// gateway's brokered dial (endpoints via the upstream dial, tunnels
// via the transport dial), so they need no more. "outbound" lets the
// plugin dial out itself; it's only for the few plugins that can't use
// the broker — e.g. one that execs helper tools (ssh, kubectl) which
// open their own connections, or a credential plugin doing its own
// token exchange.
Network string `hcl:"network,optional"`
// Sandbox controls subprocess isolation. "enforce" (the default)
// runs the plugin inside an OS sandbox — Linux namespaces (or
// Landlock where user namespaces are unavailable), macOS
// seatbelt — and fails config load when no sandbox can be
// established on this host. "off" runs the plugin with the
// gateway user's full privileges; the gateway's environment is
// scrubbed either way.
Sandbox string `hcl:"sandbox,optional"`
// ReadPaths grants the plugin recursive read-only access to
// extra host paths (e.g. "~/.ssh" for an SSH tunnel plugin).
// Paths must be absolute; a leading "~/" expands to the gateway
// user's home directory. The gateway refuses a path that overlaps
// the state dir (the secret store). There is deliberately no
// host-write grant: writing an active location (~/.bashrc, cron,
// a $PATH dir, ...) is a code-execution-as-the-gateway-user
// primitive, and no denylist of such locations can be complete.
// A plugin that genuinely needs host writes runs with
// sandbox = "off" (the single, explicit full-trust knob);
// durable plugin storage goes through the gateway's blob store.
ReadPaths []string `hcl:"read_paths,optional"`
}
PluginSource is a top-level `plugin "<name>" { source = "..." }` declaration. Name is informational (the manifest name from the subprocess wins for type namespacing).
Source is either a local path to the plugin binary or a GitHub repository to fetch it from:
- A local path ("/abs/path", "./rel", "~/p") runs that binary directly.
- "github.com/<owner>/<repo>" downloads the binary from the repo's GitHub releases, selecting the newest release tag that satisfies Version, then caches it under the state dir and pins the resolved version + per-platform hashes in clawpatrol.lock.hcl. The running gateway only ever loads the locked version; `clawpatrol plugins update` is the explicit upgrade.
type Policy ¶
type Policy struct {
Approvers map[string]*Entity
Credentials map[string]*Entity
Endpoints map[string]*Entity
Rules map[string]*Entity
Tunnels map[string]*Entity
Profiles map[string]*Profile
// Order preserves declaration order across all kinds combined.
// Useful for dashboard rendering and emit round-tripping.
Order []string
}
Policy is the resolved set of named policy entities. Maps are keyed by entity name (the second label of two-label kinds, the only label of one-label kinds). Insertion order is preserved in the parallel slices for deterministic emit / dump output.
type Profile ¶
type Profile struct {
Name string `json:"name"`
Credentials []string `json:"credentials"`
Disambiguators map[string]map[string]string `json:"disambiguators,omitempty"`
HITLAsyncGrants bool `json:"hitl_async_grants,omitempty"`
}
Profile is the lowered shape of a profile "<name>" {} block. Name is the block's single label (set by the loader). Credentials is the membership list; endpoint membership rides along as the transitive closure profile → credential → endpoint, and rules attach to endpoints (so they ride along too).
Disambiguators is the per-credential profile-side dispatch discriminator. Outer key is credential name; inner map is disambiguator-field → value (e.g. {"placeholder": "PH_x"}, {"user": "ro"}, {"database": "prod"}). Set only for credentials listed with inline object syntax in the profile's credentials list (`{ credential = name, <field> = "value", ... }`); credentials listed as bare names have no entry. The valid set of <field> names is per-credential-type — declared by the plugin's Plugin.Disambiguators slice — and the loader rejects unsupported fields here.
Compile merges these onto each CompiledCredential alongside any block-side values; on conflict, profile-inline wins (the operator's most-specific declaration).
type RefIndex ¶
type RefIndex struct {
// contains filtered or unexported fields
}
RefIndex resolves a (kind, name) pair to the typed traversal string the emitter should write. Two-label kinds become `type.name`; one- label kinds become `kind.name` (e.g. `rule.foo`). Built into Emit from the loaded *Policy so plugin Emit hooks don't each re-derive the type lookup.
func EmitRefIndex ¶
func EmitRefIndex() *RefIndex
EmitRefIndex returns the current emit's RefIndex. Plugin Emit hooks call this to resolve a bare entity name to its typed-traversal form (e.g. "anthropic" → "https.anthropic") before calling SetIdent / SetIdentList. Returns a non-nil pointer even when no Emit is in progress so callers don't have to nil-check before Ref / Refs.
type RefSpec ¶
type RefSpec struct {
// Path traverses the decoded struct. Slice elements use [*];
// nested struct fields use dot. Examples:
// "Endpoint"
// "Endpoints[*]"
// "Credentials[*].Credential"
Path string
// Kind the resolved name must belong to.
Kind Kind
// FamilyConstraint, when non-empty, requires the resolved entity's
// Family to be in this set. Used by rule plugins to require
// endpoints of a matching protocol family. Empty = any family.
FamilyConstraint []string
// Optional means an empty/zero value at Path is fine. Required
// references that resolve to "" emit a diagnostic.
Optional bool
}
RefSpec declares a field on a decoded plugin struct that holds a bare-name reference (or a list of them) into the symbol table.
type Refs ¶
type Refs struct {
// contains filtered or unexported fields
}
Refs is the result of resolving one block's RefSpec entries. Each entry is keyed by the plugin's RefSpec.Path and points back to the resolved Symbol (so plugins can read its Family or Kind without re-querying the table).
The loader populates this and passes it to plugin.Validate and plugin.Build. Plugins read Refs to wire pointers in the canonical record they produce.
type SecretSlot ¶
type SecretSlot struct {
Name string `json:"name"` // "" for single-slot; field name for multi
Label string `json:"label"` // human label rendered in the modal
Multiline bool `json:"multiline"` // true for PEM blobs (textarea, not password input)
Description string `json:"description"` // optional one-liner under the input
}
SecretSlot describes one input the operator must fill in the dashboard's connect-credential modal. Single-slot credentials (bearer / header / cookie / api-key) declare one slot with empty Name; multi-slot credentials (mtls cert+key+ca, slack bot+app) declare one entry per named field.
At runtime the secret store packs slot values into runtime.Secret: the unnamed slot fills Bytes; named slots fill Extras[Name].
type SecretSlotsProvider ¶
type SecretSlotsProvider interface {
SecretSlots() []SecretSlot
}
SecretSlotsProvider is the optional interface a credential plugin's decoded body implements when the operator can connect it via the dashboard. OAuth-flow credentials (which use OAuthFlowProvider instead) leave this unimplemented; the dashboard then renders the OAuth connect button rather than a paste-secret modal.
Plugin authors return a constant slice — slot definitions don't vary per credential instance.
type Symbol ¶
type Symbol struct {
Name string
Kind Kind
Type string // "" for one-label kinds
Family string // for endpoints: "http"|"sql"|"k8s"; "" otherwise
Block *hcl.Block
}
Symbol is one entry in the per-kind namespace.
type SymbolTable ¶
type SymbolTable struct {
// contains filtered or unexported fields
}
SymbolTable indexes every named block in the file, populated in pass 1. Names are unique within a kind; cross-kind collisions are allowed because typed refs carry the kind (one-label kinds) or type (two-label kinds) in the syntax.
func (*SymbolTable) All ¶
func (t *SymbolTable) All(kind Kind) []*Symbol
All returns every symbol of the given kind, in deterministic (declaration) order. Used by the loader's pass-2 walk.
type TailscaleBlock ¶
type TailscaleBlock struct {
// AuthKey is the Tailscale auth key for the embedded tsnet node.
// Required when the `tailscale` block is present. Falls back to
// $TS_AUTHKEY if empty.
AuthKey string `hcl:"authkey,optional"`
// Hostname is the device name requested for the tsnet node.
// Default "clawpatrol-gateway".
Hostname string `hcl:"hostname,optional"`
// ControlURL is the Tailscale control-plane URL. Empty →
// Tailscale's hosted control plane.
ControlURL string `hcl:"control_url,optional"`
// Tags is the Tailscale device-tag list applied to keys the
// gateway mints for onboarded clients (`tag:client` etc.). The
// autoApprovers exit-node ACL must reference these tags.
Tags []string `hcl:"tags,optional"`
// Operators allowlists tailnet logins permitted to use the
// dashboard without typing the root password. Each entry is
// either an exact login ("alice@example.com") or a domain
// wildcard ("*@example.com"). Tagged devices (whose whois login
// is the tag name, not a user email) never match a wildcard
// entry — agents on the tailnet can never bypass the gate
// through this path.
//
// Empty / unset → tailnet-allowlist auth is disabled. The stored
// root password is then the only way in. Lives under `tailscale
// {}` because matching requires the tsnet whois identity; there
// is no whois without an active tsnet node.
Operators []string `hcl:"operators,optional"`
// Funnel enables Tailscale Funnel on :443 so the join bootstrap
// and credential webhook paths are internet-reachable via the
// tsnet cert domain. Requires HTTPS enabled for the tailnet; if
// public_url is unset the gateway derives it from the tsnet
// cert domain at startup.
Funnel bool `hcl:"funnel,optional"`
// OAuthClientID is the OAuth client id used to mint per-device
// tailnet auth keys at approval time.
OAuthClientID string `hcl:"oauth_client_id,optional"`
// OAuthClientSecret is the OAuth client secret paired with
// OAuthClientID.
OAuthClientSecret string `hcl:"oauth_client_secret,optional"`
}
TailscaleBlock is the body of the `tailscale { ... }` sub-block inside `gateway { ... }`. Presence of the block enables tsnet.
type TunnelCommon ¶
TunnelCommon is the framework-level slice every tunnel plugin's HCL body restates. The compile pass reads them via TunnelCommonRead instead of reflecting into the plugin struct, so plugins keep full control of their schema while sharing the manager-visible knobs.
Share / Keepalive accept "" (use the plugin default) or one of the recognised values. Via / Credential are bare-name refs validated against the symbol table at load time — empty when the HCL omitted them.
type TunnelCommonRead ¶
type TunnelCommonRead interface {
TunnelCommon() TunnelCommon
}
TunnelCommonRead is the cross-cut interface tunnel plugin bodies implement so the compile pass can pick up the framework-level attrs (share / keepalive / via / credential) without depending on the concrete plugin type.
type WireGuardBlock ¶
type WireGuardBlock struct {
// SubnetCIDR is the private subnet assigned to onboarded clients.
// Required (e.g. "10.55.0.0/24").
SubnetCIDR string `hcl:"subnet_cidr,optional"`
// ListenPort is the UDP port the gateway binds for WG peers.
// Default 51820.
ListenPort int `hcl:"listen_port,optional"`
// HostLoopbackPort is the TCP port the gateway binds on
// 127.0.0.1 for host-local clients (single-host deployments where
// clawpatrol-run loops back to the gateway on the same box).
// Default 8443. Make it operator-controlled so two gateways can
// coexist on one host (dev/test, blue-green, multi-tenant)
// without colliding on the loopback landing pad.
HostLoopbackPort int `hcl:"host_loopback_port,optional"`
// Endpoint is the host:port advertised in client wg.conf as
// `Endpoint = ...`. Host defaults to public_url's host; port
// defaults to listen_port. Set only for split-host deployments
// (gateway sits behind a different hostname/IP for WG than for
// the dashboard).
Endpoint string `hcl:"endpoint,optional"`
// Interface is the WireGuard interface name the gateway manages.
// Mostly irrelevant in userspace mode; leave unset.
Interface string `hcl:"interface,optional"`
// ServerPub is the WireGuard server public key advertised to
// clients. Normally derived from gateway state; only set when
// bootstrapping from an external key.
ServerPub string `hcl:"server_pub,optional"`
}
WireGuardBlock is the body of the `wireguard { ... }` sub-block inside `gateway { ... }`. Presence of the block enables the WG transport.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package extplugin spawns and proxies clawpatrol's Terraform-style external plugins.
|
Package extplugin spawns and proxies clawpatrol's Terraform-style external plugins. |
|
wire
Package wire holds the small set of protocol constants shared between the gateway-side plugin manager (extplugin) and the plugin SDK (pluginsdk): the go-plugin handshake, the dispensed plugin name, the host-services broker stream id, and the per-call session metadata key.
|
Package wire holds the small set of protocol constants shared between the gateway-side plugin manager (extplugin) and the plugin SDK (pluginsdk): the go-plugin handshake, the dispensed plugin name, the host-services broker stream id, and the per-call session metadata key. |
|
Package facet is the registry for protocol-family plugins.
|
Package facet is the registry for protocol-family plugins. |
|
Package hostmatch parses and matches the host entries that appear in an endpoint's `hosts = [...]` list.
|
Package hostmatch parses and matches the host entries that appear in an endpoint's `hosts = [...]` list. |
|
Package match holds the runtime types the request handler walks when dispatching against the compiled policy: the Matcher interface every rule's compiled predicate satisfies and the family-tagged Request snapshot the matcher reads.
|
Package match holds the runtime types the request handler walks when dispatching against the compiled policy: the Matcher interface every rule's compiled predicate satisfies and the family-tagged Request snapshot the matcher reads. |
|
plugins
|
|
|
all
Package all blank-imports every built-in plugin so a single import from main / tests pulls the entire registry into the binary.
|
Package all blank-imports every built-in plugin so a single import from main / tests pulls the entire registry into the binary. |
|
approvers
Package approvers registers every built-in approver kind.
|
Package approvers registers every built-in approver kind. |
|
credentials
Package credentials implements clawpatrol credentials support.
|
Package credentials implements clawpatrol credentials support. |
|
endpoints
Package endpoints registers every built-in endpoint plugin.
|
Package endpoints registers every built-in endpoint plugin. |
|
facets/https
Package https is the HTTPS protocol-family facet.
|
Package https is the HTTPS protocol-family facet. |
|
facets/k8s
Package k8s is the Kubernetes protocol-family facet.
|
Package k8s is the Kubernetes protocol-family facet. |
|
facets/sql
Package sql is the SQL protocol-family facet.
|
Package sql is the SQL protocol-family facet. |
|
facets/ssh
Package ssh is the SSH protocol-family facet.
|
Package ssh is the SSH protocol-family facet. |
|
rules
Package rules registers the single `rule` block kind.
|
Package rules registers the single `rule` block kind. |
|
sshproto
Package sshproto holds the protocol-specific contract that bridges the SSH endpoint plugin and the SSH credential plugin.
|
Package sshproto holds the protocol-specific contract that bridges the SSH endpoint plugin and the SSH credential plugin. |
|
tailscaleproto
Package tailscaleproto holds the protocol-specific contract that bridges the tailscale credential and the tailscale tunnel (plus the dashboard Connect modal).
|
Package tailscaleproto holds the protocol-specific contract that bridges the tailscale credential and the tailscale tunnel (plus the dashboard Connect modal). |
|
tunnels
Package tunnels hosts the built-in tunnel plugins.
|
Package tunnels hosts the built-in tunnel plugins. |
|
Package runtime hosts the request-time dispatcher and the plugin runtime interfaces.
|
Package runtime hosts the request-time dispatcher and the plugin runtime interfaces. |