preflight

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 11 Imported by: 0

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

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 DetectUnsupportedEnv(env map[string]string) []string

func EffectiveEgressEnv

func EffectiveEgressEnv(parentEnv []string, cwd string, childArgs []string) (map[string]string, []string, bool, error)

func EffectiveEgressEnvFromInspection

func EffectiveEgressEnvFromInspection(parentEnv []string, inspect *settings.InspectionResult) (map[string]string, []string, bool, error)

func InjectedEnv

func InjectedEnv(proxyURL, caCertPath, caBundlePath string) map[string]string

func ParentEnvMap

func ParentEnvMap(env []string) map[string]string

func ResolveAPIBase

func ResolveAPIBase(explicit, inherited string) (*url.URL, model.RouteSource, error)

func ResolveAPIBaseFromInspection

func ResolveAPIBaseFromInspection(explicit string, parentEnv []string, inspect *settings.InspectionResult) (*url.URL, model.RouteSource, string, error)

func ResolveAuthFromInspection

func ResolveAuthFromInspection(parentEnv []string, inspect *settings.InspectionResult) (model.AuthMode, model.AuthSource, string, *model.AuthOverride, error)

func ResolveEgress

func ResolveEgress(flagValue string, parentEnv []string, cwd string, childArgs []string) (model.EgressConfig, []string, error)

func ResolveEgressFromInspection

func ResolveEgressFromInspection(flagValue string, parentEnv []string, inspect *settings.InspectionResult) (model.EgressConfig, []string, error)

func ScrubKeys

func ScrubKeys() []string

Types

type AuthSpec

type AuthSpec struct {
	Mode   string
	Key    string
	KeyEnv string
}

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

type ChildAuthBootstrap struct {
	EnvKey string
	Value  string
}

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 Run

func Run(opts Options) (*Result, error)

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.

Jump to

Keyboard shortcuts

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