12 KiB
phase, fixed_at, review_path, iteration, findings_in_scope, fixed, skipped, status
| phase | fixed_at | review_path | iteration | findings_in_scope | fixed | skipped | status |
|---|---|---|---|---|---|---|---|
| 10.1-runtime-admin-extension-point | 2026-09-29T08:10:00Z | .planning/phases/10.1-runtime-admin-extension-point/10.1-REVIEW.md | 1 | 7 | 7 | 0 | all_fixed |
Phase 10.1: Code Review Fix Report
Fixed at: 2026-09-29T08:10:00Z Source review: .planning/phases/10.1-runtime-admin-extension-point/10.1-REVIEW.md Iteration: 1
Summary:
- Findings in scope: 7 (CR-01, WR-01 to WR-06; the Info findings were out of scope)
- Fixed: 7
- Skipped: 0
All fixes are in the framework repo (summercms.go). The application repo (fonoteka.go) needed no change: the Albums view model passes the deeper WR-03 guard and the Albums widget declares no context, so WR-06 does not affect it. Each fix has a regression test, and each test was confirmed to fail against the pre-fix code.
Fixed Issues
CR-01: A fractional, exponent or overflowing number for an integer column is a 500, not a validation error
Status: fixed
Files modified: modules/lagoon/fill.go, modules/lagoon/fill_test.go, modules/lagoon/README.md, modules/cabana/crud.go, modules/cabana/crud_test.go, modules/cabana/README.md
Commit: e60e697
Applied fix: lagoon.Fill now returns a new exported *lagoon.FillTypeError{Key, Err} (with Unwrap) when setField cannot store a value. The error text is unchanged: lagoon: fill <key>: <err>. CRUDService.save maps it to a ValidationError on that field, "The <key> field has an invalid value.", which is a 422 validation_failed. Every other Fill failure, such as a model that is not a struct pointer, is still a CapabilityError (500).
I left the settings path unchanged. It still answers a Fill failure with a 422 on body, as it did before this fix.
Tests:
TestFillTypeErrorNamesTheKeycovers a fraction,1e21, a bool into a string field and a string into a float field, and checks that a non-pointer model stays a plain error.TestCRUDFillTypeIsValidationruns against real Postgres. Create withyearset to1977.5,1e21,99999999999999999999and"1977"is a 422 ondetails.yearand nothing is persisted.{"name": true}is a 422 onname. Update with1977.5is a 422 and the stored year is unchanged.
WR-02: Plugin stylesheets stay enabled on views that are not controller views
Status: fixed: requires human verification (see the note under WR-01)
Files modified: admin/src/app/router.ts, admin/src/app/pluginAssets.ts (comment), admin/tests/app/router.test.ts, modules/boardwalk/dist/* (rebuilt)
Commit: 849a9ff
Applied fix: A router.afterEach hook runs after every successful navigation and calls activateStyles(routeControllerId(to)). The new exported routeControllerId returns the controller id for the list, create and record routes and '' for every other route. So the following screens disable every plugin link:
- settings, login and not found;
- a controller whose schema is still loading or failed to load.
Tests: in router.test.ts:
routeControllerIdfor every named route;- navigating to
/settings,/settings/mail,/nowhereand to a controller with no styles disables every link; - a controller route enables only its own links.
WR-01: A late schema response re-activates the previous controller's stylesheets over the current view
Status: fixed: requires human verification
Files modified: admin/src/app/pluginAssets.ts, admin/tests/app/pluginAssets.test.ts, admin/tests/smoke/extension.smoke.test.ts, modules/boardwalk/dist/* (rebuilt)
Commit: 5bbb0ad
Applied fix: I used the review's alternative: the route decides, not the caller.
activateStylesis now called only by the router. It records the active controller.loadStylescreates each new link withdisabledset unless its owner is the active controller.loadControllerAssetsno longer activates anything.
A schema response that arrives after its view was left therefore adds its links disabled and leaves the current controller's links alone. No unmount guard is needed in ListView or FormView.
Tests:
- A unit test in
pluginAssets.test.tscovers a late response and links added while nothing is active. - A full-app smoke test opens list A with a deferred schema, navigates to B, then releases A's schema. B's link stays enabled and A's link is created disabled.
Needs a browser check: happy-dom only reflects the disabled attribute. Please confirm in a real browser that a <link rel="stylesheet"> inserted with disabled set loads and applies once the router enables it. That is the spec'd behaviour (such a link counts as "explicitly enabled").
WR-03: The partial view-model guard (T-10.1-09) is shallow and easy to bypass
Status: fixed
Files modified: modules/cabana/partial_render.go, modules/cabana/phase101_render_test.go, modules/cabana/README.md, modules/pact/capabilities.go (doc comment)
Commit: 7333f45
Applied fix: refusedViewModel is now a two-part walk:
- Type walk (memoised): it follows pointers, slices, arrays, map keys and values, all struct fields, and the result types of every exported method on both
Tand*T, whatever the method's arguments. - Value walk: it resolves interface-typed members such as
map[string]any,[]anyandanyfields. It skips pointer, map and slice cycles and stops after 10,000 values; a view model larger than that is refused.
It refuses the following anywhere in that structure:
- the controller's model;
- any other GORM model: a struct with
TableName(), anygorm:struct tag,gorm.Modelorgorm.DeletedAt; - html/template trusted types.
Tests: the view-model guard subtest gains 14 refused cases:
- the model wrapped in a struct;
- the model inside
map[string]any,[]anyor ananyfield; template.HTMLinsidemap[string]any;*BackendUser, a gorm-tagged struct, embeddedgorm.Model,gorm.DeletedAt;- methods returning
template.HTML(value receiver, pointer receiver, with an argument) and a method returning the model.
It also gains 7 accepted cases, which keep today's curated view models working:
- the Albums-shaped stats struct;
- a curated
map[string]any; - a
time.Timefield; - a self-referencing struct, a cyclic map, a plain method, a nil pointer field.
The fonoteka Albums tests pass unchanged.
Not done:
- A method that returns an interface is not called, so its run-time result is not checked. The README says so.
- I did not apply the optional suggestion to restrict partial
classvalues to asummer-prefix. It changes the sanitizer contract on both server and client, and the fonoteka_stats.htmmarkup, so it is wider than this finding.
WR-04: isJSONScalar trusts reflect.Kind, not the JSON the value encodes to
Status: fixed
Files modified: modules/cabana/actions.go, modules/cabana/contracts.go, modules/cabana/crud_test.go, modules/cabana/phase101_actions_test.go, modules/cabana/README.md
Commit: 0bdb6eb
Applied fix: isJSONScalar now marshals the value. It rejects a marshal error and any encoding that starts with { or [. writeJSON encodes into a buffer before WriteHeader. If encoding fails, it logs the error and writes a 500 with the generic error envelope (Server error). This applies to every WriteData, WriteError and WriteErrorDetails response.
Tests:
TestIsJSONScalarcovers the accepted scalars and the rejected cases: NaN, infinity, slice, map, struct, a named string whoseMarshalJSONreturns an array, and a func.TestWriteJSONEncodeFailurechecks that an infinity in the body is a 500 with the generic envelope and that a normal body is byte-identical to before.- A
TestPhase101Actionssubtest has an action return{name: <array-marshalling string>, active: NaN}. The response is 200 withfill: {}.
WR-05: Form schema shows widgets the admin is not allowed to run
Status: fixed
Files modified: modules/cabana/http.go, modules/cabana/phase101_actions_test.go, modules/cabana/README.md
Commit: 7b72bf4
Applied fix: formSchema drops each type: widget field whose action is not registered or whose Permissions the principal fails. This is the same filtering listSchema applies to toolbarActions. The result is a new slice, so the cached compiled schema is never modified.
Tests: in the TestPhase101Actions permission subtest:
- the
limitedadmin, who has noacme.demo.run, gets a form schema with no widget and every other field; - the full admin still gets
lookup:lookup.
WR-06: The widget action route ignores the field's context
Status: fixed
Files modified: modules/cabana/actions.go, modules/cabana/phase101_render_test.go, modules/cabana/README.md
Commit: 719ed71
Applied fix: widgetAction works out the form from the request: create when there is no record_id, update when there is one. It answers 404 not_found when contextAllows(cc, field.Name, op) refuses. The check runs before any record lookup, so the action never runs.
Tests: TestPhase101WidgetContext covers five cases:
context |
Request | Expected |
|---|---|---|
update |
no record_id |
404 |
[update, preview] |
no record_id |
404 |
create |
with record_id |
404 |
create |
no record_id |
200 |
| none | no record_id |
200 |
It also asserts that the action runs only in the 200 cases.
Security review impact (10.1-SECURITY-REVIEW.md not edited)
-
T-10.1-09 (partial view models). The review's mitigation text says
refusedViewModelrefuses the controller's model type and pointers or collections of it. After WR-03 the guard is broader. It refuses the controller's model or any GORM model nested anywhere in the view model, including behind interfaces. It also refuses trusted html/template types reached through fields or method results. Two limits remain:- a method that returns an interface is not checked;
- the
classvalues of partial nodes are not restricted to a prefix.
TestPhase101PartialSanitizer/view-model_guardstays the evidence test, and removal check RC-11 still applies. -
T-10.1-16 (plugin CSS isolation). After WR-01 and WR-02, the route decides which stylesheets are enabled, not the view. Plugin CSS is enabled only while the router shows its controller's list, create or record route, and disabled on every other screen. This includes the time before a controller's schema arrives and after it fails to load. The residual-risk wording "while its controller is open" now matches the behaviour. Evidence:
tests/app/router.test.ts("plugin stylesheets follow the route") andtests/smoke/extension.smoke.test.ts(the late-response test).
Verification
All gates ran in the main checkout. workflow.use_worktrees is false, so no worktree was created, and every result below can be reproduced from the current tree.
| Check | Result |
|---|---|
summercms.go: go vet ./... && go test ./... |
pass (all packages ok) |
fonoteka.go: go vet and go test over ./..., ./plugins/golem15/user/..., ./plugins/golem15/fonoteka/... |
pass (all packages ok, parity included) |
npm --prefix admin run typecheck |
pass |
npm --prefix admin test |
pass: 53 files, 681 tests (677 before these fixes) |
scripts/check-admin-dist.sh |
pass after each SPA commit; each of 849a9ff and 5bbb0ad commits its own rebuilt modules/boardwalk/dist |
scripts/check-phase10.1.sh --all |
pass: self-test, go, security, postgres, spa, openapi, dist, hygiene (phase10 and phase10.1), evidence files, evidence; "phase10.1 all passed" |
No OpenAPI or response shapes changed. check-admin-openapi passed inside the gate. Every identifier added to a README exists: I checked lagoon.FillTypeError with go doc.
Fixed: 2026-09-29T08:10:00Z Fixer: Claude (gsd-code-fixer) Iteration: 1