Documentation
¶
Overview ¶
Package preflight runs the launch resolution: it reads on-disk settings via settings.InspectLaunch, merges with Options-supplied launch inputs, and produces a *Result describing the resolved routing/auth/aliases/headers/egress.
Live hot-swap extends Options with file-content snapshot fields for byte-faithful switch resolution (see Options doc-comments). The supervisor retains a *LaunchContext{Options, Inspection, *Result} at launch and reuses it on every SwitchProfile, with only Options.Profile swapped. The resolver runs the SAME code path on switch as at launch; only disk I/O for file-backed inputs is bypassed via the content-snapshot fields.
ParentEnv immutability invariant ¶
Options.ParentEnv []string is captured fresh at cmd/ccwrap/main.go (via os.Environ() which always returns a new slice) and is treated as read-only thereafter. No code in this package, in internal/modelalias, in internal/upstreamheaders, in internal/supervisor, or in cmd/ccwrap mutates the slice. applyProfileOverlay builds a defensive copy when it needs to append env entries:
merged := append([]string(nil), opts.ParentEnv...) merged = append(merged, "KEY=VALUE") // opts.ParentEnv unaffected
Any future code that needs to mutate the env tier MUST take a defensive copy first. Appending directly to Options.ParentEnv is forbidden.
Index ¶
- func BuildChildEnv(parentEnv []string, proxyURL, caCertPath, caBundlePath string, ...) []string
- func ClassifyRoute(apiBaseURL *url.URL) model.RouteClass
- func ClassifyTransition(current, candidate *Result) model.RelaunchClass
- func DetectUnsupportedEnv(env map[string]string) []string
- func EffectiveEgressEnv(parentEnv []string, cwd string, childArgs []string) (map[string]string, []string, bool, error)
- func EffectiveEgressEnvFromInspection(parentEnv []string, inspect *settings.InspectionResult) (map[string]string, []string, bool, error)
- func InjectedEnv(proxyURL, caCertPath, caBundlePath string) map[string]string
- func ParentEnvMap(env []string) map[string]string
- func ResolveAPIBase(explicit, inherited string) (*url.URL, model.RouteSource, error)
- func ResolveAPIBaseFromInspection(explicit string, parentEnv []string, inspect *settings.InspectionResult) (*url.URL, model.RouteSource, string, error)
- func ResolveAuth(env map[string]string) (model.AuthMode, model.AuthSource, *model.AuthOverride, error)
- func ResolveAuthFromInspection(parentEnv []string, inspect *settings.InspectionResult) (model.AuthMode, model.AuthSource, string, *model.AuthOverride, error)
- func ResolveEgress(flagValue string, parentEnv []string, cwd string, childArgs []string) (model.EgressConfig, []string, error)
- func ResolveEgressFromInspection(flagValue string, parentEnv []string, inspect *settings.InspectionResult) (model.EgressConfig, []string, error)
- func ScrubKeys() []string
- type AuthSpec
- type ChildAuthBootstrap
- type Options
- type ProfileInput
- type ProfileView
- type Result
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildChildEnv ¶
func BuildChildEnv(parentEnv []string, proxyURL, caCertPath, caBundlePath string, modelEnv map[string]string, authBootstrap ChildAuthBootstrap, tz string) []string
BuildChildEnv composes the Claude Code child environment. The trailing tz parameter, when non-empty, overrides any inherited TZ so the child stamps a chosen timezone into its request prompt (Claude Code computes "Today's date" from the LOCAL zone). An empty tz leaves the parent's TZ (if any) untouched — byte-identical to the pre-feature environment. Callers are expected to have already validated tz via time.LoadLocation; BuildChildEnv trusts it.
func ClassifyRoute ¶
func ClassifyRoute(apiBaseURL *url.URL) model.RouteClass
ClassifyRoute reports whether a parsed upstream URL belongs to a first-party Anthropic host or a third-party gateway. Exported so the doctor command (cmd/ccwrap) can share the same predicate.
func ClassifyTransition ¶
func ClassifyTransition(current, candidate *Result) model.RelaunchClass
ClassifyTransition classifies a posture transition (current → candidate). needs_relaunch IFF Claude was started with only a placeholder (hidden/CCWRAP-owned bootstrap) and the candidate needs Claude's own first-party credentials forwarded (first-party passthrough). Every other transition — gateway↔gateway, key swap, model swap, egress swap, first-party→gateway — is a pure ccwrap-side live hot-rebind. nil current (first launch) is always live.
func DetectUnsupportedEnv ¶
func EffectiveEgressEnv ¶
func InjectedEnv ¶
func ParentEnvMap ¶
func ResolveAPIBase ¶
func ResolveAPIBaseFromInspection ¶
func ResolveAPIBaseFromInspection(explicit string, parentEnv []string, inspect *settings.InspectionResult) (*url.URL, model.RouteSource, string, error)
func ResolveAuth ¶
func ResolveAuth(env map[string]string) (model.AuthMode, model.AuthSource, *model.AuthOverride, error)
func ResolveAuthFromInspection ¶
func ResolveAuthFromInspection(parentEnv []string, inspect *settings.InspectionResult) (model.AuthMode, model.AuthSource, string, *model.AuthOverride, error)
func ResolveEgress ¶
func ResolveEgressFromInspection ¶
func ResolveEgressFromInspection(flagValue string, parentEnv []string, inspect *settings.InspectionResult) (model.EgressConfig, []string, error)
Types ¶
type AuthSpec ¶
AuthSpec is the preflight-side mirror of profiles.AuthSpec. Kept as a separate type so the runtime layer can evolve independently of the on-disk schema (e.g. future fields exposed only to resolution). Mode is one of "" | "ccwrap_bearer" | "ccwrap_x_api_key" — "passthrough" is rejected at schema-validate time.
type ChildAuthBootstrap ¶
ChildAuthBootstrap describes the non-secret auth value injected into the Claude child process so Claude can reach CCWRAP while CCWRAP owns the real upstream secret. The value must never be forwarded upstream.
func (ChildAuthBootstrap) Enabled ¶
func (b ChildAuthBootstrap) Enabled() bool
type Options ¶
type Options struct {
Upstream string
EgressProxy string
ParentEnv []string
WorkingDirectory string
ChildArgs []string
ModelAliasFile string
ModelAliasPairs []string
AllowProviderModelPassthrough bool
AllowAuthPassthroughToThirdParty bool
UpstreamHeaderFile string
UpstreamHeaderPairs []string
// Profile is the per-field overlay contributed by a selected
// provider profile. nil => inherit-env: behavior is exactly
// the env-only path (byte-identical Result). The launch path
// (cmd/ccwrap) sets this; the live-swap path reuses ResolveProfile.
Profile *ProfileInput
// File-content snapshots for byte-faithful switch resolution. Populated by
// the launcher at launch (cmd/ccwrap/main.go composeLaunch); the doctor
// command leaves them nil ⇒ disk-read fallback. When non-empty, the
// resolver MUST use the content and MUST NOT read disk for that tier input.
ModelAliasExplicitFileContent []byte // overrides ModelAliasFile reads
ModelAliasEnvFileContent []byte // overrides CCWRAP_MODEL_ALIASES_FILE-path reads
UpstreamHeaderExplicitFileContent []byte // overrides UpstreamHeaderFile reads
UpstreamHeaderEnvFileContent []byte // overrides CCWRAP_UPSTREAM_HEADERS_FILE-path reads
}
type ProfileInput ¶
type ProfileInput struct {
Name string
Provider string
BaseURL string
Auth *AuthSpec
ModelAliases map[string]string
UpstreamHeaders map[string]string
EgressMode string // "" | "inherit" | "direct" | "http"
EgressURL string
}
ProfileInput is the per-field overlay a selected provider profile contributes to resolution. A nil *ProfileInput means inherit-env: resolution is byte-identical to ccwrap's pre-feature env-only path.
Per-field precedence (highest→lowest): an explicit individual flag (already populated into the matching Options field by cmd/ccwrap) wins; then this overlay; then env; then the fallback default. ResolveProfile therefore consults the overlay for a field ONLY when the corresponding explicit Options field is empty.
Auth mirrors the schema's pointer shape (profiles.Profile.Auth): nil = "ccwrap does not own auth for this profile" (no header injected); non-nil = ccwrap injects an auth header per Mode/Key/KeyEnv.
func FromProfile ¶
func FromProfile(p *profiles.Profile) *ProfileInput
FromProfile builds a ProfileInput from a selected profile. nil-safe: a nil profile (inherit-env) returns nil. When p.Auth is nil — the profile expresses "ccwrap does not own auth" — in.Auth is also nil. profileAuthEnvKey already maps an empty mode to "" so applyProfileOverlay skips the auth-injection branch cleanly when the pointer is nil.
type ProfileView ¶
type ProfileView struct {
Name string `json:"name,omitempty"`
ProviderLabel string `json:"provider_label,omitempty"`
BaseURLHost string `json:"base_url_host,omitempty"`
ModelAliasCount int `json:"model_alias_count,omitempty"`
EgressSummary string `json:"egress_summary,omitempty"`
RelaunchClass model.RelaunchClass `json:"relaunch_class,omitempty"`
AuthPolicy model.AuthPolicy `json:"auth_policy,omitempty"`
}
ProfileView is the non-secret snapshot of a resolved posture. It carries everything the inspect switcher / posture timeline render (provider/group label, base_url host, model-alias count, egress summary, active-profile identity, relaunch-required classification, auth policy) and NEVER a credential value.
JSON tags use snake_case for stable wire format across the supervisor's SwitchOutcome serialization and the control package's SwitchOutcomeView mirror. The control client decodes View as json.RawMessage; CLI/UI re-decode into a typed shape when needed.
func SafeProfileView ¶
func SafeProfileView(r *Result) ProfileView
SafeProfileView returns the result's ProfileView with EgressSummary userinfo stripped. It wraps raw ProfileView to prevent caller-forgetting-strip.
Today's (*Result).ProfileView().EgressSummary flows through egress.redact which preserves the username (see egress.go). This wrapper post-processes the view through stripUserinfo so no surface (ccwrap profile {ls,status,switch} CLI, SwitchOutcome.View, the inspect UI) can accidentally serialize an unstripped EgressSummary.
type Result ¶
type Result struct {
APIBaseURL *url.URL
RouteSource model.RouteSource
RouteConfigSource string
RouteClass model.RouteClass
AuthMode model.AuthMode
AuthSource model.AuthSource
AuthConfigSource string
AuthPolicy model.AuthPolicy
AuthBootstrap model.AuthBootstrap
AuthBootstrapKind model.AuthBootstrapKind
AuthBootstrapEnvKey string
OverrideAuth *model.AuthOverride
ActiveSources []string
APIKeyHelperHits []string
DangerousShellSettings []settings.EnvConflict
UnsupportedEnv []string
UnsupportedSettings []settings.EnvConflict
MalformedSettingsEnv []settings.MalformedEnvIssue
OverriddenNetworkEnv []settings.EnvConflict
PolicyNetworkEnv []settings.EnvConflict
CustomAuthHeaderEnv []settings.EnvConflict
ModelOverrideHits []settings.EnvConflict
ParsedFlagSettings *settings.ParsedFlagSettings
RewrittenChildArgs []string
Egress model.EgressConfig
ModelEnv map[string]string
ModelConfigSources []string
ModelAlias modelalias.Config
UpstreamHeaders upstreamheaders.Config
Notes []string
// Active provider-profile identity (the switcher and posture
// timeline read these; empty => inherit-env).
// Non-secret: name + group label only, never a credential.
ActiveProfileName string
ActiveProfileProvider string
// MissingAuthEnv names the env var that an active profile's `auth.key_env`
// pointed at when no value was found in the process env (the profile-overlay
// path's "key_env named but env missing" branch). It is empty in the
// broader "no auth source at all" case (third-party-hidden + resolved
// mode==passthrough), because there is no specific env to name. UI/error
// surfaces branch on emptiness:
// non-empty → "profile X needs $Y" (concrete recovery target)
// empty → "profile X has no auth source configured"
// Always paired with AuthBootstrap == AuthBootstrapMissing — the boolean
// signal; MissingAuthEnv is the detail.
//
// Fail-closed is enforced on the forward path (request-time) rather than in
// preflight (launch-time). When AuthBootstrap==Missing the launch SUCCEEDS
// so inspect/popover/switch tools are reachable; only an actual request to a
// host requiring ccwrap-owned auth gets refused.
MissingAuthEnv string
}
func ResolveProfile ¶
func ResolveProfile(opts Options, inspect *settings.InspectionResult) (*Result, error)
ResolveProfile is the single resolver shared by the launch path and the live-switch path. opts.Profile is the selected-profile overlay (nil => inherit-env, byte-identical to ccwrap's pre-feature env-only resolution). This is the verbatim pre-feature resolution + hidden-auth/fail-closed validation body, with the profile overlay applied to its inputs first. A rejected profile returns an error and NO Result — nothing partially applies.
func RunWithInspection ¶
func RunWithInspection(opts Options, inspect *settings.InspectionResult) (*Result, error)
func (*Result) ProfileView ¶
func (r *Result) ProfileView() ProfileView
ProfileView builds the non-secret snapshot from a resolved Result. RelaunchClass is computed vs a nil "current" (first launch / no prior posture); callers recompute it against the live posture via ClassifyTransition. profileName is taken from the active profile overlay when present (carried on the Result is not needed — the caller passes the active *ProfileInput's identity through the Result fields it already has). Here Name/ProviderLabel derive from the resolved upstream when no profile drove it.