Files
sm-golem-plugin/README.md
Jakub Zych 93b2b05476 fix(14): WR-04 require the AI key again when the endpoint changes
An admin who cannot read the write-only key could point base_url at
their own host, leave the key redacted and have it sent there on the
next call. An update that keeps the stored key may no longer change the
adapter or base URL; changing either needs the key re-entered.
2026-10-04 08:08:07 +02:00

9.7 KiB
Raw Blame History

golem15.golem

The shared AI plugin for SummerCMS applications: provider adapters for the Anthropic Messages API and the OpenAI-compatible chat completions API, an AI service that sends prompts through the framework's guarded outbound client, the operator's AI models with their admin screen, and an SSRF guard for untrusted AI URLs.

import _ "git.golem15.com/golem15/sm-golem-plugin"

Module git.golem15.com/golem15/sm-golem-plugin, package golem, plugin ID golem15.golem.

Overview

The plugin is a compiled SummerCMS plugin ported from the WinterCMS Golem15.Golem plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go. Another plugin that sends AI requests lists golem15.golem in its Requires() and imports the classes/... packages.

The adapters are hand-written JSON mappings with no vendor SDK. Every request goes through a fetchguard.Client: a config marked Trusted (a model an operator configured) runs in fetchguard.TrustedMode, every other config (a key a user or an organisation supplied) in fetchguard.PublicOnlyMode, which refuses private and reserved addresses at dial time.

The plugin's config keys, lang namespace and plugin ID are a contract shared by every application that mounts it: change them only in a backwards-compatible way.

Features

  • Prompts. valueobjects.Prompt is an immutable request: a system prompt, user and assistant messages, messages with base64 images (AddUserMessageWithImage, AddUserMessageWithImages) or an uploaded file id (AddUserMessageWithFileID), a model override and extra options in insertion order. ToAPIPayload builds the OpenAI chat completions body as a valueobjects.Payload, a JSON object that keeps its key order.
  • Responses. valueobjects.AIResponse carries the content, token usage and the provider's decoded answer (Raw, which keeps finish_reason and stop_reason) or the failure message. Err holds the Go error behind a failure for logs and tests and is never shown to a user.
  • OpenAI-compatible adapter. providers.OpenAIAdapter posts to <base>/chat/completions with Authorization: Bearer, drops response_format, maps max_tokens to max_completion_tokens and adds reasoning_effort: low for o-series and gpt-5 models. It also serves compatible providers through their base URL. providers.ForName picks the adapter by name.
  • Anthropic adapter. providers.AnthropicAdapter posts to <base>/messages with x-api-key, anthropic-version: 2023-06-01 and anthropic-beta: files-api-2025-04-14. It sends max_tokens (the prompt's max_tokens, else max_completion_tokens, else 4096), rewrites image blocks to base64 or URL image sources and file blocks to document blocks, and maps Anthropic's usage to prompt_tokens, completion_tokens and total_tokens. Raw keeps stop_reason.
  • AI service. services.AIService.Send applies the model's system prompt when the prompt has none and posts the prompt to the config's chat endpoint; a nil config uses the default model. It returns the WinterCMS plugin's failure messages: no model configured, no API key, no connection, and an answer that is not JSON. The other calls are services.AIService.SendStream (server-sent events, one {"content":…} chunk per delta), services.AIService.SendFile, services.AIService.SendFilePath and services.AIService.SendFileReader (multipart upload to the files endpoint of a model with has_files_endpoint), services.AIService.SendWithModel, services.AIService.SendToImageModel, services.AIService.SendToVisionModel, services.AIService.GenerateImage (the images endpoint; check the returned URL with security.AssertSafeURL before fetching it) and services.AIService.Ask. services.ForApp returns the application's shared service.
  • AI models. Operators configure models in the admin under Golem › AI Models (controller golem15.golem.models, permission golem15.golem.access_settings). Each model has a name, an adapter (openai or anthropic), an API key, a base URL, a model id, a default system prompt, flags (enabled, default, generates images, accepts images, has a files endpoint), a completion token budget and an order. The key is encrypted at rest and write-only: the form shows it redacted, and an update that sends it back empty or redacted keeps the stored key, unless the same update changes the adapter or base URL: then the key must be entered again, so a kept key never follows a new endpoint. services.Models, services.DefaultModel, services.VisionModel, services.ImageModel, services.FileModel and services.ModelByName read them like the WinterCMS Settings model. Their configs are Trusted. services.DefaultModel keeps the WinterCMS quirk: without a model marked default it returns the first model only when that one is enabled.
  • Settings import. golem:import-settings copies the AI models of a WinterCMS installation (the models repeater of the system_settings row golem_settings, or a JSON file) into the table once, encrypting each plaintext key.
  • SSRF guard. security.AssertSafeURL ports the WinterCMS SSRFGuard: https only, a host on the allowlist (security.AllowedHosts), and a host that resolves to public addresses only (fetchguard.IsPrivateAddr). A failure is a security.UnsafeURLError with the WinterCMS message. Apply it to every base URL a user or an organisation supplies and to every URL a provider returns; the guarded client checks the address again when it connects.
  • Prompt factory. factories.PromptFactory builds the WinterCMS plugin's ready-made prompts: a form field suggestion, values for several fields, categories for an item, a chat and a completion.

Usage

Mount the plugin in an application as a git submodule at plugins/golem15/golem:

git submodule add git@git.golem15.com:golem15/sm-golem-plugin.git plugins/golem15/golem

Add it to the application's go.work and go.mod:

use ./plugins/golem15/golem

require git.golem15.com/golem15/sm-golem-plugin v0.0.0

replace git.golem15.com/golem15/sm-golem-plugin => ./plugins/golem15/golem

List it in summer.yaml before the plugins that require it and run summer build:

module: git.golem15.com/golem15/acme
binary: acme
plugins:
  - id: golem15.golem
    module: git.golem15.com/golem15/sm-golem-plugin
  - id: acme.blog
    module: git.golem15.com/golem15/acme/plugins/acme/blog

The plugin's own go.mod replaces the framework with ../../../../summercms.go, so it expects the framework checkout next to the application checkout.

A plugin sends a prompt with a user's own key:

svc := services.ForApp(app)
prompt := valueobjects.NewPrompt().
	WithSystem("You catalog physical items.").
	AddUserMessage("List the items.")
res := svc.Send(ctx, prompt, &services.ModelConfig{
	Adapter: "openai",
	APIKey:  key,
	Model:   "gpt-4o-mini",
})
if !res.Success {
	return errors.New(res.Error)
}

API reference

  • golem: Plugin, the golem15.golem plugin (config, lang, migrations, models, admin controllers, permissions, navigation, commands).
  • classes/valueobjects: Prompt, NewPrompt, Message, ContentPart, ImageURL, FileRef, Image, Payload, NewPayload, AIResponse, Success, Failure.
  • classes/providers: Adapter, OpenAIAdapter, AnthropicAdapter, ForName, OpenAIBaseURL, AnthropicBaseURL, AnthropicVersion, AnthropicBeta.
  • classes/services: ModelConfig, DefaultModelID, AIService, NewAIService, ForApp, ConfigTimeoutSeconds, DefaultTimeout, Models, DefaultModel, VisionModel, ImageModel, FileModel, ModelByName.
  • classes/security: AssertSafeURL, AllowedHosts, UnsafeURLError, Resolver, DefaultResolver, DefaultAllowedHosts, ConfigAllowedHosts, EnvAllowedHosts.
  • classes/factories: PromptFactory, Pair, FormField, Category, HumanizeFieldName.
  • models: AiModel (table golem15_golem_models), Register, All.
  • updates: the migrations (202610030101_create_golem_models), Register, All.
  • controllers: ModelsAdminController, AdminControllers, PermissionAccessSettings.
  • console: ImportSettings, ImportModels, IsNothingToImport, ErrModelsNotEmpty, SettingsItem.

Configuration

Defaults live in config/config.yaml and are merged under golem15.golem.*.

Key Default Meaning
golem15.golem.ssrf.allowed_hosts oaidalleapiprodscus.blob.core.windows.net, .openai.com Hosts an untrusted base URL or an AI-returned image URL may use. A leading dot matches any subdomain.
golem15.golem.ai.timeout_seconds 120 Timeout of every AI provider request.

The environment variable GOLEM15_SSRF_ALLOWED_HOSTS, a comma-separated host list, replaces golem15.golem.ssrf.allowed_hosts when it is set and not empty. With the default list, a user's Anthropic base URL is refused: add .anthropic.com (or the provider's host) to accept it.

CLI commands

Command Purpose
golem:import-settings [--file <json>] Copy the AI models of a WinterCMS golem_settings row (or of a JSON file holding the settings value or the models list) into golem15_golem_models, in order, with each key encrypted. It refuses to run when the table already has rows, and reports a missing system_settings table or row without failing.

Dependencies

  • The SummerCMS framework, git.golem15.com/golem15/summercms (modules backpack, bonfire, cabana, compass, fetchguard, lagoon, pact, party).
  • GORM and gormigrate, as pinned in go.mod.
  • testcontainers-go for the Postgres-backed tests.

Testing

go test ./...   # starts Postgres with testcontainers (needs Docker)