Files
summercms/.planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md

16 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_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 tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
10.1-runtime-admin-extension-point 01 api
go
cabana
pact
boardwalk
html-template
x-net-html
openapi
swag
admin
phase provides
10-admin-vue-spa cabana admin API, requireAjax CSRF guard, boardwalk SPA serving, typed admin OpenAPI pipeline, conformance and inventory tests
phase provides
10.2-nest-framework-packages-under-modules-and-write-run-docs modules/ layout and module README rules
pact contracts AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult, HasAdminActions, AdminPartialData
fields.yaml type widget (widget, action, fill) and type partial (path); config_list.yaml headerPartial; registered toolbar.buttons names
POST {prefix}/api/v1/{vendor}/{plugin}/{controller}/widgets/{field} and .../toolbar/{action}
GET {prefix}/api/v1/{vendor}/{plugin}/{controller}/partials/{name} returning an allowlisted node tree
GET {prefix}/assets/{vendor}/{plugin}/{file...} exact-allowlist plugin asset route with SPA fall-through
list and form schema keys assets, toolbarActions, headerPartial and field keys widget, action, actionLabel, fill, path
exported boardwalk.ContentType and boardwalk.SetSecurityHeaders
typed OpenAPI operations and schemas (AdminActionRequest, AdminActionResult, ControllerAssets, ToolbarAction, PartialNode, PartialView)
10.1-02 SPA extension seams
10.1-03 Albums extension
10.1-04 unit tests and gate
14 Discogs
tokens tasks commits
39500 3 3
9b98d8409f 771d2ccce0
added patterns
golang.org/x/net/html (promoted from indirect to direct
no new module)
Cabana-owned action routes: plugins register Go Run functions; cabana owns the route, CSRF, permissions and record scope
Single action namespace: toolbar.buttons and widget action: both resolve through CompiledController.Actions; create and delete are reserved
Exact-key asset allowlist built at boot, miss falls through to the SPA handler
Server-sanitized partials: html/template Clone per request, ParseFragment, allowlist walk into JSON nodes with caps
created modified
modules/cabana/extension.go
modules/cabana/actions.go
modules/cabana/plugin_assets.go
modules/cabana/partial_render.go
modules/pact/capabilities.go
modules/pact/README.md
modules/boardwalk/boardwalk.go
modules/boardwalk/README.md
modules/cabana/form_schema.go
modules/cabana/list_schema.go
modules/cabana/schema_types.go
modules/cabana/contracts.go
modules/cabana/registry.go
modules/cabana/messages.go
modules/cabana/settings.go
modules/cabana/http.go
modules/cabana/admin_openapi.go
modules/cabana/README.md
modules/cabana/openapi_conformance_test.go
modules/cabana/security_coverage_test.go
admin/openapi/admin.json
admin/src/api/schema.d.ts
go.mod
../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go
../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go
An action Label containing :: is treated as a phrase key and must resolve at boot; other labels are literal button text
A toolbar action body must be {} exactly: record_id or values (even an empty values object) is a 422
The partial view-model guard also refuses types containing html/template trusted content types (template.HTML and siblings), enforcing the plan's no-raw-HTML prohibition
Partial and asset href/src URLs with whitespace or control characters are refused, since browsers strip them and could turn /<tab>/host into a protocol-relative URL
The empty-body POST is a 422 like any malformed body; the SPA sends {}
compileExtension runs after BindWritableFields and owns every controller-level extension check (actions, widgets, assets, partials)
Per-request locale for action messages and partial trans through towel.WithLocale(schemaLocale(...))
ADMIN-07
id description requirement verification human_judgment
D1 Widget field runs its registered action through POST .../widgets/{field}; the response fill holds only the declared fill keys ADMIN-07
kind ref status
integration modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/POST_/{vendor}/{plugin}/{controller}/widgets/{field} pass
kind ref status
unit modules/cabana/phase10_csrf_test.go#TestPhase10CSRF pass
kind ref status
unit modules/cabana/security_coverage_test.go#TestPhase09PermissionMatrix pass
false
id description requirement verification human_judgment
D2 Controller JS/CSS served at {prefix}/assets/{vendor}/{plugin}/... with explicit MIME, nosniff, CSP, CORP, no-cache and ETag; schema URLs carry ?v=; undeclared files fall through to the SPA 404 ADMIN-07
kind ref status
integration modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/GET_/{vendor}/{plugin}/{controller}/schema/list (assertPluginAsset) pass
kind ref status
integration ../fonoteka.go/plugins/golem15/fonoteka#TestPhase10TracerSPA pass
false
id description requirement verification human_judgment
D3 Registered toolbar actions: toolbar.buttons resolves create, delete and registered names; toolbarActions is permission-filtered; POST .../toolbar/{action} runs the action ADMIN-07
kind ref status
integration modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/POST_/{vendor}/{plugin}/{controller}/toolbar/{action} pass
kind ref status
unit modules/cabana/messages_test.go#TestPhase10Toolbar pass
kind ref status
unit modules/cabana/list_schema_test.go#TestListSchemaRejects pass
false
id description requirement verification human_judgment
D4 Header and form partials render through html/template into an allowlisted, capped node tree served by GET .../partials/{name} ADMIN-07
kind ref status
integration modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance/GET_/{vendor}/{plugin}/{controller}/partials/{name} pass
kind ref status
unit modules/cabana/form_schema_test.go#TestFormSchemaRejects pass
false
id description verification human_judgment rationale
D5 Sanitizer edge behaviour (dropped script/svg/on*/style/id, javascript: and // URLs, 64 KiB, 2000-node and depth-32 caps) and the model-type guard have dedicated unit tests
true Checked here only with a throwaway probe; the named sanitizer, asset and action unit tests (TestPhase101*) are plan 10.1-04's scope per CLAUDE.md rule 3
id description requirement verification human_judgment
D6 OpenAPI document and generated TypeScript types cover every new admin API route ADMIN-07
kind ref status
other scripts/check-admin-openapi.sh --check pass
kind ref status
unit modules/cabana/phase09_contract_test.go#TestPhase09ContractInventory pass
kind ref status
other npm --prefix admin run typecheck pass
false
25min 2026-09-28 complete

Phase 10.1 Plan 01: Runtime admin extension point (framework Go) Summary

cabana-owned widget, toolbar and partial routes backed by new pact contracts, an exact-allowlist plugin asset route under the admin prefix, and an html/template partial renderer whose output is sanitized with x/net/html into a capped JSON node tree, all typed in the admin OpenAPI document and proven on the acme conformance fixture

Performance

  • Duration: 25 min
  • Started: 2026-09-28T21:28:17Z
  • Completed: 2026-09-28T21:52:58Z
  • Tasks: 3
  • Files modified: 31 in summercms.go, 2 in fonoteka.go

Accomplishments

  • Six pact contracts (AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult, HasAdminActions, AdminPartialData) that plans 02 and 03 build against.
  • type: widget fields with boot checks: the tag must start with the plugin's {vendor}-{plugin}- prefix and must not be a reserved name, the action must be registered, fill keys must be writable scalar fields of the same form, and the controller must declare JS. POST .../widgets/{field} loads the record through FormExtendQuery without a row lock and filters fill keys on the server.
  • Registered toolbar actions share one namespace with widget actions, with create and delete reserved. toolbarActions in the list schema is filtered by permission, and POST .../toolbar/{action} accepts only {}.
  • Plugin JS/CSS is read and sha256-hashed at boot and served by exact key with boardwalk.SetSecurityHeaders, Cross-Origin-Resource-Policy: same-origin, no-cache and an ETag. List and form schemas carry assets URLs with ?v=. A lookup miss falls through to the SPA, so dist assets still load.
  • type: partial is supported and headerPartial is added. Templates are parsed at boot and cloned for each request. Output goes through x/net/html ParseFragment and a tag, attribute and URL allowlist, with caps of 64 KiB, 2000 nodes and depth 32. GET .../partials/{name} with an optional scoped ?id= serves the result.
  • OpenAPI regenerated with the recursive cabana.PartialNode. Route inventories were updated in both repositories, and the acme conformance fixture exercises widget, toolbar, both partials and both assets end to end.

Task Commits

summercms.go:

  1. Task 1: widget action tracer - f928194 (feat)
  2. Task 2: controller assets and toolbar actions - 8b1cb24 (feat)
  3. Task 3: header and form partials - 771d2cc (feat)

fonoteka.go (route inventory, separate repository):

  1. dcb64c9 test(10.1-01): expect the framework widget action route
  2. 75ab47f test(10.1-01): expect the framework toolbar action route
  3. be3fbf4 test(10.1-01): expect the framework partial route and new partial error

Files Created/Modified

  • modules/pact/capabilities.go: the six extension contracts
  • modules/cabana/extension.go: boot validation of actions, widgets, client assets and partials
  • modules/cabana/actions.go: widget and toolbar handlers, strict body decode, readScopedRecord, fill filter
  • modules/cabana/plugin_assets.go: exact-allowlist asset handler and schema URL builder
  • modules/cabana/partial_render.go: template parse and render, allowlist walk, caps, view-model guard, partial handler
  • modules/cabana/form_schema.go, list_schema.go, settings.go: widget, partial, headerPartial and toolbar YAML rules
  • modules/cabana/schema_types.go, contracts.go, registry.go, messages.go, http.go, admin_openapi.go: types, registry wiring, label checks, routes, annotations
  • modules/boardwalk/boardwalk.go: exported ContentType, SetSecurityHeaders
  • admin/openapi/admin.json, admin/src/api/schema.d.ts: regenerated
  • admin/tests/fixtures/*.json: assets and toolbarActions keys for the typed fixtures
  • READMEs of pact, cabana and boardwalk

Decisions Made

  • An action label containing :: is a phrase key and must resolve at boot. Any other label is literal text.
  • A toolbar action body must be exactly {}. A record_id or values key is a 422, so a toolbar action can never become a record lookup.
  • The partial view-model guard also refuses types that contain html/template's trusted content types (template.HTML and its siblings). This enforces the plan's "no raw HTML string marked safe" rule at the type level. The allowlist still backs it up for values stored behind any.
  • Any href or src containing whitespace or control characters is refused. Browsers strip those characters, so /<tab>/host would otherwise become a protocol-relative URL.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Renamed the new scalar helper to avoid a collision

  • Found during: Task 1
  • Issue: scalarValue already exists in query.go (it converts a filter jsonScalar), so the package did not compile.
  • Fix: Named the new predicate isJSONScalar.
  • Files modified: modules/cabana/actions.go
  • Committed in: f928194

2. [Rule 3 - Blocking] Updated the unsafe-route pins in TestPhase10CSRF and TestPhase10Coverage

  • Found during: Tasks 1 and 2
  • Issue: Both tests pin the exact set of mounted unsafe routes (9, besides login). The plan's inventory step named only phase09Routes and phase09ProtectedCalls.
  • Fix: Added each new POST to the pinned list and raised the count to 10, then 11. The CSRF walk still exercises every new route automatically.
  • Files modified: modules/cabana/phase10_csrf_test.go, modules/cabana/phase10_coverage_test.go
  • Committed in: f928194, 8b1cb24

3. [Rule 3 - Blocking] Updated the exact-JSON list schema expectations

  • Found during: Task 2
  • Issue: TestListSchemaCompile/Empty/Single/Filter compare whole ListSchema JSON, and toolbarActions and assets are always emitted.
  • Fix: Inserted "toolbarActions":[],"assets":{"scripts":[],"styles":[]} after toolbarButtons in the expected strings.
  • Files modified: modules/cabana/list_schema_test.go
  • Committed in: 8b1cb24

4. [Rule 3 - Blocking] Updated a fonoteka test that pinned the Phase 9 partial rejection text

  • Found during: Task 3 (full fonoteka suite)
  • Issue: TestCollectionsAdminRejectsPartial expected "type partial is not supported", which the plan removes.
  • Fix: It now expects "type partial needs a path". A bare legacy type: partial still fails boot, for the new reason.
  • Files modified: ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go
  • Committed in: be3fbf4 (fonoteka.go)

5. [Rule 2 - Missing critical] Extra URL and view-model hardening

  • Found during: Task 3
  • Issue: The plan's URL rule (one leading /) is bypassable with tab or newline characters. Its model-type guard does not cover template.HTML fields.
  • Fix: safePartialURL refuses whitespace and control characters. refusedViewModel refuses trusted template content types.
  • Files modified: modules/cabana/partial_render.go
  • Committed in: 771d2cc

Total deviations: 5 auto-fixed (4 blocking, 1 missing critical) Impact on plan: All were needed to keep pinned tests green or to close a security gap. No scope creep.

Issues Encountered

  • go.sum did not change; go mod tidy only moved golang.org/x/net to the direct block. go.sum is listed in the plan's files but needed no edit.
  • Task 1 and Task 2 ran the plan's targeted fonoteka tests. The full fonoteka suite ran at Task 3 and surfaced deviation 4, which was fixed in the same task.

Known Stubs

None. The fixture actions are test doubles by design, and the Discogs stubs belong to plan 10.1-03.

User Setup Required

None. No external service configuration required.

Next Phase Readiness

  • Plan 10.1-02 (SPA) can consume assets, toolbarActions, headerPartial, the widget field keys, and the PartialNode/PartialView/AdminActionResult schema aliases from schema.d.ts.
  • Plan 10.1-03 (Albums) implements AdminClientAssets, HasAdminActions and AdminPartialData on the albums controller.
  • Plan 10.1-04 owns the named unit tests (TestPhase101FormExtensionSchema, Actions, Toolbar, Assets, PartialSanitizer) and scripts/check-phase10.1.sh.
  • The README partial example uses the summer-stats class names. The admin SPA only styles them after plan 10.1-02.

Self-Check: PASSED

  • FOUND: modules/cabana/extension.go, actions.go, plugin_assets.go, partial_render.go, modules/pact/capabilities.go, admin/openapi/admin.json
  • FOUND commits: f928194, 8b1cb24, 771d2cc (summercms.go); dcb64c9, 75ab47f, be3fbf4 (fonoteka.go)
  • Plan verification: go vet ./... && go test ./... (summercms.go), go vet and go test ./plugins/golem15/fonoteka/... (fonoteka.go), scripts/check-admin-openapi.sh --check, npm --prefix admin run typecheck, scripts/check-phase10.sh --hygiene: all pass.

Phase: 10.1-runtime-admin-extension-point Completed: 2026-09-28