package agentconfig import ( "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" "io" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) const RuntimeConfigSchemaVersion = "1" // RepoGlobalRuntimeConfig is the version-controlled, secret-free input owned // by a project repository. The runtime only reads this document. type RepoGlobalRuntimeConfig struct { Version string `yaml:"version"` Catalog Catalog `yaml:"catalog,omitempty"` Defaults RuntimeDefaults `yaml:"defaults,omitempty"` Selection SelectionPolicy `yaml:"selection,omitempty"` Isolation IsolationPolicy `yaml:"isolation,omitempty"` Retention RetentionPolicy `yaml:"retention,omitempty"` } // UserLocalRuntimeConfig is the device-owned input applied after the // repository input. It deliberately has no credential or raw environment // value fields. type UserLocalRuntimeConfig struct { Version string `yaml:"version"` Device DeviceRuntimeConfig `yaml:"device"` Clients map[string]ClientProcessSpec `yaml:"clients,omitempty"` Override RuntimeConfigOverride `yaml:"override,omitempty"` Projects map[string]ProjectRegistrationOverlay `yaml:"projects,omitempty"` } // RuntimeConfig is the fully merged configuration captured by a // RuntimeSnapshot. type RuntimeConfig struct { Version string `yaml:"version"` Revision string `yaml:"-"` SourceRevisions SourceRevisions `yaml:"-"` Catalog Catalog `yaml:"catalog,omitempty"` Device DeviceRuntimeConfig `yaml:"device"` Clients map[string]ClientProcessSpec `yaml:"clients,omitempty"` Defaults RuntimeDefaults `yaml:"defaults,omitempty"` Selection SelectionPolicy `yaml:"selection,omitempty"` Isolation IsolationPolicy `yaml:"isolation,omitempty"` Retention RetentionPolicy `yaml:"retention,omitempty"` Projects map[string]ProjectRegistration `yaml:"projects,omitempty"` } // RuntimeDefaults contains scalar and map defaults. ProfileAliases is merged // by key, with the user-local value winning for duplicate aliases. type RuntimeDefaults struct { DefaultProfile string `yaml:"default_profile,omitempty"` AutoResumeInterrupted bool `yaml:"auto_resume_interrupted,omitempty"` ProfileAliases map[string]string `yaml:"profile_aliases,omitempty"` } // RuntimeDefaultsOverride uses pointers for scalars so an explicit false or // empty value remains distinguishable from an omitted value. type RuntimeDefaultsOverride struct { DefaultProfile *string `yaml:"default_profile,omitempty"` AutoResumeInterrupted *bool `yaml:"auto_resume_interrupted,omitempty"` ProfileAliases map[string]string `yaml:"profile_aliases,omitempty"` } // TargetRef is a provider/model/profile identity consumed by the shared // selector. Profile is the runtime target; provider and model preserve the // declared identity when supplied. type TargetRef struct { Provider string `yaml:"provider,omitempty"` Model string `yaml:"model,omitempty"` Profile string `yaml:"profile,omitempty"` } // SelectionPolicy is ordered. Rules are evaluated by the selector in their // stored order; the config registry never sorts them. type SelectionPolicy struct { Version string `yaml:"version,omitempty"` Revision string `yaml:"-"` Timezone string `yaml:"timezone,omitempty"` Default TargetRef `yaml:"default,omitempty"` Rules []SelectionRule `yaml:"rules,omitempty"` } // SelectionPolicyOverride replaces Rules as a whole when rules is present, // including when it is explicitly an empty array. type SelectionPolicyOverride struct { Timezone *string `yaml:"timezone,omitempty"` Default *TargetRef `yaml:"default,omitempty"` Rules *[]SelectionRule `yaml:"rules,omitempty"` } // SelectionRule is a strict policy input. Evaluation belongs to the // agentpolicy package; this package validates and preserves rule order. type SelectionRule struct { ID string `yaml:"id"` Match SelectionMatch `yaml:"match,omitempty"` Target TargetRef `yaml:"target"` } // SelectionMatch contains the SDD-defined policy predicates without assigning // evaluator semantics to them. type SelectionMatch struct { TimeWindows []SelectionTimeWindow `yaml:"time_windows,omitempty"` QuotaStates []string `yaml:"quota_states,omitempty"` MinRemainingToken *int64 `yaml:"min_remaining_tokens,omitempty"` Agents []string `yaml:"agents,omitempty"` Stages []string `yaml:"stages,omitempty"` Lanes []string `yaml:"lanes,omitempty"` MinGrade int `yaml:"min_grade,omitempty"` MaxGrade int `yaml:"max_grade,omitempty"` Capabilities []string `yaml:"capabilities,omitempty"` FailureCodes []string `yaml:"failure_codes,omitempty"` } // SelectionTimeWindow is interpreted in SelectionPolicy.Timezone. type SelectionTimeWindow struct { Days []string `yaml:"days,omitempty"` Start string `yaml:"start"` End string `yaml:"end"` } // IsolationPolicy defines the default isolation mode and its ordered fallback // modes. FallbackModes is another whole-replacement ordered array. type IsolationPolicy struct { DefaultMode string `yaml:"default_mode,omitempty"` FallbackModes []string `yaml:"fallback_modes,omitempty"` } type IsolationPolicyOverride struct { DefaultMode *string `yaml:"default_mode,omitempty"` FallbackModes *[]string `yaml:"fallback_modes,omitempty"` } // RetentionPolicy contains non-negative local retention limits. Zero leaves a // limit disabled. type RetentionPolicy struct { CompletedDays int `yaml:"completed_days,omitempty"` BlockedDays int `yaml:"blocked_days,omitempty"` MaxProjectLogRecords int `yaml:"max_project_log_records,omitempty"` } type RetentionPolicyOverride struct { CompletedDays *int `yaml:"completed_days,omitempty"` BlockedDays *int `yaml:"blocked_days,omitempty"` MaxProjectLogRecords *int `yaml:"max_project_log_records,omitempty"` } // DeviceRuntimeConfig contains device-owned roots. StateRoot, OverlayRoot, and // LogRoot are required absolute clean paths; TempRoot and CacheRoot are // optional but must meet the same rule when set. type DeviceRuntimeConfig struct { StateRoot string `yaml:"state_root"` OverlayRoot string `yaml:"overlay_root"` LogRoot string `yaml:"log_root"` TempRoot string `yaml:"temp_root,omitempty"` CacheRoot string `yaml:"cache_root,omitempty"` } // ClientProcessSpec is a device-local, argv-only launch policy for one // daemon-owned desktop client. Environment and credential fields are // intentionally absent. type ClientProcessSpec struct { Executable string `yaml:"executable"` Args []string `yaml:"args,omitempty"` WorkingDirectory string `yaml:"working_directory"` LaunchOnStart bool `yaml:"launch_on_start,omitempty"` RestartOnCrash bool `yaml:"restart_on_crash,omitempty"` RestartLimit int `yaml:"restart_limit,omitempty"` RestartBackoffMillis int `yaml:"restart_backoff_millis,omitempty"` FocusArgs []string `yaml:"focus_args,omitempty"` } // RuntimeConfigOverride is applied field-by-field after its parent config. // Scalar pointers replace scalars, maps merge local-wins, and ordered arrays // replace rather than append. type RuntimeConfigOverride struct { Defaults RuntimeDefaultsOverride `yaml:"defaults,omitempty"` Selection SelectionPolicyOverride `yaml:"selection,omitempty"` Isolation IsolationPolicyOverride `yaml:"isolation,omitempty"` Retention RetentionPolicyOverride `yaml:"retention,omitempty"` } // ProjectRegistrationOverlay is the user-local schema for one project. type ProjectRegistrationOverlay struct { Workspace string `yaml:"workspace"` Enabled *bool `yaml:"enabled,omitempty"` SelectedMilestone string `yaml:"selected_milestone,omitempty"` Override RuntimeConfigOverride `yaml:"override,omitempty"` } // ProjectRegistration is an effective project configuration. It retains the // project identity and the complete revision-pinned defaults/policies that // apply to the registered workspace. type ProjectRegistration struct { ID string Workspace string Enabled bool SelectedMilestone string ConfigRevision string Defaults RuntimeDefaults Selection SelectionPolicy Isolation IsolationPolicy Retention RetentionPolicy AutoResumeInterrupted bool } // SourceRevisions identifies the exact byte content of both configuration // inputs. type SourceRevisions struct { RepoGlobal string UserLocal string } // RuntimeSnapshot is immutable through its API. The merged configuration is // private and every accessor returns a defensive deep copy. type RuntimeSnapshot struct { config RuntimeConfig revision string sources SourceRevisions } // LoadRuntimeConfig reads the repository input without opening it for writing, // reads the user-local input, and returns one immutable merged snapshot. func LoadRuntimeConfig(repoGlobalPath, userLocalPath string) (RuntimeSnapshot, error) { repoGlobal, err := os.ReadFile(repoGlobalPath) if err != nil { return RuntimeSnapshot{}, fmt.Errorf("agentconfig: read repo-global runtime config: %w", err) } userLocal, err := os.ReadFile(userLocalPath) if err != nil { return RuntimeSnapshot{}, fmt.Errorf("agentconfig: read user-local runtime config: %w", err) } return LoadRuntimeConfigBytes(repoGlobal, userLocal) } // LoadRuntimeConfigBytes strictly decodes and composes one repo-global and one // user-local document. It is pure and never writes either input. func LoadRuntimeConfigBytes(repoGlobal, userLocal []byte) (RuntimeSnapshot, error) { var global RepoGlobalRuntimeConfig if err := decodeStrictRuntimeConfig(repoGlobal, "repo-global", &global); err != nil { return RuntimeSnapshot{}, err } var local UserLocalRuntimeConfig if err := decodeStrictRuntimeConfig(userLocal, "user-local", &local); err != nil { return RuntimeSnapshot{}, err } normalizedGlobal, err := normalizeRepoGlobal(global) if err != nil { return RuntimeSnapshot{}, err } if err := validateUserLocal(local); err != nil { return RuntimeSnapshot{}, err } merged, err := mergeRuntimeConfig(normalizedGlobal, local) if err != nil { return RuntimeSnapshot{}, err } sources := SourceRevisions{ RepoGlobal: digestRevision(repoGlobal), UserLocal: digestRevision(userLocal), } revision := digestRevisionParts("runtime-config", sources.RepoGlobal, sources.UserLocal) merged.Revision = revision merged.SourceRevisions = sources merged.Selection.Revision = digestRevisionParts("selection-policy", revision) for projectID, project := range merged.Projects { project.ConfigRevision = revision project.Selection.Revision = digestRevisionParts("selection-policy", revision, projectID) merged.Projects[projectID] = project } return RuntimeSnapshot{ config: cloneRuntimeConfig(merged), revision: revision, sources: sources, }, nil } // Revision returns the immutable revision pinned to an invocation. func (s RuntimeSnapshot) Revision() string { return s.revision } // SourceRevisions returns the exact source revisions used by this snapshot. func (s RuntimeSnapshot) SourceRevisions() SourceRevisions { return s.sources } // Config returns a defensive deep copy of the merged runtime configuration. func (s RuntimeSnapshot) Config() RuntimeConfig { return cloneRuntimeConfig(s.config) } // Project returns a defensive copy of one effective project registration. func (s RuntimeSnapshot) Project(projectID string) (ProjectRegistration, bool) { project, ok := s.config.Projects[projectID] if !ok { return ProjectRegistration{}, false } return cloneProjectRegistration(project), true } func decodeStrictRuntimeConfig(data []byte, source string, destination any) error { decoder := yaml.NewDecoder(strings.NewReader(string(data))) decoder.KnownFields(true) if err := decoder.Decode(destination); err != nil { return fmt.Errorf("agentconfig: decode %s runtime config: %w", source, err) } var extra any if err := decoder.Decode(&extra); err != io.EOF { if err == nil { return fmt.Errorf("agentconfig: %s runtime config must contain exactly one YAML document", source) } return fmt.Errorf("agentconfig: decode trailing %s runtime document: %w", source, err) } return nil } func normalizeRepoGlobal(global RepoGlobalRuntimeConfig) (RepoGlobalRuntimeConfig, error) { if global.Version != RuntimeConfigSchemaVersion { return RepoGlobalRuntimeConfig{}, fmt.Errorf("agentconfig: unsupported repo-global runtime config version %q", global.Version) } if catalogConfigured(global.Catalog) { catalog, err := Normalize(global.Catalog) if err != nil { return RepoGlobalRuntimeConfig{}, fmt.Errorf("agentconfig: repo-global catalog: %w", err) } global.Catalog = catalog } if err := validateRuntimeDefaults("repo-global defaults", global.Defaults); err != nil { return RepoGlobalRuntimeConfig{}, err } if global.Selection.Version == "" { global.Selection.Version = RuntimeConfigSchemaVersion } if err := validateSelectionPolicy("repo-global selection", global.Selection); err != nil { return RepoGlobalRuntimeConfig{}, err } if err := validateIsolationPolicy("repo-global isolation", global.Isolation); err != nil { return RepoGlobalRuntimeConfig{}, err } if err := validateRetentionPolicy("repo-global retention", global.Retention); err != nil { return RepoGlobalRuntimeConfig{}, err } return global, nil } func validateUserLocal(local UserLocalRuntimeConfig) error { if local.Version != RuntimeConfigSchemaVersion { return fmt.Errorf("agentconfig: unsupported user-local runtime config version %q", local.Version) } if err := validateDeviceConfig(local.Device); err != nil { return err } if err := validateClientProcessSpecs(local.Clients); err != nil { return err } if err := validateRuntimeOverride("user-local override", local.Override); err != nil { return err } for projectID, project := range local.Projects { if err := validateID("project", projectID); err != nil { return err } if err := validateCleanAbsolutePath("project "+projectID+" workspace", project.Workspace, true); err != nil { return err } if err := validateRuntimeOverride("project "+projectID+" override", project.Override); err != nil { return err } } return nil } func mergeRuntimeConfig(global RepoGlobalRuntimeConfig, local UserLocalRuntimeConfig) (RuntimeConfig, error) { merged := RuntimeConfig{ Version: RuntimeConfigSchemaVersion, Catalog: cloneCatalog(global.Catalog), Device: local.Device, Clients: cloneClientProcessSpecs(local.Clients), Defaults: cloneRuntimeDefaults(global.Defaults), Selection: cloneSelectionPolicy(global.Selection), Isolation: cloneIsolationPolicy(global.Isolation), Retention: global.Retention, Projects: make(map[string]ProjectRegistration, len(local.Projects)), } applyRuntimeOverride(&merged.Defaults, &merged.Selection, &merged.Isolation, &merged.Retention, local.Override) if err := validateMergedRuntimeConfig("merged runtime config", merged.Defaults, merged.Selection, merged.Isolation, merged.Retention); err != nil { return RuntimeConfig{}, err } if err := validateRuntimeCatalogBindings("merged runtime config", merged.Catalog, merged.Defaults, merged.Selection); err != nil { return RuntimeConfig{}, err } for projectID, overlay := range local.Projects { defaults := cloneRuntimeDefaults(merged.Defaults) selection := cloneSelectionPolicy(merged.Selection) isolation := cloneIsolationPolicy(merged.Isolation) retention := merged.Retention applyRuntimeOverride(&defaults, &selection, &isolation, &retention, overlay.Override) if err := validateMergedRuntimeConfig("project "+projectID, defaults, selection, isolation, retention); err != nil { return RuntimeConfig{}, err } if err := validateRuntimeCatalogBindings("project "+projectID, merged.Catalog, defaults, selection); err != nil { return RuntimeConfig{}, err } enabled := true if overlay.Enabled != nil { enabled = *overlay.Enabled } merged.Projects[projectID] = ProjectRegistration{ ID: projectID, Workspace: overlay.Workspace, Enabled: enabled, SelectedMilestone: overlay.SelectedMilestone, Defaults: defaults, Selection: selection, Isolation: isolation, Retention: retention, AutoResumeInterrupted: defaults.AutoResumeInterrupted, } } return merged, nil } func applyRuntimeOverride( defaults *RuntimeDefaults, selection *SelectionPolicy, isolation *IsolationPolicy, retention *RetentionPolicy, override RuntimeConfigOverride, ) { if override.Defaults.DefaultProfile != nil { defaults.DefaultProfile = *override.Defaults.DefaultProfile } if override.Defaults.AutoResumeInterrupted != nil { defaults.AutoResumeInterrupted = *override.Defaults.AutoResumeInterrupted } if override.Defaults.ProfileAliases != nil { if defaults.ProfileAliases == nil { defaults.ProfileAliases = make(map[string]string, len(override.Defaults.ProfileAliases)) } for alias, profile := range override.Defaults.ProfileAliases { defaults.ProfileAliases[alias] = profile } } if override.Selection.Timezone != nil { selection.Timezone = *override.Selection.Timezone } if override.Selection.Default != nil { selection.Default = *override.Selection.Default } if override.Selection.Rules != nil { selection.Rules = cloneSelectionRules(*override.Selection.Rules) } if override.Isolation.DefaultMode != nil { isolation.DefaultMode = *override.Isolation.DefaultMode } if override.Isolation.FallbackModes != nil { isolation.FallbackModes = append([]string(nil), (*override.Isolation.FallbackModes)...) } if override.Retention.CompletedDays != nil { retention.CompletedDays = *override.Retention.CompletedDays } if override.Retention.BlockedDays != nil { retention.BlockedDays = *override.Retention.BlockedDays } if override.Retention.MaxProjectLogRecords != nil { retention.MaxProjectLogRecords = *override.Retention.MaxProjectLogRecords } } func validateMergedRuntimeConfig( label string, defaults RuntimeDefaults, selection SelectionPolicy, isolation IsolationPolicy, retention RetentionPolicy, ) error { if err := validateRuntimeDefaults(label+" defaults", defaults); err != nil { return err } if err := validateSelectionPolicy(label+" selection", selection); err != nil { return err } if err := validateIsolationPolicy(label+" isolation", isolation); err != nil { return err } return validateRetentionPolicy(label+" retention", retention) } func validateRuntimeOverride(label string, override RuntimeConfigOverride) error { if override.Defaults.DefaultProfile != nil && *override.Defaults.DefaultProfile != "" { if err := validateID(label+" default profile", *override.Defaults.DefaultProfile); err != nil { return err } } for alias, profile := range override.Defaults.ProfileAliases { if err := validateAlias(label, alias, profile); err != nil { return err } } if override.Selection.Default != nil { if err := validateTargetRef(label+" selection default", *override.Selection.Default); err != nil { return err } } if override.Selection.Rules != nil { if err := validateSelectionRules(label+" selection rules", *override.Selection.Rules); err != nil { return err } } if override.Isolation.DefaultMode != nil { if err := validateIsolationMode(label+" default isolation", *override.Isolation.DefaultMode, true); err != nil { return err } } if override.Isolation.FallbackModes != nil { if err := validateIsolationModes(label+" isolation fallbacks", *override.Isolation.FallbackModes); err != nil { return err } } for field, value := range map[string]*int{ "completed_days": override.Retention.CompletedDays, "blocked_days": override.Retention.BlockedDays, "max_project_log_records": override.Retention.MaxProjectLogRecords, } { if value != nil && *value < 0 { return fmt.Errorf("agentconfig: %s retention %s must be non-negative", label, field) } } return nil } func validateRuntimeDefaults(label string, defaults RuntimeDefaults) error { if defaults.DefaultProfile != "" { if err := validateID(label+" default profile", defaults.DefaultProfile); err != nil { return err } } for alias, profile := range defaults.ProfileAliases { if err := validateAlias(label, alias, profile); err != nil { return err } } return nil } func validateRuntimeCatalogBindings( label string, catalog Catalog, defaults RuntimeDefaults, selection SelectionPolicy, ) error { if !catalogConfigured(catalog) { return nil } profileIDs := make([]string, 0, 2+len(defaults.ProfileAliases)+len(selection.Rules)) if defaults.DefaultProfile != "" { profileIDs = append(profileIDs, defaults.DefaultProfile) } for _, profileID := range defaults.ProfileAliases { profileIDs = append(profileIDs, profileID) } for _, profileID := range profileIDs { if _, ok := catalog.ResolveProfile(profileID); !ok { return fmt.Errorf("agentconfig: %s references unknown profile %q", label, profileID) } } if err := validateTargetCatalogBinding(label+" selection default", catalog, selection.Default); err != nil { return err } for _, rule := range selection.Rules { if err := validateTargetCatalogBinding("selection rule "+rule.ID+" target", catalog, rule.Target); err != nil { return err } } return nil } func validateTargetCatalogBinding(label string, catalog Catalog, target TargetRef) error { if target.Profile == "" { return nil } resolved, ok := catalog.ResolveProfile(target.Profile) if !ok { return fmt.Errorf("agentconfig: %s references unknown profile %q", label, target.Profile) } if target.Provider != "" && target.Provider != resolved.Provider.ID { return fmt.Errorf( "agentconfig: %s provider %q does not match profile provider %q", label, target.Provider, resolved.Provider.ID, ) } if target.Model != "" && target.Model != resolved.Model.ID { return fmt.Errorf( "agentconfig: %s model %q does not match profile model %q", label, target.Model, resolved.Model.ID, ) } return nil } func validateAlias(label, alias, profile string) error { if err := validateID(label+" alias", alias); err != nil { return err } if err := validateID(label+" alias profile", profile); err != nil { return err } return nil } func validateSelectionPolicy(label string, policy SelectionPolicy) error { if policy.Version != RuntimeConfigSchemaVersion { return fmt.Errorf("agentconfig: %s has unsupported version %q", label, policy.Version) } if err := validateTargetRef(label+" default", policy.Default); err != nil { return err } return validateSelectionRules(label+" rules", policy.Rules) } func validateSelectionRules(label string, rules []SelectionRule) error { seen := make(map[string]struct{}, len(rules)) for _, rule := range rules { if err := validateID("selection rule", rule.ID); err != nil { return err } if _, exists := seen[rule.ID]; exists { return fmt.Errorf("agentconfig: %s repeats rule id %q", label, rule.ID) } seen[rule.ID] = struct{}{} if err := validateTargetRef("selection rule "+rule.ID+" target", rule.Target); err != nil { return err } if rule.Target.Profile == "" { return fmt.Errorf("agentconfig: selection rule %q target.profile is required", rule.ID) } if rule.Match.MinRemainingToken != nil && *rule.Match.MinRemainingToken < 0 { return fmt.Errorf("agentconfig: selection rule %q min_remaining_tokens must be non-negative", rule.ID) } if rule.Match.MinGrade < 0 || rule.Match.MaxGrade < 0 { return fmt.Errorf("agentconfig: selection rule %q grades must be non-negative", rule.ID) } if rule.Match.MinGrade != 0 && rule.Match.MaxGrade != 0 && rule.Match.MinGrade > rule.Match.MaxGrade { return fmt.Errorf("agentconfig: selection rule %q min_grade exceeds max_grade", rule.ID) } for _, window := range rule.Match.TimeWindows { if strings.TrimSpace(window.Start) == "" || strings.TrimSpace(window.End) == "" { return fmt.Errorf("agentconfig: selection rule %q time window start and end are required", rule.ID) } } } return nil } func validateTargetRef(label string, target TargetRef) error { if target.Profile == "" && (target.Provider != "" || target.Model != "") { return fmt.Errorf("agentconfig: %s profile is required when provider or model is set", label) } for field, value := range map[string]string{ "provider": target.Provider, "model": target.Model, "profile": target.Profile, } { if value != "" { if err := validateID(label+" "+field, value); err != nil { return err } } } return nil } func validateIsolationPolicy(label string, policy IsolationPolicy) error { if err := validateIsolationMode(label+" default_mode", policy.DefaultMode, false); err != nil { return err } return validateIsolationModes(label+" fallback_modes", policy.FallbackModes) } func validateIsolationModes(label string, modes []string) error { seen := make(map[string]struct{}, len(modes)) for _, mode := range modes { if err := validateIsolationMode(label, mode, true); err != nil { return err } if _, exists := seen[mode]; exists { return fmt.Errorf("agentconfig: %s repeats mode %q", label, mode) } seen[mode] = struct{}{} } return nil } func validateIsolationMode(label, mode string, required bool) error { if mode == "" && !required { return nil } switch mode { case "overlay", "worktree", "clone": return nil default: return fmt.Errorf("agentconfig: %s has unsupported mode %q", label, mode) } } func validateRetentionPolicy(label string, retention RetentionPolicy) error { if retention.CompletedDays < 0 || retention.BlockedDays < 0 || retention.MaxProjectLogRecords < 0 { return fmt.Errorf("agentconfig: %s values must be non-negative", label) } return nil } func validateDeviceConfig(device DeviceRuntimeConfig) error { for label, path := range map[string]string{ "state_root": device.StateRoot, "overlay_root": device.OverlayRoot, "log_root": device.LogRoot, } { if err := validateCleanAbsolutePath("device "+label, path, true); err != nil { return err } } for label, path := range map[string]string{ "temp_root": device.TempRoot, "cache_root": device.CacheRoot, } { if err := validateCleanAbsolutePath("device "+label, path, false); err != nil { return err } } return nil } func validateClientProcessSpecs(specs map[string]ClientProcessSpec) error { for kind, spec := range specs { switch kind { case "flutter", "unity": default: return fmt.Errorf("agentconfig: unsupported client kind %q", kind) } if err := validateCleanAbsolutePath( "client "+kind+" executable", spec.Executable, true, ); err != nil { return err } if err := validateCleanAbsolutePath( "client "+kind+" working_directory", spec.WorkingDirectory, true, ); err != nil { return err } if err := validateClientArguments("client "+kind+" args", spec.Args); err != nil { return err } if err := validateClientArguments( "client "+kind+" focus_args", spec.FocusArgs, ); err != nil { return err } if kind != "flutter" && len(spec.FocusArgs) != 0 { return fmt.Errorf( "agentconfig: client %s cannot configure Flutter focus arguments", kind, ) } if spec.RestartLimit < 0 || spec.RestartBackoffMillis < 0 { return fmt.Errorf( "agentconfig: client %s restart limits must be non-negative", kind, ) } if spec.RestartLimit > 100 || spec.RestartBackoffMillis > 60_000 { return fmt.Errorf( "agentconfig: client %s restart policy exceeds its bound", kind, ) } if spec.RestartOnCrash && spec.RestartLimit == 0 { return fmt.Errorf( "agentconfig: client %s restart_limit must be positive when restart_on_crash is enabled", kind, ) } if !spec.RestartOnCrash && (spec.RestartLimit != 0 || spec.RestartBackoffMillis != 0) { return fmt.Errorf( "agentconfig: client %s restart policy requires restart_on_crash", kind, ) } } return nil } func validateClientArguments(label string, arguments []string) error { if len(arguments) > 256 { return fmt.Errorf("agentconfig: %s exceeds 256 arguments", label) } for _, argument := range arguments { if len(argument) > 32<<10 || strings.ContainsRune(argument, '\x00') { return fmt.Errorf("agentconfig: %s contains an invalid argument", label) } } return nil } func validateCleanAbsolutePath(label, path string, required bool) error { if path == "" && !required { return nil } if strings.TrimSpace(path) == "" { return fmt.Errorf("agentconfig: %s is required", label) } if !filepath.IsAbs(path) || filepath.Clean(path) != path { return fmt.Errorf("agentconfig: %s must be an absolute clean path", label) } return nil } func catalogConfigured(catalog Catalog) bool { return catalog.Version != "" || len(catalog.Providers) != 0 || len(catalog.Models) != 0 || len(catalog.Profiles) != 0 } func digestRevision(data []byte) string { sum := sha256.Sum256(data) return "sha256:" + hex.EncodeToString(sum[:]) } func digestRevisionParts(parts ...string) string { hash := sha256.New() var length [8]byte for _, part := range parts { binary.BigEndian.PutUint64(length[:], uint64(len(part))) _, _ = hash.Write(length[:]) _, _ = hash.Write([]byte(part)) } return "sha256:" + hex.EncodeToString(hash.Sum(nil)) } func cloneRuntimeConfig(config RuntimeConfig) RuntimeConfig { out := config out.Catalog = cloneCatalog(config.Catalog) out.Clients = cloneClientProcessSpecs(config.Clients) out.Defaults = cloneRuntimeDefaults(config.Defaults) out.Selection = cloneSelectionPolicy(config.Selection) out.Isolation = cloneIsolationPolicy(config.Isolation) if config.Projects != nil { out.Projects = make(map[string]ProjectRegistration, len(config.Projects)) for id, project := range config.Projects { out.Projects[id] = cloneProjectRegistration(project) } } return out } func cloneClientProcessSpecs( specs map[string]ClientProcessSpec, ) map[string]ClientProcessSpec { if specs == nil { return nil } out := make(map[string]ClientProcessSpec, len(specs)) for kind, spec := range specs { spec.Args = append([]string(nil), spec.Args...) spec.FocusArgs = append([]string(nil), spec.FocusArgs...) out[kind] = spec } return out } func cloneProjectRegistration(project ProjectRegistration) ProjectRegistration { out := project out.Defaults = cloneRuntimeDefaults(project.Defaults) out.Selection = cloneSelectionPolicy(project.Selection) out.Isolation = cloneIsolationPolicy(project.Isolation) return out } func cloneRuntimeDefaults(defaults RuntimeDefaults) RuntimeDefaults { out := defaults if defaults.ProfileAliases != nil { out.ProfileAliases = make(map[string]string, len(defaults.ProfileAliases)) for alias, profile := range defaults.ProfileAliases { out.ProfileAliases[alias] = profile } } return out } func cloneSelectionPolicy(policy SelectionPolicy) SelectionPolicy { out := policy out.Rules = cloneSelectionRules(policy.Rules) return out } func cloneSelectionRules(rules []SelectionRule) []SelectionRule { if rules == nil { return nil } out := make([]SelectionRule, len(rules)) for index, rule := range rules { out[index] = rule out[index].Match.TimeWindows = append([]SelectionTimeWindow(nil), rule.Match.TimeWindows...) for windowIndex := range out[index].Match.TimeWindows { out[index].Match.TimeWindows[windowIndex].Days = append( []string(nil), rule.Match.TimeWindows[windowIndex].Days..., ) } out[index].Match.QuotaStates = append([]string(nil), rule.Match.QuotaStates...) out[index].Match.Agents = append([]string(nil), rule.Match.Agents...) out[index].Match.Stages = append([]string(nil), rule.Match.Stages...) out[index].Match.Lanes = append([]string(nil), rule.Match.Lanes...) out[index].Match.Capabilities = append([]string(nil), rule.Match.Capabilities...) out[index].Match.FailureCodes = append([]string(nil), rule.Match.FailureCodes...) if rule.Match.MinRemainingToken != nil { value := *rule.Match.MinRemainingToken out[index].Match.MinRemainingToken = &value } } return out } func cloneIsolationPolicy(policy IsolationPolicy) IsolationPolicy { out := policy out.FallbackModes = append([]string(nil), policy.FallbackModes...) return out } func cloneCatalog(catalog Catalog) Catalog { out := catalog if catalog.Providers != nil { out.Providers = make([]Provider, len(catalog.Providers)) for index, provider := range catalog.Providers { out.Providers[index] = provider out.Providers[index].VersionProbe.Args = append([]string(nil), provider.VersionProbe.Args...) out.Providers[index].Authentication.Args = append([]string(nil), provider.Authentication.Args...) out.Providers[index].ModelProbe.Args = append([]string(nil), provider.ModelProbe.Args...) out.Providers[index].Capabilities = append([]string(nil), provider.Capabilities...) } } out.Models = append([]Model(nil), catalog.Models...) if catalog.Profiles != nil { out.Profiles = make([]Profile, len(catalog.Profiles)) for index, profile := range catalog.Profiles { out.Profiles[index] = profile out.Profiles[index].Args = append([]string(nil), profile.Args...) out.Profiles[index].ResumeArgs = append([]string(nil), profile.ResumeArgs...) out.Profiles[index].Env = append([]string(nil), profile.Env...) out.Profiles[index].Capabilities = append([]string(nil), profile.Capabilities...) } } return out }