Skip to content

feat(robot-types): add config template API#81

Merged
shark0F0497 merged 1 commit into
mainfrom
keystone-worktree1-main
May 21, 2026
Merged

feat(robot-types): add config template API#81
shark0F0497 merged 1 commit into
mainfrom
keystone-worktree1-main

Conversation

@shark0F0497
Copy link
Copy Markdown
Collaborator

Pull Request Checklist

Please ensure your PR meets the following requirements:

  • Code follows the style guidelines
  • Tests pass locally
  • Code is formatted
  • Documentation updated if needed
  • Commit messages follow conventional commits
  • PR description is complete and clear

Summary

This PR adds database-backed robot type config templates in Keystone and exposes the canonical API for retrieving raw Axon config templates by robot_type_id and filename.


Motivation

  • Axon config lookup should use the canonical robot type identity rather than the older factory/model path shape.
  • Keystone needs to store the current active recorder.yaml and transfer.yaml templates per robot type.
  • Synapse needs admin APIs to upload, list, and delete these templates, while Axon-compatible public reads should not require JWT authentication.

Changes

Modified Files

  • [internal/server/server.go](internal/server/server.go) - Registers public config template GET routes and admin-only management routes.

Added Files

  • [internal/api/handlers/robot_type_config_template.go](internal/api/handlers/robot_type_config_template.go) - Adds public raw YAML retrieval plus admin list/get/upsert/delete handlers.
  • [internal/api/handlers/robot_type_config_template_test.go](internal/api/handlers/robot_type_config_template_test.go) - Covers successful public fetches, validation errors, fixed-slot listing, PUT validation, upsert, delete, and route registration.
  • [internal/storage/database/migrations/000003_robot_type_config_templates.up.sql](internal/storage/database/migrations/000003_robot_type_config_templates.up.sql) - Creates the robot_type_config_templates table with soft-delete-aware active uniqueness.
  • [internal/storage/database/migrations/000003_robot_type_config_templates.down.sql](internal/storage/database/migrations/000003_robot_type_config_templates.down.sql) - Drops the new table.
  • [docs/designs/robot-type-config-templates.html](docs/designs/robot-type-config-templates.html) - Documents the agreed API, validation rules, storage model, Synapse scope, and future Axon integration notes.

Deleted Files

  • None

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update (documentation changes only)
  • Refactoring (code improvement without functional changes)
  • Performance improvement (code changes that improve performance)
  • Test changes (adding, modifying, or removing tests)

Impact Analysis

Breaking Changes

None

Backward Compatibility

Fully backward compatible. This adds new endpoints and does not change existing robot type or device registration behavior.


Testing

Test Environment

  • Local development environment
  • Go test cache redirected to /tmp/go-build-cache

Test Cases

  • Unit tests pass locally
  • Integration tests pass locally
  • E2E tests pass (if applicable)
  • Manual testing completed

Manual Testing Steps

  • GOCACHE=/tmp/go-build-cache go test ./internal/api/handlers -run RobotTypeConfigTemplate -v
  • GOCACHE=/tmp/go-build-cache go test ./internal/server ./internal/api/handlers
  • GOCACHE=/tmp/go-build-cache go test ./...
  • swag init -g internal/server/server.go -o docs
  • Verified public template fetches through the Synapse dev proxy for robot_type_id=1:
    • GET /api/v1/robot_types/1/configs/recorder.yaml returned 200 with text/yaml; charset=utf-8
    • GET /api/v1/robot_types/1/configs/transfer.yaml returned 200 with text/yaml; charset=utf-8

Test Coverage

  • New tests added
  • Existing tests updated
  • Coverage maintained or improved

Screenshots / Recordings

Not applicable for Keystone backend changes.


Performance Impact

  • Memory usage: No change
  • CPU usage: No change
  • Throughput: No change
  • Lock contention: No change

Documentation


Related Issues

  • Related to the Axon registration/config-template flow design.

Additional Notes

  • The public raw template endpoint intentionally returns unrendered template text.
  • Only recorder.yaml and transfer.yaml are accepted in this first version.
  • Admin management endpoints require an admin JWT; the public raw template endpoint does not require JWT authentication.
  • Swagger was regenerated locally, but generated docs/docs.go, docs/swagger.json, and docs/swagger.yaml are ignored by this repository.

Reviewers

@reviewer1 @reviewer2


Notes for Reviewers

  • Please focus on the route split between unauthenticated public reads and admin-only management endpoints.
  • Please review the soft-delete-aware uniqueness strategy in the migration.
  • Please review the decision to return raw, unrendered YAML from the public endpoint.

Checklist for Reviewers

  • Code changes are correct and well-implemented
  • Tests are adequate and pass
  • Documentation is updated and accurate
  • No unintended side effects
  • Performance impact is acceptable
  • Backward compatibility maintained (if applicable)

@shark0F0497 shark0F0497 merged commit 933dc0b into main May 21, 2026
6 checks passed
@shark0F0497 shark0F0497 deleted the keystone-worktree1-main branch May 21, 2026 12:32
// @Summary Get robot type config
// @Description Returns an unrendered robot type config template by robot type and filename.
// @Tags robot_type_config_templates
// @Produce plain
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Swagger @Produce: plain annotation does not match actual response type

The handler at line 115 returns c.Data(http.StatusOK, "text/yaml; charset=utf-8", ...), but the gin-swagger annotation at this line declares @Produce: plain. For a YAML-specific response, update the annotation to @Produce: text/yaml so generated API docs accurately expose the response format to consumers.

FROM robot_type_config_templates
WHERE robot_type_id = ? AND filename = ? AND deleted_at IS NULL
LIMIT 1
`, robotTypeID, filename).Scan(&existingID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: Upsert has a time-of-check-to-time-of-use race under concurrent PUT requests

UpsertRobotTypeConfigTemplate runs a pre-check SELECT id FROM robot_type_config_templates WHERE robot_type_id=? AND filename=? AND deleted_at IS NULL LIMIT 1 at line 264, then decides between INSERT and UPDATE based on the SELECT result. When two concurrent PUTs arrive simultaneously, both goroutines can receive sql.ErrNoRows from that SELECT and both will enter the tx.Exec(INSERT INTO ...) branch, causing the second transaction to fail with a unique-constraint violation that is swallowed by the generic 500 Internal Server Error at lines 285-290 — callers see a hard error instead of last-write-wins.

Fix: replace the two-step (SELECT + INSERT/UPDATE) with a single INSERT INTO robot_type_config_templates (robot_type_id, filename, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE content=VALUES(content), updated_at=VALUES(updated_at). The stored generated column _active_unique is NULL for soft-deleted rows, so only the single active row (deleted_at IS NULL) participates in the uniqueness constraint — ON DUPLICATE KEY UPDATE will therefore fire correctly for the active row and never for a soft-deleted one.

Comment thread internal/server/server.go
s.robotType.RegisterRoutes(v1Tasks)
s.robotType.RegisterConfigTemplatePublicRoutes(v1Tasks)
adminRobotTypes := v1Routes.Group("", middleware.JWTAuth(&s.cfg.Auth), middleware.RequireRole("admin"))
s.robotType.RegisterConfigTemplateAdminRoutes(adminRobotTypes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider separating route registration for public and admin endpoints with a comment for reviewers

Line 259 registers RegisterConfigTemplatePublicRoutes under v1Tasks (no middleware), and line 261 registers RegisterConfigTemplateAdminRoutes under adminRobotTypes (JWT + admin role). The two registrations sit side-by-side here with no separating comment, which makes the intent and the middleware boundary easy to misread during a future change. The design and the function names are clear, so this is low urgency.

// @Summary Get robot type config
// @Description Returns an unrendered robot type config template by robot type and filename.
// @Tags robot_type_config_templates
// @Produce plain
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Swagger @Produce: plain annotation does not match actual response type

The handler at line 115 returns c.Data(http.StatusOK, "text/yaml; charset=utf-8", ...), but the gin-swagger annotation at this line declares @Produce: plain. For a YAML-specific response, update the annotation to @Produce: text/yaml so generated API docs accurately expose the response format to consumers.

FROM robot_type_config_templates
WHERE robot_type_id = ? AND filename = ? AND deleted_at IS NULL
LIMIT 1
`, robotTypeID, filename).Scan(&existingID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: Upsert has a TOCTOU race under concurrent PUT requests

UpsertRobotTypeConfigTemplate runs a pre-check SELECT id ... WHERE robot_type_id=? AND filename=? AND deleted_at IS NULL LIMIT 1 at line 264, then decides between INSERT and UPDATE based on the SELECT result. When two concurrent PUTs arrive simultaneously, both can see sql.ErrNoRows and both will attempt the INSERT INTO ... branch. The second hits the _active_unique unique-constraint violation and falls into the generic 500 / JS error branch at lines 285-290 — callers receive an opaque error instead of the expected last-write-wins result.

Fix: replace the two-step SELECT + INSERT/UPDATE with a single INSERT INTO robot_type_config_templates (...) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE content=VALUES(content), updated_at=VALUES(updated_at). The stored generated column _active_unique is NULL for soft-deleted rows, so the DUPLICATE KEY branch fires only for the single active row (deleted_at IS NULL), which is the exact behavior the current code intends.

Comment thread internal/server/server.go
s.robotType.RegisterRoutes(v1Tasks)
s.robotType.RegisterConfigTemplatePublicRoutes(v1Tasks)
adminRobotTypes := v1Routes.Group("", middleware.JWTAuth(&s.cfg.Auth), middleware.RequireRole("admin"))
s.robotType.RegisterConfigTemplateAdminRoutes(adminRobotTypes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Separate the public and admin route registrations with an explicit comment

Line 259 registers RegisterConfigTemplatePublicRoutes under v1Tasks (no middleware — deliberate design per the API spec) and line 261 registers RegisterConfigTemplateAdminRoutes under adminRobotTypes (JWT + admin). The adjacent lines differ in critical ways — one is unauthenticated by design, the other enforces admin auth — but there is no comment separating them. Future maintainers can easily miss the distinction when extending the handler.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 21, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Approve with changes

Overview

Severity Count
HIGH 1
SUGGESTION 2
Issue Details (click to expand)

HIGH

File Line Issue
internal/api/handlers/robot_type_config_template.go 264 Upsert has a TOCTOU race: concurrent PUTs both pass the pre-check SELECT and attempt INSERT, causing the second to fail with a generic 500 instead of a correct last-write-wins result

SUGGESTION

File Line Issue
internal/api/handlers/robot_type_config_template.go 86 Swagger @Produce: plain does not match the actual text/yaml; charset=utf-8 response produced by the handler
internal/server/server.go 261 Public (no-auth) and admin (JWT + admin) config-template route registrations sit adjacent without a separating comment, making the deliberate middleware boundary easy to miss in a future change
Other Observations (not in diff, cannot receive inline comments)

Test coverage gaps

File Line Issue
internal/api/handlers/robot_type_config_template_test.go TestRobotTypeConfigTemplateUpsertAndDelete does not cover concurrent-PUT IO/TOCTOU behavior; add a table-driven concurrent test that fires two PUTs in goroutines and asserts the final active record reflects one winner without an error
internal/api/handlers/robot_type_config_template_test.go TestRobotTypeConfigTemplateRoutesDoNotConflictWithRobotTypeRoutes only checks ServeHTTP does not panic; it does not assert that a request for /robot_types/1/configs/recorder.yaml is dispatched to GetRobotTypeConfig rather than GetRobotType (the competing GET /robot_types/:id handler)
internal/api/handlers/robot_type_config_template_test.go No test verifies that the admin PUT/PUT/GET/DELETE endpoints are actually behind JWT + admin middleware when RegisterConfigTemplateAdminRoutes is mounted on an authenticated group (the test fixture newTestRobotTypeConfigTemplateRouter mounts admin routes without JWT auth)

Design note

File Line Issue
internal/api/handlers/robot_type_config_template.go 395 getActiveRobotTypeConfigTemplate respects deleted_at IS NULL, which is correct — soft-deleted rows are properly excluded from active lookups even though they share the robot_type_id + filename unique constraint space
Files Reviewed (6 files)
  • docs/designs/robot-type-config-templates.html — design doc, no changes
  • internal/api/handlers/robot_type_config_template.go — 2 issues
  • internal/api/handlers/robot_type_config_template_test.go — coverage gaps noted in summary
  • internal/server/server.go — 1 issue
  • internal/storage/database/migrations/000003_robot_type_config_templates.up.sql — looks correct; soft-delete + unique generated-column pattern matches existing codebase patterns
  • internal/storage/database/migrations/000003_robot_type_config_templates.down.sql — clean drop, matches up.sql

Reviewed by step-3.5-flash · 3,769,541 tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant