22 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, app_repo_head_before, app_repo_head_after, plugin_repo_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | plan_head_before | plan_head_after | app_repo_head_before | app_repo_head_after | plugin_repo_head_after | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-domain-jobs-and-external-integrations | 04 | ai |
|
|
|
|
|
f519261da6 |
7c2c43359f |
28349433d77ba16c0dec7c87e41ef486996169f2 | eea0b1607ef9b5d14b3a5186bf322e6eb45a4b9b | c93e2d9 |
|
|
|
|
|
|
69min | 2026-10-03 | complete |
Phase 14 Plan 04: sm-golem-plugin and AI cover recognition Summary
A new shared plugin, sm-golem-plugin, brings the Golem AI layer to Go: SDK-free OpenAI-compatible and Anthropic adapters through the guarded client, the operator's AI models with an admin screen and a one-time importer, and the exact SSRF guard. On top of it a collector can test an AI key and photograph albums in the Nuxt app or through MCP, and gets PHP's answers, recorded from PHP and replayed offline.
Performance
- Duration: 69 min
- Started: 2026-10-03T20:27:58Z
- Completed: 2026-10-03T21:37:08Z
- Tasks: 4
- Commits: 3 in sm-golem-plugin (pushed), 8 in fonoteka.go, 1 code commit in summercms.go (plus this summary)
- Files: 34 in the plugin (+4505), 105 in fonoteka.go (+2664/-176), 5 in summercms.go (+55)
Accomplishments
- sm-golem-plugin (D-01). The empty remote was cloned at
plugins/golem15/golem, built, committed and pushed (masteratc93e2d9, in sync with origin), then registered withgit submodule add. fonoteka.go loadsgolem15.golembeforegolem15.fonoteka:go.work, bothgo.modfiles,summer.yaml, the regeneratedplugins.gen.goandapp.PluginIDsname it.golem15.fonotekarequires it, and every test activation list names it. - AI layer (D-02, D-04).
OpenAIAdapterandAnthropicAdapterare hand-written JSON mappings, with no SDK. The OpenAI adapter mapsmax_completion_tokens,response_formatandreasoning_effortas PHP does. The Anthropic adapter sets themax_tokenschain, builds image and document blocks and maps usage.AIServicecovers every PHP call except faces and chat, with PHP's messages.- Requests go through
fetchguard:TrustedModefor operator models andPublicOnlyModefor everyone else. They carry PHP curl'sAccept: */*and no User-Agent.
- Models and admin (D-03, D-18).
golem15_golem_modelskeeps an encrypted, write-onlyapi_keyand has a cabana list and form undergolem15.golem.access_settings. Its lookups keep PHP's default-model quirk.golem:import-settingscopies thegolem_settingsrepeater once and encrypts each key. - SSRF (D-05, D-20).
security.AssertSafeURLports SSRFGuard: https only, the allowlist (overridable withGOLEM15_SSRF_ALLOWED_HOSTS), and the resolve-time check through the newfetchguard.IsPrivateAddr. User and orgbase_urloverrides are checked when they are resolved. A refused URL answers Winter's 500 page. - AI tier (D-03).
AIConfig.Trustedis set only by the admin seam, which Boot fills from the golem vision model. A normal user never gets the admin model, and an org-locked member never falls back to it. - Routes (INTG-02, D-07).
POST ai-credential/test(JWT) tests an inline key or the caller's own stored key.POST albums/recognizeruns on the JWT group and on the token group (inv.scope:ai). The checks run in PHP's order: gate, limiter (10 per 60 s), validation, image guard, then the AI tier. It answers 200 with the albums, 200 withrecognition_truncated, or 502.RecognizeAlbumssends PHP's system prompt verbatim, with the collection's genre hint and the language directive. It retries once, then normalises the answer as PHP does.
- Parity. 20 cases were recorded against PHP through
summer parity:upstream, 12 of them with sidecars, covering both the OpenAI and the Anthropic exchanges. The cases include the admin tier through the global vision model, truncation onlengthand onmax_tokens, an empty answer, a provider error, a JPEG polyglot refused before any upstream call, and the unsafe and non-allowlisted base URLs (500 page, no upstream). The corpus has 168 routes ported and passing and 3 pending (the D-09 routes).check_corpus --require-recorded --check-secretsis green. A mutation of one word in the prompt failed both recognize routes, which shows the upstream body is asserted.
Task Commits
sm-golem-plugin (git@git.golem15.com:golem15/sm-golem-plugin.git, master, pushed):
cd3f0f6feat: golem15.golem plugin with the OpenAI-compatible adapter and AIService.Send (Task 1)da9fcdefeat: security.AssertSafeURL ports the SSRFGuard (Task 2)c93e2d9feat: AI models admin, settings import, Anthropic adapter and the rest of AIService (Task 2)
fonoteka.go (not pushed):
91b3f72chore(14-04): mount sm-golem-plugin at plugins/golem15/golem (.gitmodules+ gitlink only)bf90131feat(14-04): the application loads golem15.golem before golem15.fonoteka (Task 1)cf18030fix(14-04): check_corpus header scans stay on one line (Task 1 blocker)bbb641bfeat(14-04): a collector tests an AI key and gets PHP's answer through golem15.golem (Task 1)cce718atest(14-04): fonoteka's migration, schema and admin menu checks ignore golem15.golem's own (Task 2)8ec5e34chore(14-04): bump sm-golem-plugin (gitlink only, Task 2)72b6edcfeat(14-04): the AI tier is chosen as PHP chooses it and unsafe base URLs get PHP's 500 page (Task 3)eea0b16feat(14-04): a collector photographs albums in the Nuxt app or through MCP and gets PHP's recognised album list (Task 4)
summercms.go:
7c2c433feat(14-04): fetchguard.IsPrivateAddr exposes the dial guard's address classification (README and docs updated; TestDocsTree and docs:build --check pass)
Tracer gate: after Task 1 the <verify> was re-run end to end (vet, the named tests with -race, the corpus, check_corpus, submodule status) and passed. The expansion tasks then went ahead.
Deviations from Plan
Auto-fixed issues
1. [Rule 3 - Blocking] fetchguard.IsPrivateAddr exported (framework)
- Issue: The plan's guard must use "fetchguard's classification", but
isReservedOrPrivatewas unexported, and a plugin cannot reach it. - Fix: Exported
IsPrivateAddr, with a test, an example, README anddocs/services/outbound-http.md. - Commit:
7c2c433(summercms.go)
2. [Rule 1 - Bug] check_corpus flagged masked credential headers
- Issue: Once its
{{secret:ai-key}}placeholder is stripped,Authorization: Bearerran on through\s+into the next sorted header (Content-Type:). The same would happen to an unquotedX-Api-Key. - Fix: The patterns stay on one line (
[ \t]). Two clean vectors were added to the test. - Commit: cf18030
3. [Rule 3 - Blocking] The parity mismatch probe used ai-credential/test
- Fix:
assertPortedMismatchnow probesGET oauth-identities, a D-09 route that stays pending. - Commit: bbb641b
4. [Rule 3 - Blocking] Tests that pinned the plugin list
- Issue:
activateAppPluginsasserted two plugins. Once golem has migrations, a menu and a table, the migration-status indexes, the history-table list, the schema snapshot and the admin navigation test would all change. - Fix: The activation-order check came first (in bf90131). The other checks were then made bump-neutral in cce718a:
withoutGolem, the history-table filter, anallowedDiffsentry andfonotekaCodes. As a result the pointer bump8ec5e34changes only the gitlink and stays green, as the acceptance criterion requires.
5. [Rule 1 - Own omission] The part-file audit (T-12-17) rejected spoofed.jpg
- Fix: The audit allows the deliberate polyglot (it must not be an image). The fix was folded into eea0b16 before anything was pushed, so every commit is green.
6. [Rule 2 - Correctness] Parity recording environment
php_parity.shexportsGOLEM15_SSRF_ALLOWED_HOSTS=(empty), so PHP uses the plugin default whatever the checkout's.envsays.fonoteka_reset.phpforgets thegolem_settingsquery cache beforeSettings::set. Otherwise Winter saves onto the deleted row (the firstadmin-truncatedrecording answered 403 because of this).
Plan statements that conflict with PHP (parity kept)
7. ai-credential/test tests the caller's own stored credential (PHP UserAiConfig::fromCredential), not "the stored user or org credential through ResolveAIConfig".
8. GenerateImage does not run AssertSafeURL on the returned URL, because PHP's generateImage does not either; its caller does. The README says so, and TestAIServiceFailures checks the returned URL through the guard.
Plan details refined
9. AdminVisionModel(ctx, db) and SetAdminVisionModel(func(ctx, db)) replace the db-less signature. The seam is stateless and serves every app in the process, which is the 14-02 ReleaseFetcher precedent. Trusted is set by the seam itself.
10. The plan's aiModelConfig lives in classes as (*AIConfig).ModelConfig(), because the api package cannot import the plugin root. golem_wiring.go holds the vision-model source.
11. NewAIService(db func() *gorm.DB, cfg) and services.ForApp(app) resolve the database per call. ModelConfig also carries AcceptsImages, GeneratesImages and Position, which the lookups and the default-model quirk need. SendFileReader uploads from a reader, which is the Go form of PHP's sendFile(File).
12. The genre hint is GROUP BY name ORDER BY MAX(created_at) DESC LIMIT 20, because Postgres rejects PHP's DISTINCT with ORDER BY on an unselected column. The recorded prompts match.
13. No recorded 429 case for recognize. The Go replay keeps one app, and so one in-memory bucket, per route, so the JWT cases reach the limiter at most nine times. TestRecognizeRoutes asserts that the 11th call is 429.
14. An admin model with an empty base_url uses the provider's default. PHP would build "/messages" and fail to connect.
15. Extra tests beyond the plan: TestAdminModelsForm (write-only key through cabana's CRUD), TestPromptFactory, TestIsPrivateAddr and ExampleIsPrivateAddr.
16. Commits land on master in both repos, as the sequential-executor instructions and the earlier Phase 14 plans do (branching_strategy: none).
Total deviations: 16. Six are auto-fixes, two are plan conflicts resolved for parity, and eight are refinements. Impact: the only framework change is the small, documented fetchguard.IsPrivateAddr. There is no scope creep.
Issues Encountered
- The shell aliases
rmto its interactive form, and one merge step waited on its prompt. Later steps usecommand rm -f. - A
pkill -fwhose pattern matched its own command line killed the shell. The PHP server was then stopped by PID. - The recordings write nothing into the PHP checkout: the AI routes store no files, and
git statusthere shows only the user's own pre-existing files.
Verification
- sm-golem-plugin:
go vet ./...andgo test ./...pass, both standalone (GOWORK=off) and in the workspace. Every named test passes with-race. - fonoteka.go:
go vet ./...passes, andgo test ./... ./plugins/golem15/fonoteka/... ./plugins/golem15/golem/... -count=1passes in every package.go test ./... -shortpasses. - Parity:
TestParityCorpusreports 171/171 recorded, 168 passing, 0 failing and 3 pending.TestCheckCorpusPortedCaseStatus,TestUpstreamSidecarsAreReplayedandTestParityContractpass.check_corpus --require-recorded --check-secretspasses, with 0 pending case-status mismatches. - summercms.go:
go vet ./...andgo test ./... -count=1pass.TestDocsTreeandsummer docs:build --checkpass. - Acceptance greps for all four tasks pass.
expectedPortedRoutesis 168,phase14Absentis gone, andinv.scope:aisits on the recognize line. The pointer-bump commit touches onlyplugins/golem15/golem, and the plugin README names no consuming application.
Known Stubs
None.
Threat Flags
None. The new surfaces are in the plan's threat register: the AI models admin (T-14-24, permission golem15.golem.access_settings, write-only encrypted key), the user/org base URLs (T-14-22), trusted mode (T-14-23) and the recognize route (T-14-25..27). No key, token or DSN literal was pushed (T-14-29: the diff was scanned before each push; test keys are short fakes).
User Setup Required
None for development. At cutover (Phase 15) the operator runs golem:import-settings once and confirms whether production needs GOLEM15_SSRF_ALLOWED_HOSTS (for example .anthropic.com for user Anthropic base URLs). This is research Open Question 1, still open.
Next Phase Readiness
- 14-05 can follow the same submodule bootstrap for sm-feedback-plugin: empty clone at the mount path, push,
git submodule add, a gitlink-only commit, and bump-neutral app tests first. - 14-06 picks up the unit-test gate. INTG-02 is left Pending because 14-06 also declares it.
Phase: 14-domain-jobs-and-external-integrations Completed: 2026-10-03
Self-Check: PASSED
Every listed file exists; commits 7c2c433 (summercms.go), 91b3f72, bf90131, cf18030, bbb641b, cce718a, 8ec5e34, 72b6edc and eea0b16 (fonoteka.go) and cd3f0f6, da9fcde and c93e2d9 (sm-golem-plugin, on origin/master) are present.