okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5924d14
main
1package daemon 2 3import ( 4"context" 5"errors" 6"io" 7"net/http" 8 9"albedo/cli/internal/config" 10"albedo/cli/internal/daemon/protocol" 11) 12 13type ReloadResult struct {Message ,Warning string } 14type UIPreferencesPatch struct { 15Thinking * bool `json:"thinking,omitempty"` 16Tools * bool `json:"tools,omitempty"` 17Pinned * bool `json:"pinned,omitempty"` 18Archived * bool `json:"archived,omitempty"` 19ETag string `json:"-"` 20} 21type UIPreferences struct { 22DismissedNotices []string 23Opens map [string ]int 24Pinned ,Archived []string 25Thinking ,Tools bool 26ETag string 27SessionETags map [string ]string 28} 29type Settings struct { 30Credentials Credentials 31Profiles config.Profiles 32Capabilities config.CapabilityPrefs 33MCP map [string ]config.MCPServer 34UI UIPreferences 35ETags map [string ]string 36Extensions map [string ]bool 37} 38 39func GetSettings (ctx context.Context ,conn * Connection ) (Settings ,error ) { 40var wire protocol.Settings 41err := executeRead (ctx ,conn ,operation {Name :"read shared settings" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 42return protocol .NewGetSettingsRequest (base ,nil ) 43 },Policy :readRecovery },func (data []byte )error { 44return decodeRequired (data ,& wire ) 45 }) 46if err != nil { 47return Settings {},err 48 } 49result := Settings {Profiles : config.Profiles {Active :value (wire .Providers .DefaultProfile ),Providers :map [string ]config.Settings {},ETag :wire .GroupResources .Providers .ETag },MCP :map [string ]config.MCPServer {},Credentials :Credentials {MCP :map [string ]MCPSecretNames {}},ETags :map [string ]string {"providers" :wire .GroupResources .Providers .ETag ,"mcp" :wire .GroupResources .MCP .ETag ,"extensions" :wire .GroupResources .Extensions .ETag ,"capabilities" :wire .GroupResources .Capabilities .ETag ,"models" :wire .GroupResources .Models .ETag ,"ui" :wire .GroupResources .UI .ETag },Extensions :wire .Extensions .Defaults } 50for name ,profile := range wire .Providers .Profiles { 51result .Profiles .Providers [name ]= config.Settings {ProfileName :name ,Extension :profile .Extension ,BaseURL :value (profile .Endpoint ),Model :profile .Model ,Protocol :profile .Protocol ,HasKey :profile .HasKey ,Effort :profile .Effort ,ImageEdge :intPointer (profile .ImageEdge ),AccountID :profile .AccountID ,Project :profile .Project ,Location :profile .Location } 52if profile .HasKey { 53result .Credentials .Providers = append (result .Credentials .Providers ,name ) 54 } 55 } 56for name ,server := range wire .MCP .Definitions { 57enabled := server .Enabled 58result .MCP [name ]= config.MCPServer {Enabled :& enabled ,Type :server .Transport ,URL :value (server .URL ),Command :value (server .Command ),Args :server .Arguments ,CWD :value (server .Cwd ),BearerTokenEnvVar :value (server .BearerTokenEnvVar ),Env :sourceValues (server .Environment ),Headers :sourceValues (server .Headers ),EnabledTools :server .EnabledTools ,DisabledTools :server .DisabledTools ,StartupTimeoutMs :int (server .StartupTimeoutMs ),CallTimeoutMs :int (server .CallTimeoutMs )} 59result .Credentials .MCP [name ]= MCPSecretNames {BearerToken :server .SecretPresence .BearerToken ,Headers :server .SecretPresence .Headers ,Env :server .SecretPresence .Environment } 60 } 61result .Capabilities = config.CapabilityPrefs {Global :map [string ]map [string ]bool {"mcp" : {}},Sessions :map [string ]map [string ]map [string ]bool {}} 62for name := range result .MCP { 63if preference ,ok := wire .Capabilities .Preferences ["mcp:" + name ];ok { 64result .Capabilities .Global ["mcp" ][name ]= preference 65 } 66 } 67result .UI = UIPreferences {DismissedNotices :wire .UI .DismissedNotices ,Thinking :wire .UI .Thinking ,Tools :wire .UI .Tools ,ETag :wire .GroupResources .UI .ETag } 68for _ ,etag := range result .ETags { 69if etag == "" { 70return Settings {},fieldError ("settings group validator" ) 71 } 72 } 73return result ,nil 74} 75func sourceValues (values map [string ]protocol.ValueSource )map [string ]map [string ]string { 76result := map [string ]map [string ]string {} 77for name ,value := range values { 78result [name ]= map [string ]string {"source" :value .Source ,"value" :value .Value } 79 } 80return result 81} 82func ProviderProfiles (ctx context.Context ,conn * Connection ) (config.Profiles ,error ) { 83settings ,err := GetSettings (ctx ,conn ) 84return settings .Profiles ,err 85} 86 87func patchSettingsGroup (ctx context.Context ,conn * Connection ,group ,etag string ,body any ,target any )error { 88headers ,err := observedHeaders (etag ) 89if err != nil { 90return err 91 } 92return executeMutation (ctx ,conn ,operation {Name :"edit " + group + " settings" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 93return protocol .NewPatchSettingsRequestWithBody (base ,& protocol.PatchSettingsParams {Group :group ,IfMatch :etag },"application/merge-patch+json" ,body ) 94 },Headers :headers ,Body :body ,Policy :authRecovery }, []int {200 },func (data []byte ,_ int )error { 95if err := decodeRequired (data ,target );err != nil { 96return err 97 } 98var savedGroup ,savedURL ,savedETag string 99switch change := target .(type ) { 100case * protocol.ProviderSettingsChange : 101savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 102case * protocol.CapabilitySettingsChange : 103savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 104case * protocol.MCPSettingsChange : 105savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 106case * protocol.ExtensionSettingsChange : 107savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 108case * protocol.ModelSettingsChange : 109savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 110case * protocol.UISettingsChange : 111savedGroup ,savedURL ,savedETag = change .Group ,change .Resource .URL ,change .Resource .ETag 112default : 113return fieldError ("settings group" ) 114 } 115request ,err := protocol .NewGetSettingsRequest ("" ,& protocol.GetSettingsParams {Group :& group }) 116if err != nil { 117return err 118 } 119if savedGroup != group || savedETag == "" || savedURL != request .URL .RequestURI () { 120return fieldError ("settings resource" ) 121 } 122return nil 123 }) 124} 125func SaveProvider (ctx context.Context ,conn * Connection ,name string ,profile config.Settings ,etag string )error { 126return saveProvider (ctx ,conn ,name ,profile ,etag ,false ) 127} 128func SaveAndSelectProvider (ctx context.Context ,conn * Connection ,name string ,profile config.Settings ,etag string )error { 129return saveProvider (ctx ,conn ,name ,profile ,etag ,true ) 130} 131func saveProvider (ctx context.Context ,conn * Connection ,name string ,profile config.Settings ,etag string ,selectDefault bool )error { 132// Nullable patch fields must be emitted to clear saved overrides. Generated 133// optional pointers omit nil and would preserve those overrides instead. 134profilePatch := map [string ]any { 135"extension" :profile .Extension ,"endpoint" :optionalText (profile .BaseURL ), 136"protocol" :profile .Protocol ,"model" :profile .Model ,"effort" :profile .Effort , 137"image_edge" :profile .ImageEdge ,"account_id" :profile .AccountID , 138"project" :profile .Project ,"location" :profile .Location , 139 } 140if profile .APIKey != "" { 141profilePatch ["api_key" ]= profile .APIKey 142 } 143body := map [string ]any {"profiles" :map [string ]any {name :profilePatch }} 144if selectDefault { 145body ["default_profile" ]= name 146 } 147var change protocol.ProviderSettingsChange 148err := patchSettingsGroup (ctx ,conn ,"providers" ,etag ,body ,& change ) 149return err 150} 151func DeleteProvider (ctx context.Context ,conn * Connection ,name string ,profiles config.Profiles )error { 152body := map [string ]any {"profiles" :map [string ]any {name :nil }} 153if profiles .Active == name { 154body ["default_profile" ]= nil 155 } 156var change protocol.ProviderSettingsChange 157err := patchSettingsGroup (ctx ,conn ,"providers" ,profiles .ETag ,body ,& change ) 158return err 159} 160 161type CapabilitySelectionRequest struct { 162Kind ,Name ,Scope ,ETag ,Revision string 163Enabled * bool 164} 165 166func SetCapability (ctx context.Context ,conn * Connection ,session string ,request CapabilitySelectionRequest ) (ReloadResult ,error ) { 167return setSelection (ctx ,conn ,session ,request .Kind ,request .Name ,request .Revision ,request .Scope ,request .ETag ,request .Enabled ) 168} 169func setSelection (ctx context.Context ,conn * Connection ,session ,kind ,id ,revision ,scope ,etag string ,enabled * bool ) (ReloadResult ,error ) { 170if scope == "global" { 171var change protocol.CapabilitySettingsChange 172err := patchSettingsGroup (ctx ,conn ,"capabilities" ,etag , protocol.CapabilitySettingsPatch { 173CatalogSessionID :session ,CatalogRevision :revision ,Choices :map [string ]* bool {id :enabled }, 174 },& change ) 175return ReloadResult {Message :"Saved the global default; reload open sessions to apply it." },err 176 } 177body := map [string ]any {"selection" :map [string ]any {kind :map [string ]* bool {id :enabled }}} 178if kind == "skills" || kind == "instructions" { 179body ["catalog_revision" ]= revision 180 } 181_ ,err := patchSession (ctx ,conn ,session ,etag ,body ) 182return ReloadResult {Message :"Saved the session selection; reload the session to apply it." },err 183} 184 185type MCPUpdateRequest struct { 186Name ,ETag string 187Server * config.MCPServer 188Secrets MCPSecretsPatch 189StoredSecrets MCPSecretNames 190} 191 192func SaveMCP (ctx context.Context ,conn * Connection ,session string ,request MCPUpdateRequest ) (ReloadResult ,error ) { 193var definition any 194if request .Server != nil { 195server := request .Server 196secrets := request .Secrets 197if secrets .ClearHeaders { 198secrets .ClearHeaders = false 199secrets .Headers = map [string ]* string {} 200for _ ,name := range request .StoredSecrets .Headers { 201secrets .Headers [name ]= nil 202 } 203 } 204if secrets .ClearEnv { 205secrets .ClearEnv = false 206secrets .Env = map [string ]* string {} 207for _ ,name := range request .StoredSecrets .Env { 208secrets .Env [name ]= nil 209 } 210 } 211// These nulls clear fields when changing transports; nil optional 212// pointers in MCPDefinitionPatch would omit them. 213patch := map [string ]any { 214"enabled" :server .Enabled == nil || * server .Enabled ,"transport" :server .Type , 215"command" :optionalText (server .Command ),"arguments" :nonNil (server .Args ), 216"cwd" :optionalText (server .CWD ),"url" :optionalText (server .URL ), 217"environment" :nonNilMap (server .Env ),"headers" :nonNilMap (server .Headers ), 218"bearer_token_env_var" :optionalText (server .BearerTokenEnvVar ), 219"enabled_tools" :nonNil (server .EnabledTools ),"disabled_tools" :nonNil (server .DisabledTools ), 220"secrets" :secrets , 221 } 222if server .StartupTimeoutMs != 0 { 223patch ["startup_timeout_ms" ]= server .StartupTimeoutMs 224 } 225if server .CallTimeoutMs != 0 { 226patch ["call_timeout_ms" ]= server .CallTimeoutMs 227 } 228definition = patch 229 } 230var change protocol.MCPSettingsChange 231err := patchSettingsGroup (ctx ,conn ,"mcp" ,request .ETag ,map [string ]any { 232"definitions" :map [string ]any {request .Name :definition }, 233 },& change ) 234return applicationResult (change .Application ),err 235} 236func nonNil [T any ](s []T ) []T { 237if s == nil { 238return []T {} 239 } 240return s 241} 242func nonNilMap [K comparable ,V any ](m map [K ]V )map [K ]V { 243if m == nil { 244return map [K ]V {} 245 } 246return m 247} 248func applicationResult (application protocol.SettingsApplication )ReloadResult { 249result := ReloadResult {Message :"Saved settings." } 250if application .CompositionChanged { 251result .Message = "Saved settings; reload open sessions to apply them." 252 } 253for _ ,warning := range application .Warnings { 254if result .Warning != "" { 255result .Warning += "; " 256 } 257result .Warning += warning .Detail 258 } 259return result 260} 261func PatchUI (ctx context.Context ,conn * Connection ,patch UIPreferencesPatch ) (UIPreferences ,error ) { 262if patch .Pinned != nil || patch .Archived != nil { 263return UIPreferences {},errors .New ("pin and archive are session preferences" ) 264 } 265var change protocol.UISettingsChange 266err := patchSettingsGroup (ctx ,conn ,"ui" ,patch .ETag ,patch ,& change ) 267return UIPreferences {Thinking :change .Resource .Value .Thinking ,Tools :change .Resource .Value .Tools ,ETag :change .Resource .ETag },err 268} 269 270// PatchSessionUI pins or archives one session. Session lists carry no ETag, 271// so a caller without one has the session's current ETag read first. 272func PatchSessionUI (ctx context.Context ,conn * Connection ,session string ,patch UIPreferencesPatch ) (UIPreferences ,error ) { 273if patch .Thinking != nil || patch .Tools != nil { 274return UIPreferences {},errors .New ("thinking and tools are shared preferences" ) 275 } 276if patch .ETag == "" { 277current ,err := GetSessionConfiguration (ctx ,conn ,session ) 278if err != nil { 279return UIPreferences {},err 280 } 281patch .ETag = current .ETag 282 } 283body := protocol.SessionPatch {Preferences :& protocol.PreferencePatch {Pinned :patch .Pinned ,Archived :patch .Archived }} 284updated ,err := patchSession (ctx ,conn ,session ,patch .ETag ,body ) 285prefs := UIPreferences {SessionETags :map [string ]string {session :updated .ETag }} 286if updated .Pinned { 287prefs .Pinned = []string {session } 288 } 289if updated .Archived { 290prefs .Archived = []string {session } 291 } 292return prefs ,err 293} 294func RecordOpen (ctx context.Context ,conn * Connection ,session string ) (UIPreferences ,error ) { 295id ,err := operationID () 296if err != nil { 297return UIPreferences {},err 298 } 299var visit protocol.Visit 300err = executeMutation (ctx ,conn ,operation {Name :"record session visit" ,BuildRequest :func (base string ,body io.Reader ) (* http.Request ,error ) { 301return protocol .NewRecordVisitRequestWithBody (base ,session ,id ,"application/json" ,body ) 302 },Body :struct {}{},Policy :noRecovery }, []int {201 ,200 },func (data []byte ,_ int )error { 303if err := decodeRequired (data ,& visit );err != nil { 304return err 305 } 306if visit .VisitID != id || visit .SessionID != session { 307return fieldError ("visit identity" ) 308 } 309return nil 310 }) 311return UIPreferences {Opens :map [string ]int {session :int (visit .Opens )}},err 312}