okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(cli): remove unused state fields5924d14

main
13.4 KiB312 linesraw
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 {
15	Thinking *bool  `json:"thinking,omitempty"`
16	Tools    *bool  `json:"tools,omitempty"`
17	Pinned   *bool  `json:"pinned,omitempty"`
18	Archived *bool  `json:"archived,omitempty"`
19	ETag     string `json:"-"`
20}
21type UIPreferences struct {
22	DismissedNotices []string
23	Opens            map[string]int
24	Pinned, Archived []string
25	Thinking, Tools  bool
26	ETag             string
27	SessionETags     map[string]string
28}
29type Settings struct {
30	Credentials  Credentials
31	Profiles     config.Profiles
32	Capabilities config.CapabilityPrefs
33	MCP          map[string]config.MCPServer
34	UI           UIPreferences
35	ETags        map[string]string
36	Extensions   map[string]bool
37}
38
39func GetSettings(ctx context.Context, conn *Connection) (Settings, error) {
40	var wire protocol.Settings
41	err := executeRead(ctx, conn, operation{Name: "read shared settings", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
42		return protocol.NewGetSettingsRequest(base, nil)
43	}, Policy: readRecovery}, func(data []byte) error {
44		return decodeRequired(data, &wire)
45	})
46	if err != nil {
47		return Settings{}, err
48	}
49	result := 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}
50	for name, profile := range wire.Providers.Profiles {
51		result.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}
52		if profile.HasKey {
53			result.Credentials.Providers = append(result.Credentials.Providers, name)
54		}
55	}
56	for name, server := range wire.MCP.Definitions {
57		enabled := server.Enabled
58		result.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)}
59		result.Credentials.MCP[name] = MCPSecretNames{BearerToken: server.SecretPresence.BearerToken, Headers: server.SecretPresence.Headers, Env: server.SecretPresence.Environment}
60	}
61	result.Capabilities = config.CapabilityPrefs{Global: map[string]map[string]bool{"mcp": {}}, Sessions: map[string]map[string]map[string]bool{}}
62	for name := range result.MCP {
63		if preference, ok := wire.Capabilities.Preferences["mcp:"+name]; ok {
64			result.Capabilities.Global["mcp"][name] = preference
65		}
66	}
67	result.UI = UIPreferences{DismissedNotices: wire.UI.DismissedNotices, Thinking: wire.UI.Thinking, Tools: wire.UI.Tools, ETag: wire.GroupResources.UI.ETag}
68	for _, etag := range result.ETags {
69		if etag == "" {
70			return Settings{}, fieldError("settings group validator")
71		}
72	}
73	return result, nil
74}
75func sourceValues(values map[string]protocol.ValueSource) map[string]map[string]string {
76	result := map[string]map[string]string{}
77	for name, value := range values {
78		result[name] = map[string]string{"source": value.Source, "value": value.Value}
79	}
80	return result
81}
82func ProviderProfiles(ctx context.Context, conn *Connection) (config.Profiles, error) {
83	settings, err := GetSettings(ctx, conn)
84	return settings.Profiles, err
85}
86
87func patchSettingsGroup(ctx context.Context, conn *Connection, group, etag string, body any, target any) error {
88	headers, err := observedHeaders(etag)
89	if err != nil {
90		return err
91	}
92	return executeMutation(ctx, conn, operation{Name: "edit " + group + " settings", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
93		return 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 {
95		if err := decodeRequired(data, target); err != nil {
96			return err
97		}
98		var savedGroup, savedURL, savedETag string
99		switch change := target.(type) {
100		case *protocol.ProviderSettingsChange:
101			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
102		case *protocol.CapabilitySettingsChange:
103			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
104		case *protocol.MCPSettingsChange:
105			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
106		case *protocol.ExtensionSettingsChange:
107			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
108		case *protocol.ModelSettingsChange:
109			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
110		case *protocol.UISettingsChange:
111			savedGroup, savedURL, savedETag = change.Group, change.Resource.URL, change.Resource.ETag
112		default:
113			return fieldError("settings group")
114		}
115		request, err := protocol.NewGetSettingsRequest("", &protocol.GetSettingsParams{Group: &group})
116		if err != nil {
117			return err
118		}
119		if savedGroup != group || savedETag == "" || savedURL != request.URL.RequestURI() {
120			return fieldError("settings resource")
121		}
122		return nil
123	})
124}
125func SaveProvider(ctx context.Context, conn *Connection, name string, profile config.Settings, etag string) error {
126	return saveProvider(ctx, conn, name, profile, etag, false)
127}
128func SaveAndSelectProvider(ctx context.Context, conn *Connection, name string, profile config.Settings, etag string) error {
129	return 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.
134	profilePatch := 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	}
140	if profile.APIKey != "" {
141		profilePatch["api_key"] = profile.APIKey
142	}
143	body := map[string]any{"profiles": map[string]any{name: profilePatch}}
144	if selectDefault {
145		body["default_profile"] = name
146	}
147	var change protocol.ProviderSettingsChange
148	err := patchSettingsGroup(ctx, conn, "providers", etag, body, &change)
149	return err
150}
151func DeleteProvider(ctx context.Context, conn *Connection, name string, profiles config.Profiles) error {
152	body := map[string]any{"profiles": map[string]any{name: nil}}
153	if profiles.Active == name {
154		body["default_profile"] = nil
155	}
156	var change protocol.ProviderSettingsChange
157	err := patchSettingsGroup(ctx, conn, "providers", profiles.ETag, body, &change)
158	return err
159}
160
161type CapabilitySelectionRequest struct {
162	Kind, Name, Scope, ETag, Revision string
163	Enabled                           *bool
164}
165
166func SetCapability(ctx context.Context, conn *Connection, session string, request CapabilitySelectionRequest) (ReloadResult, error) {
167	return 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) {
170	if scope == "global" {
171		var change protocol.CapabilitySettingsChange
172		err := patchSettingsGroup(ctx, conn, "capabilities", etag, protocol.CapabilitySettingsPatch{
173			CatalogSessionID: session, CatalogRevision: revision, Choices: map[string]*bool{id: enabled},
174		}, &change)
175		return ReloadResult{Message: "Saved the global default; reload open sessions to apply it."}, err
176	}
177	body := map[string]any{"selection": map[string]any{kind: map[string]*bool{id: enabled}}}
178	if kind == "skills" || kind == "instructions" {
179		body["catalog_revision"] = revision
180	}
181	_, err := patchSession(ctx, conn, session, etag, body)
182	return ReloadResult{Message: "Saved the session selection; reload the session to apply it."}, err
183}
184
185type MCPUpdateRequest struct {
186	Name, ETag    string
187	Server        *config.MCPServer
188	Secrets       MCPSecretsPatch
189	StoredSecrets MCPSecretNames
190}
191
192func SaveMCP(ctx context.Context, conn *Connection, session string, request MCPUpdateRequest) (ReloadResult, error) {
193	var definition any
194	if request.Server != nil {
195		server := request.Server
196		secrets := request.Secrets
197		if secrets.ClearHeaders {
198			secrets.ClearHeaders = false
199			secrets.Headers = map[string]*string{}
200			for _, name := range request.StoredSecrets.Headers {
201				secrets.Headers[name] = nil
202			}
203		}
204		if secrets.ClearEnv {
205			secrets.ClearEnv = false
206			secrets.Env = map[string]*string{}
207			for _, name := range request.StoredSecrets.Env {
208				secrets.Env[name] = nil
209			}
210		}
211		// These nulls clear fields when changing transports; nil optional
212		// pointers in MCPDefinitionPatch would omit them.
213		patch := 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		}
222		if server.StartupTimeoutMs != 0 {
223			patch["startup_timeout_ms"] = server.StartupTimeoutMs
224		}
225		if server.CallTimeoutMs != 0 {
226			patch["call_timeout_ms"] = server.CallTimeoutMs
227		}
228		definition = patch
229	}
230	var change protocol.MCPSettingsChange
231	err := patchSettingsGroup(ctx, conn, "mcp", request.ETag, map[string]any{
232		"definitions": map[string]any{request.Name: definition},
233	}, &change)
234	return applicationResult(change.Application), err
235}
236func nonNil[T any](s []T) []T {
237	if s == nil {
238		return []T{}
239	}
240	return s
241}
242func nonNilMap[K comparable, V any](m map[K]V) map[K]V {
243	if m == nil {
244		return map[K]V{}
245	}
246	return m
247}
248func applicationResult(application protocol.SettingsApplication) ReloadResult {
249	result := ReloadResult{Message: "Saved settings."}
250	if application.CompositionChanged {
251		result.Message = "Saved settings; reload open sessions to apply them."
252	}
253	for _, warning := range application.Warnings {
254		if result.Warning != "" {
255			result.Warning += "; "
256		}
257		result.Warning += warning.Detail
258	}
259	return result
260}
261func PatchUI(ctx context.Context, conn *Connection, patch UIPreferencesPatch) (UIPreferences, error) {
262	if patch.Pinned != nil || patch.Archived != nil {
263		return UIPreferences{}, errors.New("pin and archive are session preferences")
264	}
265	var change protocol.UISettingsChange
266	err := patchSettingsGroup(ctx, conn, "ui", patch.ETag, patch, &change)
267	return 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) {
273	if patch.Thinking != nil || patch.Tools != nil {
274		return UIPreferences{}, errors.New("thinking and tools are shared preferences")
275	}
276	if patch.ETag == "" {
277		current, err := GetSessionConfiguration(ctx, conn, session)
278		if err != nil {
279			return UIPreferences{}, err
280		}
281		patch.ETag = current.ETag
282	}
283	body := protocol.SessionPatch{Preferences: &protocol.PreferencePatch{Pinned: patch.Pinned, Archived: patch.Archived}}
284	updated, err := patchSession(ctx, conn, session, patch.ETag, body)
285	prefs := UIPreferences{SessionETags: map[string]string{session: updated.ETag}}
286	if updated.Pinned {
287		prefs.Pinned = []string{session}
288	}
289	if updated.Archived {
290		prefs.Archived = []string{session}
291	}
292	return prefs, err
293}
294func RecordOpen(ctx context.Context, conn *Connection, session string) (UIPreferences, error) {
295	id, err := operationID()
296	if err != nil {
297		return UIPreferences{}, err
298	}
299	var visit protocol.Visit
300	err = executeMutation(ctx, conn, operation{Name: "record session visit", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
301		return protocol.NewRecordVisitRequestWithBody(base, session, id, "application/json", body)
302	}, Body: struct{}{}, Policy: noRecovery}, []int{201, 200}, func(data []byte, _ int) error {
303		if err := decodeRequired(data, &visit); err != nil {
304			return err
305		}
306		if visit.VisitID != id || visit.SessionID != session {
307			return fieldError("visit identity")
308		}
309		return nil
310	})
311	return UIPreferences{Opens: map[string]int{session: int(visit.Opens)}}, err
312}