feat: expose safe pi management api

This commit is contained in:
2026-08-05 00:31:44 +02:00
parent 09eeea17e5
commit d6b4a08a02
10 changed files with 744 additions and 61 deletions
+63 -44
View File
@@ -28,6 +28,12 @@ type ModelOption struct {
ID string `json:"id"`
}
type piOptions struct {
Providers []string `json:"providers"`
Models []ModelOption `json:"models"`
Reasoning []string `json:"reasoning"`
}
type settingsFileSnapshot struct {
Exists bool `json:"exists"`
RawBase64 string `json:"rawBase64"`
@@ -46,19 +52,16 @@ func Configure(ctx context.Context, runner Runner, value Defaults) error {
if !choicePattern.MatchString(value.Provider) || !choicePattern.MatchString(value.Model) {
return errors.New("provider and model must be supported identifiers")
}
if value.Thinking != "low" && value.Thinking != "medium" && value.Thinking != "high" {
return errors.New("thinking must be low, medium, or high")
}
before, err := renderedCore(ctx, runner)
if err != nil {
return err
}
options, err := ConfigurationOptions(ctx, runner)
options, err := configurationOptions(ctx, runner)
if err != nil {
return err
}
found := false
for _, model := range options {
for _, model := range options.Models {
if model.Provider == value.Provider && model.ID == value.Model {
found = true
}
@@ -66,6 +69,15 @@ func Configure(ctx context.Context, runner Runner, value Defaults) error {
if !found {
return errors.New("provider/model is not in Pi options")
}
thinkingFound := false
for _, reasoning := range options.Reasoning {
if reasoning == value.Thinking {
thinkingFound = true
}
}
if !thinkingFound {
return errors.New("thinking is not in Pi options")
}
old, err := captureSettingsFile(ctx, runner)
if err != nil {
return err
@@ -107,24 +119,47 @@ func Configure(ctx context.Context, runner Runner, value Defaults) error {
}
func ConfigurationOptions(ctx context.Context, runner Runner) ([]ModelOption, error) {
args := append([]string{"exec", "-T", "core", "curl", "-fsS"}, internalIdentityHeaders...)
args = append(args, "http://127.0.0.1:8787/models")
models, err := runCompose(ctx, runner, args...)
options, err := configurationOptions(ctx, runner)
if err != nil {
return nil, commandError("Pi options check", models, err)
return nil, err
}
var payload struct {
Models []ModelOption `json:"models"`
return options.Models, nil
}
func configurationOptions(ctx context.Context, runner Runner) (piOptions, error) {
args := append([]string{"exec", "-T", "core", "curl", "-fsS"}, internalIdentityHeaders...)
args = append(args, "http://127.0.0.1:8787/pi-management/options")
result, err := runCompose(ctx, runner, args...)
if err != nil {
return piOptions{}, commandError("Pi options check", result, err)
}
if json.Unmarshal([]byte(models.Stdout), &payload) != nil || len(payload.Models) == 0 {
return nil, errors.New("Pi options response is invalid or empty")
var payload piOptions
if json.Unmarshal([]byte(result.Stdout), &payload) != nil || len(payload.Providers) == 0 || len(payload.Models) == 0 || len(payload.Reasoning) == 0 {
return piOptions{}, errors.New("Pi options response is invalid or empty")
}
for _, option := range payload.Models {
if !choicePattern.MatchString(option.Provider) || !choicePattern.MatchString(option.ID) {
return nil, errors.New("Pi options response contains an invalid provider/model")
providers := make(map[string]bool, len(payload.Providers))
for _, provider := range payload.Providers {
if !choicePattern.MatchString(provider) || providers[provider] {
return piOptions{}, errors.New("Pi options response contains an invalid provider")
}
providers[provider] = true
}
return payload.Models, nil
models := make(map[string]bool, len(payload.Models))
for _, option := range payload.Models {
key := option.Provider + "\x00" + option.ID
if !providers[option.Provider] || !choicePattern.MatchString(option.ID) || models[key] {
return piOptions{}, errors.New("Pi options response contains an invalid provider/model")
}
models[key] = true
}
reasoning := make(map[string]bool, len(payload.Reasoning))
for _, value := range payload.Reasoning {
if (value != "low" && value != "medium" && value != "high") || reasoning[value] {
return piOptions{}, errors.New("Pi options response contains an invalid reasoning choice")
}
reasoning[value] = true
}
return payload, nil
}
func writeDefaults(ctx context.Context, runner Runner, value Defaults) (compose.Result, error) {
@@ -253,41 +288,25 @@ func expectedVersions(ctx context.Context, runner Runner) (string, string, error
return expectedValue, labelValue, nil
}
// Test performs the pre-Task-8 composite smoke through core's private loopback endpoint.
// Test retains the direct image-version signal, then delegates all Pi configuration/provider smoke
// validation to core's dedicated, admin-only Pi Management endpoint.
func Test(ctx context.Context, runner Runner) error {
if _, err := Status(ctx, runner); err != nil {
return err
}
health, err := runCompose(ctx, runner, "exec", "-T", "core", "curl", "-fsS", "http://127.0.0.1:8787/health")
args := append([]string{"exec", "-T", "core", "curl", "-fsS", "-X", "POST"}, internalIdentityHeaders...)
args = append(args, "http://127.0.0.1:8787/pi-management/test")
smoke, err := runCompose(ctx, runner, args...)
if err != nil {
return commandError("Pi smoke check", health, err)
return commandError("Pi smoke check", smoke, err)
}
var healthPayload struct {
Status string `json:"status"`
var smokePayload struct {
Ready bool `json:"ready"`
}
if json.Unmarshal([]byte(health.Stdout), &healthPayload) != nil || healthPayload.Status != "ok" {
return errors.New("Pi smoke health response is not ready")
if json.Unmarshal([]byte(smoke.Stdout), &smokePayload) != nil || !smokePayload.Ready {
return errors.New("Pi smoke response is not ready")
}
models, err := ConfigurationOptions(ctx, runner)
if err != nil {
return err
}
settingsArgs := append([]string{"exec", "-T", "core", "curl", "-fsS"}, internalIdentityHeaders...)
settingsArgs = append(settingsArgs, "http://127.0.0.1:8787/settings")
settings, err := runCompose(ctx, runner, settingsArgs...)
if err != nil {
return commandError("Pi smoke settings check", settings, err)
}
var selected Defaults
if json.Unmarshal([]byte(settings.Stdout), &selected) != nil || !choicePattern.MatchString(selected.Provider) || !choicePattern.MatchString(selected.Model) || (selected.Thinking != "low" && selected.Thinking != "medium" && selected.Thinking != "high") {
return errors.New("Pi smoke settings response is incomplete")
}
for _, model := range models {
if model.Provider == selected.Provider && model.ID == selected.Model {
return nil
}
}
return errors.New("configured provider/model does not match an available Pi model entry")
return nil
}
func renderedCore(ctx context.Context, runner Runner) (Image, error) {