Skip to content

feat: add skills repository data model and config store support - #4229

Merged
akshaydeo merged 1 commit into
devfrom
06-10-feat_add_skills_repository_data_model_and_config_store_support
Jun 15, 2026
Merged

feat: add skills repository data model and config store support#4229
akshaydeo merged 1 commit into
devfrom
06-10-feat_add_skills_repository_data_model_and_config_store_support

Conversation

@danpiths

@danpiths danpiths commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the core Skills Repository persistence layer: database models, migrations,
config store interfaces, CRUD/versioning logic, validators, and tests. This is
the foundation used by the management API, serving layer, config registry, and
dashboard.

Changes

  • Added Skills Repository tables for skills, versions, files, and file blobs.
  • Added config store APIs for skill CRUD, versioning, file persistence,
    validation, and orphan blob cleanup.
  • Added migrations and indexes for Skills Repository storage.
  • Added configstore tests for skill version validation and latest-created
    version ordering.
  • Implemented arbitrary relative file paths instead of fixed folder categories.
  • Kept files version-level so each skill version owns its file set.
  • Centralized skill name, version, file path, frontmatter, and reserved-field
    validation helpers.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

Validate this change with the following command:

# From bifrost/
direnv exec . go test ./framework/configstore

Expected result: configstore tests pass, including Skills Repository version
increment validation and latest-created highest_version coverage.

No new configs or environment variables are added in this PR.

Screenshots/Recordings

N/A — backend persistence-only change.

Breaking changes

  • Yes
  • No

Related issues

N/A

Security considerations

This PR adds persistence for skill metadata, version snapshots, file references,
and DB fallback blobs. File path validation rejects absolute paths, backslashes,
trailing slashes, and ./.. segments before storage.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@danpiths
danpiths requested a review from akshaydeo June 9, 2026 20:18
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@danpiths, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 50 seconds. Learn how PR review limits work.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e53b17f-abd1-49b0-9cd7-4e62590bf301

📥 Commits

Reviewing files that changed from the base of the PR and between 2b50a90 and c98c62f.

📒 Files selected for processing (6)
  • .coderabbit.yaml
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go
📝 Walkthrough

Walkthrough

Adds a skills repository: DB models and migrations, ConfigStore API extensions, validation and semver governance, object-store vs inline blob plumbing with post‑commit uploads and compensation, full CRUD/version/serve flows, orphan-blob cleanup, tests, and repository review-config updates.

Changes

Skills Repository Implementation

Layer / File(s) Summary
Data Models & Database Schema
framework/configstore/tables/skills.go
Defines GORM models for skills, versions, files, and file blobs; JSON map Value/Scan implementations and GORM lifecycle hooks.
Migrations
framework/configstore/migrations.go
Registers migrationAddSkillsRepoTables in triggerMigrations; adds idempotent creation and rollback for skills, skill_versions, skill_file_blobs, and skill_files.
API Contract
framework/configstore/store.go
Adds SkillListQueryParams, imports objectstore, and extends ConfigStore with skills methods (Create/Get/Update/Delete/List/Shift versions, GetAllSkillsVersion, CreateSkillFileBlob, CleanupOrphanSkillFileBlobs).
Core plumbing & uploads
framework/configstore/skills.go
Pending object-store write representation, post-commit uploads with compensating DB deletes on failure, inline/blob fallback support, SkillObjectPrefix, and StoreSkillFileContent branching.
Validation & Semver Governance
framework/configstore/skills.go, framework/configstore/rdb_test.go
Skill/name/file/path/version validation, strict semver parsing and monotonic increment enforcement, data: URL decoding and size limits, ownership/scoping checks, and TestValidateSkillVersionIncrementRequiresGreaterVersion.
File persistence, blob helpers & maintenance
framework/configstore/skills.go
Enforces storage_key scoping, persists inline blobs when no object-store, provides CreateSkillFileBlob and CleanupOrphanSkillFileBlobs.
CRUD, Versioning & Serving
framework/configstore/skills.go, framework/configstore/rdb_test.go
CreateSkill (transaction + deferred uploads + governance bump), GetSkill/GetSkillLean/GetSkillByName, ListSkillVersions/GetSkillVersion, UpdateSkill (deferred uploads + optional atomic serve flip), DeleteSkill (with post-commit cleanup), ListSkills, ShiftSkillVersion, and TestLatestCreatedSkillVersionUsesCreationOrder.
Repo config
.coderabbit.yaml
Enables early_access and updates review settings and path-specific review instructions, including a Go context-key typing rule.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RDBConfigStore
  participant PostgresDB
  participant ObjectStore
  Client->>RDBConfigStore: CreateSkill(skill, version, files)
  RDBConfigStore->>PostgresDB: BeginTx + insert skill/version/files
  PostgresDB-->>RDBConfigStore: Commit
  RDBConfigStore->>ObjectStore: Upload deferred file objects (post-commit)
  ObjectStore-->>RDBConfigStore: Upload result / error
  RDBConfigStore->>PostgresDB: Compensating DB delete on upload failure
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • akshaydeo
  • roroghost17

Poem

I'm a rabbit with a migration hop and careful stash,
I trim paths, tuck snapshots, and nudge each semver sash,
I queue uploads, commit, then tidy keys that stray,
I bump the all‑skills tag and chase orphan blobs away,
A cheerful thump — new skills hop out into the cache.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding the skills repository data model and config store support, which aligns with the comprehensive skill-related persistence layer additions.
Description check ✅ Passed The description covers all required sections from the template including Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, and most checklist items are checked.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-10-feat_add_skills_repository_data_model_and_config_store_support

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge; the three-phase update design, compensating deletes, and FOR UPDATE serialization are sound, and the only open issues are minor ordering/contract nits.

The migration is idempotent, the rollback drop order is correct (dependent skill_files dropped first), and the concurrency patterns throughout use proper locking. The two open comments are non-blocking: a missing ORDER BY tiebreaker observable only under simultaneous version creation with identical timestamps, and an implicit caller contract in the serve=true response path that fails loudly rather than silently if violated.

framework/configstore/skills.go — latestCreatedSkillVersion tiebreaker and UpdateSkill Phase 1 serve=true populateSkillFiles contract.

Important Files Changed

Filename Overview
framework/configstore/skills.go New 1216-line file implementing full CRUD/versioning for the Skills Repository; logic is well-structured with compensating deletes for object-store failures. Minor ordering/contract issues noted above.
framework/configstore/migrations.go Adds migrationAddSkillsRepoTables with idempotent guards; rollback drops skill_files first (correct dependency order relative to skill_file_blobs FK), then versions, skills, blobs — no FK constraint violations.
framework/configstore/tables/skills.go Clean GORM table definitions with proper FK constraints, custom JSON scanner/valuer, and BeforeCreate/BeforeSave hooks to initialize map fields.
framework/configstore/store.go Adds Skills Repository method signatures to the ConfigStore interface and defines SkillListQueryParams / SkillVersionListQueryParams.
framework/configstore/rdb_test.go Adds two focused tests: version-increment validation edge cases and latestCreatedSkillVersion creation-order behaviour; both are correct and deterministic given the explicit created_at offsets used.

Reviews (24): Last reviewed commit: "feat: add skills repository data model a..." | Re-trigger Greptile

Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 6607-6616: The rollback in the Rollback function reverses table
drops in an order that violates FK constraints (it calls
mg.DropTable(&tables.TableSkillFileBlob{}) before dropping
&tables.TableSkillFile{}), which can cause failures; change the drop order to
drop &tables.TableSkillFile{} first, then &tables.TableSkillFileBlob{}, and then
&tables.TableSkillVersion{} (or otherwise order drops to respect foreign key
dependencies) so that tables.TableSkillFile is removed before
tables.TableSkillFileBlob in the Rollback function.

In `@framework/configstore/rdb_test.go`:
- Around line 68-92: Add the suggested edge-case table-driven tests to
TestValidateSkillVersionIncrementRequiresGreaterVersion: include cases for an
empty previousVersion (first release) with latest == "" and next == "1.0.0"
expecting no error, an exact duplicate case latest == "1.0.3-beta1" next ==
"1.0.3-beta1" expecting an error, a malformed version case next ==
"not-a-version" expecting an error, and a major increment case latest == "1.0.3"
next == "2.0.0" expecting no error; keep these as additional entries in the
tests slice so validateSkillVersionIncrement is exercised for these edge
conditions.
- Around line 94-143: The field name HighestVersion is ambiguous; add a clear
inline comment on the TableSkill struct next to HighestVersion (e.g., "//
HighestVersion = most recently created version, not the highest semver") to
document the semantic, and if you prefer a stronger change, rename
HighestVersion to LatestCreatedVersion (or MostRecentVersion) and update all
references (latestCreatedSkillVersion, store.GetSkillLean, tests such as
TestLatestCreatedSkillVersionUsesCreationOrder and any DB mappings) to maintain
compilation and behavior.

In `@framework/configstore/skills.go`:
- Around line 177-196: validateSkillVersionIncrement currently only checks
major.minor.patch and treats suffixes as equal only when identical, allowing
regressive prerelease changes; update it to compare prerelease/suffix using
semver precedence after parsing (in the same function that calls
parseSkillSemver) so that when major/minor/patch are equal you compute the
semver precedence of next versus prev (treat empty suffix/release as higher than
any prerelease) and return an error if next is not strictly greater than prev;
use the existing symbols validateSkillVersionIncrement, parseSkillSemver, and
the parsed fields (next.suffix, prev.suffix, next.major/minor/patch) to
implement the comparison and keep the exact-equality check for fully identical
versions.
- Around line 902-916: CreateSkillFileBlob currently creates unreferenced blobs
that CleanupOrphanSkillFileBlobs will immediately delete; change the cleanup to
avoid removing recent uploads by adding a grace-window or explicit state: either
(A) ensure TableSkillFileBlob has a created_at timestamp (or use existing one)
and modify CleanupOrphanSkillFileBlobs to only delete blobs where NOT
EXISTS(...) AND skill_file_blobs.created_at < now() - GRACE_PERIOD (e.g. 10min),
or (B) add a status/attached flag on TableSkillFileBlob set to "pending" in
CreateSkillFileBlob and only delete blobs where status = "pending" AND
created_at older than GRACE_PERIOD; update CreateSkillFileBlob to set the
timestamp/status and update the CleanupOrphanSkillFileBlobs query to filter by
that field instead of deleting all unreferenced rows.
- Around line 295-307: The object-store mutations (objectStore.Put in
StoreSkillFileContent and DeleteBatch used elsewhere) are being executed inside
the DB transaction callback which can cause divergence and long-held locks;
modify StoreSkillFileContent (and the code around DeleteBatch at the other
location) so the transaction only updates/persists DB state (e.g., save the
storage key and metadata) and does not call objectStore.Put/DeleteBatch inside
the tx callback — instead capture the necessary post-commit work (storage key,
file ID, operation type) and perform the object-store Put/Delete after the
transaction successfully commits (either immediately after tx.Commit or via an
outbox/post-commit handler/async GC job), ensuring the DB commit happens first
and network I/O runs outside the transaction.
- Around line 366-396: In the SkillSourceTypeText, SkillSourceTypeDataURL and
SkillSourceTypeUpload branches, reject any non-nil file.StorageKey or
file.BlobID that refer to foreign/unnamed resources by adding ownership
validation before accepting/persisting them: call or implement a helper like
validateStorageKeyOwner(storageKey, expectedPrefix) and
validateBlobOwner(blobID, expectedOwner) (or perform a DB lookup on blob rows)
and return an error if the key/blob is not owned by this skill/tenant; only
clear/accept storageKey/blobID after those checks (update the same branches
around file.StorageKey / file.BlobID handling and use decodeSkillDataURL as
before for data URL parsing).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cd14d42c-d52d-44aa-950d-522f4cd29888

📥 Commits

Reviewing files that changed from the base of the PR and between fdcd6f6 and bded229.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

Comment thread framework/configstore/migrations.go
Comment thread framework/configstore/rdb_test.go
Comment thread framework/configstore/rdb_test.go
Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go Outdated
Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go
@danpiths
danpiths force-pushed the 06-10-chore_update_skills_repository_dependencies_and_workspace_tooling branch from fdcd6f6 to 4c8e557 Compare June 10, 2026 05:57
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from bded229 to 93ef5fd Compare June 10, 2026 05:57
Comment thread framework/configstore/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 6607-6622: The Rollback implementation in the migration currently
unconditionally drops tables (see Rollback function and references to
tables.TableSkillFileBlob, tables.TableSkillFile, tables.TableSkillVersion,
tables.TableSkill) which will destroy pre-existing data on upgraded DBs; change
this to either make the migration forward-only by removing or disabling the
Rollback body (marking it non-rollbackable per guidelines) or implement a safe
conditional rollback that only drops tables you created (e.g., check for a
migration-created marker or existence of schema version before dropping). Update
the migration metadata to explicitly flag it as non-rollbackable if you choose
the forward-only approach.

In `@framework/configstore/skills.go`:
- Around line 472-478: GetSkillLean currently swallows errors from
latestCreatedSkillVersion causing HighestVersion to be missing; change the code
to check the error returned by latestCreatedSkillVersion (called with
s.ScopedDB(ctx), skill.ID) and propagate it instead of ignoring it: if latest,
err := latestCreatedSkillVersion(...); err != nil { return nil,
fmt.Errorf("lookup latest skill version: %w", err) } else if latest != "" {
skill.HighestVersion = latest } so DB failures are returned to the caller rather
than silently dropped.
- Around line 792-798: populateSkillFiles currently preloads only "Files" so
callers like CreateSkill, UpdateSkill, and GetSkillLean may receive skill.Files
without the Blob payload when DB blob fallback is used; modify
populateSkillFiles to Preload "Files.Blob" (i.e., call
tx.Preload("Files").Preload("Files.Blob") or equivalent) when loading the
TableSkillVersion in populateSkillFiles so that skill.Files includes the Blob
data before assigning to skill.Files.

In `@framework/configstore/store.go`:
- Around line 548-561: Create a new SkillsStore interface containing the listed
skill methods (CreateSkill, GetSkill, GetSkillLean, GetSkillByName,
GetSkillVersion, ListSkillVersions, UpdateSkill, DeleteSkill, ListSkills,
ShiftSkillVersion, GetAllSkillsVersion, CreateSkillFileBlob,
CleanupOrphanSkillFileBlobs) and add a Skills() SkillsStore accessor to
ConfigStore; then keep the existing exported skill methods on ConfigStore but
implement them as thin forwarders that call through to c.Skills() so existing
implementations/tests (e.g., MockConfigStore) do not break while new code can
use the dedicated SkillsStore abstraction.

In `@framework/configstore/tables/skills.go`:
- Around line 62-80: scanSkillJSON currently returns early for nil/empty DB JSON
without clearing the provided dest, which can leave stale map/slice data; modify
scanSkillJSON so that when value is nil or when data length is 0 it resets dest
to its zero value before returning (handle pointer-to-map, pointer-to-slice,
etc.), e.g. use reflection to detect that dest is a non-nil pointer and set its
Elem() to its zero value, then return nil; keep the existing behavior for
unsupported types and successful json.Unmarshal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3dd5fe53-6e12-48ea-bd36-4b98a2ec6b72

📥 Commits

Reviewing files that changed from the base of the PR and between bded229 and 93ef5fd.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

Comment thread framework/configstore/migrations.go
Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go
Comment thread framework/configstore/store.go Outdated
Comment thread framework/configstore/tables/skills.go
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 93ef5fd to 7296b12 Compare June 10, 2026 06:27
@danpiths
danpiths force-pushed the 06-10-chore_update_skills_repository_dependencies_and_workspace_tooling branch from 4c8e557 to 2a806eb Compare June 10, 2026 06:27
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 7296b12 to 7971f41 Compare June 10, 2026 11:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 6568-6569: The current HasTable guard only checks for the base
table and can skip applying missing indexes/constraints on retry; replace the
simple HasTable short-circuit with a migration that ensures full schema is
present: for each skills-related model referenced in this file (e.g., the Skill
model and any repo-related models), if db.Migrator().HasTable(...) is true still
call db.AutoMigrate(&Skill{}) (or run explicit
Migrator().CreateIndex/CreateConstraint where needed) so missing DDL is applied,
or alternatively detect specific missing pieces via
db.Migrator().HasIndex/HasConstraint/HasColumn and apply them; update the code
paths around HasTable and the block guarded by it (lines referencing HasTable in
this diff) to always reconcile schema rather than skipping when the table
exists.

In `@framework/configstore/skills.go`:
- Around line 35-45: The runPendingSkillObjectWrites function currently fires
off a goroutine to perform objectStore.Put and returns immediately, which can
leave DB rows referencing missing objects; change this to a durable, bounded,
and error-aware flow: either perform the Put synchronously before returning from
CreateSkill/UpdateSkill (call runPendingSkillObjectWrites synchronously and
propagate Put errors) or implement an explicit outbox/pending state with retry
and acknowledgement (persist pendingSkillObjectWrite rows, have a bounded worker
that reads them and retries Put with backoff and marks them completed), ensure
you respect ctx cancellation and limit concurrency, and surface any
non-transient failures back to the caller instead of only logging in
runPendingSkillObjectWrites and objectStore.Put.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 21e502b6-7f93-4935-8c72-63ee8953126d

📥 Commits

Reviewing files that changed from the base of the PR and between 7296b12 and 7971f41.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

Comment thread framework/configstore/migrations.go
Comment thread framework/configstore/skills.go Outdated
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 7971f41 to 23e726b Compare June 10, 2026 11:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
framework/configstore/skills.go (1)

35-43: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Post-commit object write failures still return success.

CreateSkill/UpdateSkill commit skill_files.storage_key rows before this helper runs, but this helper only logs Put failures. A canceled request context or transient object-store error therefore returns success while leaving committed file rows pointing at objects that were never stored.

As per coding guidelines, framework changes should use explicit error handling and bounded resource usage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 35 - 43,
runPendingSkillObjectWrites currently swallows Put errors (only logs them) so
CreateSkill/UpdateSkill can commit skill_files.storage_key rows even when
objectStore.Put fails; change runPendingSkillObjectWrites to return an error
(e.g., error from objectStore.Put or an aggregated error) instead of only
logging, propagate that error back to callers (CreateSkill/UpdateSkill) and make
callers abort the commit or perform compensating cleanup (delete committed DB
rows) on error; ensure the function respects the provided ctx and bounds
resource usage (avoid unbounded goroutines or retries) and reference the
function name runPendingSkillObjectWrites and the objectStore.Put call when
updating code.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/skills.go`:
- Around line 61-74: SkillReservedFrontmatterFields is exported as a mutable
global map which allows external mutation and can cause concurrent map
read/write panics; change the backing map to an unexported variable (e.g.,
skillReservedFrontmatterFields) and replace the exported map with an access
helper that returns a defensive copy or a read-only query function (e.g.,
GetSkillReservedFrontmatterFields() returning a copied map or
IsSkillReservedFrontmatterField(key) bool). Update callers to use the helper so
external packages cannot mutate the original map and concurrent reads are safe.
- Around line 194-202: parseSkillSemver currently ignores strconv.Atoi errors,
so huge numeric components overflow to zero; update parseSkillSemver to validate
each numeric component conversion and return an error on overflow/parse failure:
after matching with skillSemverPattern, convert matches[1], matches[2],
matches[3] using strconv.ParseInt (or strconv.Atoi) but check and propagate any
error instead of discarding it, and only construct and return the
skillVersionParts on success (preserving suffix from matches[4]); this will
ensure ValidateSkillVersion and ordering/bump logic see correct errors for
values like 999999999999999999999.

---

Duplicate comments:
In `@framework/configstore/skills.go`:
- Around line 35-43: runPendingSkillObjectWrites currently swallows Put errors
(only logs them) so CreateSkill/UpdateSkill can commit skill_files.storage_key
rows even when objectStore.Put fails; change runPendingSkillObjectWrites to
return an error (e.g., error from objectStore.Put or an aggregated error)
instead of only logging, propagate that error back to callers
(CreateSkill/UpdateSkill) and make callers abort the commit or perform
compensating cleanup (delete committed DB rows) on error; ensure the function
respects the provided ctx and bounds resource usage (avoid unbounded goroutines
or retries) and reference the function name runPendingSkillObjectWrites and the
objectStore.Put call when updating code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0bf2fd9d-94ce-4f10-988b-c318b406e0dd

📥 Commits

Reviewing files that changed from the base of the PR and between 7971f41 and 23e726b.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 23e726b to 7fdc066 Compare June 10, 2026 12:07
Comment thread framework/configstore/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
framework/configstore/skills.go (3)

430-444: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce the blob-size limit before decode/insert, not after.

decodeSkillDataURL materializes the full base64 payload before validateSkillFileSource checks maxSkillFileContentSize, so a very large data: URL can still spike memory before rejection. CreateSkillFileBlob also inserts arbitrary blob.Data with no size gate at all. The size bound needs to be enforced at ingress for both paths.

Suggested fix
 func decodeSkillDataURL(dataURL string) ([]byte, string, error) {
 	parsed, err := url.Parse(dataURL)
 	if err != nil || parsed.Scheme != "data" {
 		return nil, "", fmt.Errorf("dataurl must be a valid data: URL")
 	}
 	parts := strings.SplitN(dataURL, ",", 2)
 	if len(parts) != 2 || !strings.Contains(parts[0], ";base64") {
 		return nil, "", fmt.Errorf("dataurl must contain ;base64,")
 	}
+	if len(parts[1]) > base64.StdEncoding.EncodedLen(maxSkillFileContentSize) {
+		return nil, "", fmt.Errorf("dataurl file content exceeds maximum size of %d bytes", maxSkillFileContentSize)
+	}
 	mimeType := strings.TrimPrefix(strings.Split(parts[0], ";")[0], "data:")
 	data, err := base64.StdEncoding.DecodeString(parts[1])
 	if err != nil {
 		return nil, "", fmt.Errorf("dataurl base64 decode failed: %w", err)
 	}
 	return data, mimeType, nil
 }

 func (s *RDBConfigStore) CreateSkillFileBlob(ctx context.Context, blob *tables.TableSkillFileBlob) error {
+	if blob == nil {
+		return fmt.Errorf("skill file blob is required")
+	}
+	if len(blob.Data) > maxSkillFileContentSize {
+		return fmt.Errorf("blob content exceeds maximum size of %d bytes", maxSkillFileContentSize)
+	}
 	return s.DB().WithContext(ctx).Create(blob).Error
 }

As per coding guidelines, framework changes must enforce size limits and bounded resource usage.

Also applies to: 491-497, 1041-1044

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 430 - 444, The function
decodeSkillDataURL currently decodes the entire base64 payload before size
checks and CreateSkillFileBlob inserts blob.Data without gating size; update
decodeSkillDataURL to parse the data: URL, determine the base64 payload length
(or decode in a streaming/limited manner) and reject when the decoded size would
exceed maxSkillFileContentSize (used by validateSkillFileSource) before
performing full base64.DecodeString, and update CreateSkillFileBlob to validate
blob.Data length against maxSkillFileContentSize prior to inserting into
storage; reference decodeSkillDataURL, CreateSkillFileBlob,
validateSkillFileSource, and maxSkillFileContentSize to add the
pre-decode/pre-insert checks so large inputs are rejected early and memory
spikes are avoided.

Source: Coding guidelines


35-43: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Surface post-commit object-store failures to the caller.

runPendingSkillObjectWrites only logs Put failures, so CreateSkill and UpdateSkill still return success even when some persisted storage_key objects were never uploaded. That leaves committed skill_files rows pointing at missing content. This path needs an error-propagating post-commit mechanism (or durable outbox/retry), not best-effort logging.

As per coding guidelines, framework changes should use explicit error handling and bounded resource usage.

Also applies to: 559-560, 744-745

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 35 - 43,
runPendingSkillObjectWrites currently swallows objectStore.Put errors (only
logs) so CreateSkill/UpdateSkill appear successful while storage objects may be
missing; change runPendingSkillObjectWrites to return an error and propagate it
up to callers (CreateSkill and UpdateSkill) so the commit can fail if any Put
fails, and ensure you process the writes with bounded resources (e.g.,
sequentially or with a limited worker pool) using the existing
pendingSkillObjectWrite struct and objectStore.Put calls; update all call sites
(CreateSkill, UpdateSkill and other usages) to handle and return the propagated
error instead of assuming success.

Source: Coding guidelines


377-396: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

blob_id can still bind a foreign pending upload.

The blob_id path only checks that the blob exists and is not already referenced by another skill. Any unbound blob row created through CreateSkillFileBlob is therefore attachable by a different skill, because there is no owner/upload identity on the blob and no ownership check here. That permits cross-skill attachment of foreign uploaded content and later exposure/deletion through the wrong skill.

As per coding guidelines, validate all untrusted input.

Also applies to: 1041-1044

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@framework/configstore/skills.go`:
- Around line 430-444: The function decodeSkillDataURL currently decodes the
entire base64 payload before size checks and CreateSkillFileBlob inserts
blob.Data without gating size; update decodeSkillDataURL to parse the data: URL,
determine the base64 payload length (or decode in a streaming/limited manner)
and reject when the decoded size would exceed maxSkillFileContentSize (used by
validateSkillFileSource) before performing full base64.DecodeString, and update
CreateSkillFileBlob to validate blob.Data length against maxSkillFileContentSize
prior to inserting into storage; reference decodeSkillDataURL,
CreateSkillFileBlob, validateSkillFileSource, and maxSkillFileContentSize to add
the pre-decode/pre-insert checks so large inputs are rejected early and memory
spikes are avoided.
- Around line 35-43: runPendingSkillObjectWrites currently swallows
objectStore.Put errors (only logs) so CreateSkill/UpdateSkill appear successful
while storage objects may be missing; change runPendingSkillObjectWrites to
return an error and propagate it up to callers (CreateSkill and UpdateSkill) so
the commit can fail if any Put fails, and ensure you process the writes with
bounded resources (e.g., sequentially or with a limited worker pool) using the
existing pendingSkillObjectWrite struct and objectStore.Put calls; update all
call sites (CreateSkill, UpdateSkill and other usages) to handle and return the
propagated error instead of assuming success.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8865f61e-b178-4aaf-ba94-3545c36cb5f0

📥 Commits

Reviewing files that changed from the base of the PR and between 7971f41 and 7fdc066.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 7fdc066 to 603a3cf Compare June 10, 2026 12:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
framework/configstore/store.go (1)

548-561: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat the ConfigStore expansion as a breaking API change.

Line 548 adds thirteen methods to an exported Go interface. That is a compile-time break for any out-of-repo ConfigStore implementation or test double, even if this stack updates the in-repo store and mocks. Either keep the skills API off the root interface, or explicitly ship this as a breaking change with the appropriate versioning/release-note treatment. As per coding guidelines, framework/** changes should preserve backward-compatible contracts where possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/store.go` around lines 548 - 561, The ConfigStore
interface was extended with thirteen skill-related methods (CreateSkill,
GetSkill, GetSkillLean, GetSkillByName, GetSkillVersion, ListSkillVersions,
UpdateSkill, DeleteSkill, ListSkills, ShiftSkillVersion, GetAllSkillsVersion,
CreateSkillFileBlob, CleanupOrphanSkillFileBlobs), causing a breaking API change
for external implementations; instead, extract these methods into a new exported
interface (e.g., SkillStore or SkillsRepository) and remove them from
ConfigStore so callers can depend on the smaller ConfigStore and opt into the
new SkillStore where needed; implement the new interface in the in-repo store
types that currently implement those methods and update internal usage sites to
require SkillStore (or both interfaces) while leaving ConfigStore unchanged to
preserve backward compatibility.

Source: Coding guidelines

framework/configstore/skills.go (3)

35-43: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't report success when the post-commit object upload fails.

CreateSkill and UpdateSkill still return success even when objectStore.Put fails, because runPendingSkillObjectWrites only logs the error. That leaves committed storage_key rows pointing at missing objects. This needs a real consistency boundary: either a durable pending/outbox state that is retried before the file becomes serveable, or an API contract that surfaces the failure and prevents exposing the new version until the upload succeeds. As per coding guidelines, framework changes should use explicit error handling and bounded resource usage.

Also applies to: 559-560, 744-745

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 35 - 43,
runPendingSkillObjectWrites currently swallows objectStore.Put failures which
lets CreateSkill/UpdateSkill report success despite missing blobs; change
runPendingSkillObjectWrites to return an error (or aggregate errors) when any
pendingSkillObjectWrite fails, propagate that error back to callers (CreateSkill
and UpdateSkill) and make those APIs fail/rollback (or block committing the
storage_key) when the upload fails; ensure you reference and handle
objectStore.Put errors instead of only logging them, and update all call sites
(including the other occurrences noted) to respect the new error return so the
API does not expose a new version unless all uploads succeeded or a durable
pending/outbox retry state is implemented.

Source: Coding guidelines


377-397: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Orphan blob_ids are still attachable by any skill.

The blob path only checks that the row exists and is not already bound to another skill. Because CreateSkillFileBlob creates unscoped orphan blobs, any caller that knows an unbound blob_id can attach it to a different skill. storage_key has prefix-based ownership checks; DB blobs need an equivalent ownership/scope check as well, or the blob table needs enough metadata to enforce one. As per coding guidelines, framework storage changes should validate untrusted references and preserve least-privilege access to persisted data.

Also applies to: 1041-1044

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 377 - 397, The
validateSkillFileReference currently only checks existence and non-duplication
of a blob_id (in validateSkillFileReference, tables.TableSkillFileBlob and
tables.TableSkillFile), allowing any caller who knows an orphan blob_id to
attach it; fix by enforcing ownership/scope: when loading the blob row in
validateSkillFileReference, select its owner/metadata (e.g., storage_key or an
owner_id/tenant column on tables.TableSkillFileBlob) and verify it is allowed to
be bound to the given skillID (for example by checking storage_key prefix
matches the skill's owner/namespace or by joining skill_versions to verify same
owner/tenant), and reject attachment if the blob's owner/namespace does not
match the skill's owner; if the blob table lacks owner metadata, add a
persistent owner/tenant column and populate it in CreateSkillFileBlob, then use
that column in validateSkillFileReference for the check.

Source: Coding guidelines


430-444: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject oversized data URLs before decoding the payload.

The size limit is enforced only after DecodeString materializes the full byte slice. A sufficiently large data: URL can still blow up memory before the guard runs. Check the decoded length from the base64 payload first and fail early.

Suggested fix
 func decodeSkillDataURL(dataURL string) ([]byte, string, error) {
 	parsed, err := url.Parse(dataURL)
 	if err != nil || parsed.Scheme != "data" {
 		return nil, "", fmt.Errorf("dataurl must be a valid data: URL")
 	}
 	parts := strings.SplitN(dataURL, ",", 2)
 	if len(parts) != 2 || !strings.Contains(parts[0], ";base64") {
 		return nil, "", fmt.Errorf("dataurl must contain ;base64,")
 	}
 	mimeType := strings.TrimPrefix(strings.Split(parts[0], ";")[0], "data:")
+	if base64.StdEncoding.DecodedLen(len(parts[1])) > maxSkillFileContentSize {
+		return nil, "", fmt.Errorf("dataurl file content exceeds maximum size of %d bytes", maxSkillFileContentSize)
+	}
 	data, err := base64.StdEncoding.DecodeString(parts[1])
 	if err != nil {
 		return nil, "", fmt.Errorf("dataurl base64 decode failed: %w", err)
 	}
 	return data, mimeType, nil
 }

As per coding guidelines, framework changes should enforce size limits and bounded resource usage.

Also applies to: 491-497

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 430 - 444, The
decodeSkillDataURL function currently decodes the entire base64 payload before
enforcing the size limit; change it to validate the payload size first by
calculating the decoded length from the base64 payload string (use
base64.StdEncoding.DecodedLen(len(payload))) and reject if that exceeds a
defined limit (introduce or use a constant like maxSkillDataSize), returning an
error before calling base64.StdEncoding.DecodeString; apply the same pre-decode
size check to the other similar routine referenced (the block around lines
491-497) so no data URL is fully materialized before validation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@framework/configstore/skills.go`:
- Around line 35-43: runPendingSkillObjectWrites currently swallows
objectStore.Put failures which lets CreateSkill/UpdateSkill report success
despite missing blobs; change runPendingSkillObjectWrites to return an error (or
aggregate errors) when any pendingSkillObjectWrite fails, propagate that error
back to callers (CreateSkill and UpdateSkill) and make those APIs fail/rollback
(or block committing the storage_key) when the upload fails; ensure you
reference and handle objectStore.Put errors instead of only logging them, and
update all call sites (including the other occurrences noted) to respect the new
error return so the API does not expose a new version unless all uploads
succeeded or a durable pending/outbox retry state is implemented.
- Around line 377-397: The validateSkillFileReference currently only checks
existence and non-duplication of a blob_id (in validateSkillFileReference,
tables.TableSkillFileBlob and tables.TableSkillFile), allowing any caller who
knows an orphan blob_id to attach it; fix by enforcing ownership/scope: when
loading the blob row in validateSkillFileReference, select its owner/metadata
(e.g., storage_key or an owner_id/tenant column on tables.TableSkillFileBlob)
and verify it is allowed to be bound to the given skillID (for example by
checking storage_key prefix matches the skill's owner/namespace or by joining
skill_versions to verify same owner/tenant), and reject attachment if the blob's
owner/namespace does not match the skill's owner; if the blob table lacks owner
metadata, add a persistent owner/tenant column and populate it in
CreateSkillFileBlob, then use that column in validateSkillFileReference for the
check.
- Around line 430-444: The decodeSkillDataURL function currently decodes the
entire base64 payload before enforcing the size limit; change it to validate the
payload size first by calculating the decoded length from the base64 payload
string (use base64.StdEncoding.DecodedLen(len(payload))) and reject if that
exceeds a defined limit (introduce or use a constant like maxSkillDataSize),
returning an error before calling base64.StdEncoding.DecodeString; apply the
same pre-decode size check to the other similar routine referenced (the block
around lines 491-497) so no data URL is fully materialized before validation.

In `@framework/configstore/store.go`:
- Around line 548-561: The ConfigStore interface was extended with thirteen
skill-related methods (CreateSkill, GetSkill, GetSkillLean, GetSkillByName,
GetSkillVersion, ListSkillVersions, UpdateSkill, DeleteSkill, ListSkills,
ShiftSkillVersion, GetAllSkillsVersion, CreateSkillFileBlob,
CleanupOrphanSkillFileBlobs), causing a breaking API change for external
implementations; instead, extract these methods into a new exported interface
(e.g., SkillStore or SkillsRepository) and remove them from ConfigStore so
callers can depend on the smaller ConfigStore and opt into the new SkillStore
where needed; implement the new interface in the in-repo store types that
currently implement those methods and update internal usage sites to require
SkillStore (or both interfaces) while leaving ConfigStore unchanged to preserve
backward compatibility.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62216439-6436-485b-8b1f-f7a209676fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 7971f41 and 7fdc066.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
framework/configstore/rdb_test.go (1)

107-142: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the fixture on a valid monotonic version history.

validateSkillVersionIncrement() rejects 1.0.2-1 after 1.0.3, so this test currently asserts GetSkillLean() behavior on a state the write path should never persist. That makes HighestVersion diverge from the invariant it is later used for during bump validation.

♻️ Minimal fixture tweak
  err = store.DB().Create(&tables.TableSkill{
  	ID:            skillID,
  	Name:          "latest-created",
  	Description:   "Latest created version test",
  	SkillMDBody:   "body",
- 	LatestVersion: "1.0.3",
+ 	LatestVersion: "1.0.4",
  	CreatedAt:     baseTime,
  	UpdatedAt:     baseTime,
  }).Error
@@
  err = store.DB().Create(&tables.TableSkillVersion{
  	ID:                  "skill-version-new",
  	SkillID:             skillID,
- 	Version:             "1.0.2-1",
+ 	Version:             "1.0.4",
  	SkillMDBody:         "body",
  	FrontmatterSnapshot: tables.SkillJSONMap{"name": "latest-created", "description": "Latest created version test"},
  	CreatedAt:           baseTime.Add(time.Minute),
  }).Error
@@
- assert.Equal(t, "1.0.2-1", latest)
+ assert.Equal(t, "1.0.4", latest)
@@
- assert.Equal(t, "1.0.2-1", skill.HighestVersion)
+ assert.Equal(t, "1.0.4", skill.HighestVersion)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/rdb_test.go` around lines 107 - 142, Test fixture uses
an out-of-order version ("1.0.2-1" created after "1.0.3") which violates
validateSkillVersionIncrement() and makes HighestVersion unrealistic; update the
created-later version in the TableSkillVersion record (the "skill-version-new"
entry used by latestCreatedSkillVersion() and verified via GetSkillLean()) to a
monotonic-next value (e.g., "1.0.4") and keep its CreatedAt after baseTime so
the test represents a valid version history for HighestVersion.
framework/configstore/skills.go (1)

35-43: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Propagate post-commit object write failures.

objectStore.Put errors are only logged here, so CreateSkill and UpdateSkill can commit storage_key references and still return success when the object bytes were never stored. Using the request context after commit makes this easy to hit on cancellation/timeouts as well. This needs an error-aware durable handoff (outbox/retry/compensation), not log-and-continue. As per coding guidelines, framework changes should use explicit error handling and bounded resource usage.

Also applies to: 559-560, 744-745

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 35 - 43,
runPendingSkillObjectWrites currently swallows objectStore.Put errors (just
logs) causing CreateSkill/UpdateSkill to commit storage_key refs despite failed
writes; change runPendingSkillObjectWrites to return an error (or aggregated
error) and make callers (CreateSkill, UpdateSkill) handle it instead of assuming
success, e.g., collect per-write failures from objectStore.Put for each
pendingSkillObjectWrite, apply limited retries/backoff or enqueue for durable
handoff and return a clear error if writes ultimately fail; ensure the
implementation bounds resource usage (limit concurrency/queue size) and update
all call sites (CreateSkill, UpdateSkill and the other occurrences noted) to
propagate or handle the returned error rather than log-and-continue.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/skills.go`:
- Around line 566-570: The current query in s.ScopedDB(...) preloads
"Versions.Files.Blob", which eagerly materializes every historical blob and can
cause unbounded memory/IO spikes; remove or stop using
Preload("Versions.Files.Blob") in the skill detail load and instead only load
Blob payloads for the single serving/requested version (e.g., load Versions and
Versions.Files but not Files.Blob), then fetch the Blob for the specific
Version/File afterwards via a targeted query (or lazy accessor) to bound
resource usage; apply the same change where Preload("Versions.Files.Blob") is
used at the other spot (lines 607-611) so blob bytes are only retrieved
on-demand for the serving version.
- Around line 698-704: The update currently writes skills.name via the DB update
(see the tx.Model(...).Select(... "Name" ...) .Updates(skill) call that sets
skill.LatestVersion), but snapshots and ShiftSkillVersion do not capture/restore
Name, causing historical versions to be shown under a renamed name; either make
name immutable by removing "Name" from the update/Select lists or properly
version name by adding Name to the snapshot creation and restoring it inside
ShiftSkillVersion. Fix by (a) removing "Name" from the Select fields in the
update paths that set skill.LatestVersion (and the equivalent blocks at the
other referenced spots) so renames are rejected here, or (b) add Name to the
version snapshot struct and include it when creating/restoring snapshots in
ShiftSkillVersion so old versions preserve their original name—apply the same
change consistently to the update blocks and the snapshot/restore logic.

---

Duplicate comments:
In `@framework/configstore/rdb_test.go`:
- Around line 107-142: Test fixture uses an out-of-order version ("1.0.2-1"
created after "1.0.3") which violates validateSkillVersionIncrement() and makes
HighestVersion unrealistic; update the created-later version in the
TableSkillVersion record (the "skill-version-new" entry used by
latestCreatedSkillVersion() and verified via GetSkillLean()) to a monotonic-next
value (e.g., "1.0.4") and keep its CreatedAt after baseTime so the test
represents a valid version history for HighestVersion.

In `@framework/configstore/skills.go`:
- Around line 35-43: runPendingSkillObjectWrites currently swallows
objectStore.Put errors (just logs) causing CreateSkill/UpdateSkill to commit
storage_key refs despite failed writes; change runPendingSkillObjectWrites to
return an error (or aggregated error) and make callers (CreateSkill,
UpdateSkill) handle it instead of assuming success, e.g., collect per-write
failures from objectStore.Put for each pendingSkillObjectWrite, apply limited
retries/backoff or enqueue for durable handoff and return a clear error if
writes ultimately fail; ensure the implementation bounds resource usage (limit
concurrency/queue size) and update all call sites (CreateSkill, UpdateSkill and
the other occurrences noted) to propagate or handle the returned error rather
than log-and-continue.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aacd7e3e-0bbb-48cb-b944-2fb1467fa7b9

📥 Commits

Reviewing files that changed from the base of the PR and between 7971f41 and 603a3cf.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

Comment thread framework/configstore/skills.go
Comment thread framework/configstore/skills.go Outdated
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 603a3cf to 7626347 Compare June 10, 2026 12:50
@danpiths
danpiths changed the base branch from graphite-base/4229 to dev June 11, 2026 19:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
framework/configstore/store.go (1)

590-604: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Adding the skills surface directly to ConfigStore is a source-breaking API change.

ConfigStore is an exported interface, so widening it here forces every out-of-package implementation to add all of these methods before it compiles, even if that consumer never uses skills. If this package needs to remain backward-compatible, keep the skills API behind an opt-in secondary interface/capability instead of extending the base contract directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/store.go` around lines 590 - 604, The change added many
skill-related methods directly to the exported ConfigStore interface
(CreateSkill, GetSkill, GetSkillLean, GetSkillByName, GetSkillVersion,
ListSkillVersions, UpdateSkill, DeleteSkill, ListSkills, ShiftSkillVersion,
GetAllSkillsVersion, BumpAllSkillsVersion, CreateSkillFileBlob,
CleanupOrphanSkillFileBlobs), which is a breaking API change; instead, extract
those methods into a new opt-in interface (e.g., SkillsStore or
ConfigStoreSkills) and keep the original ConfigStore unchanged, then update only
call sites that need skills to depend on the new interface (or accept
ConfigStore + SkillsStore) and adjust implementing packages to implement the new
interface as needed.
framework/configstore/skills.go (3)

757-765: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject rename attempts explicitly.

UpdateSkill validates skill.Name but never persists it, so a caller can send a rename and still get success while the row keeps the old name. Since this flow now treats names as immutable, compare the incoming name with existing.Name after the row lock and fail when they differ.

Suggested fix
 		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&existing, "id = ?", skill.ID).Error; err != nil {
 			if errors.Is(err, gorm.ErrRecordNotFound) {
 				return ErrNotFound
 			}
 			return err
 		}
+		if skill.Name != existing.Name {
+			return fmt.Errorf("skill name is immutable")
+		}
 		existingLatestVersion = existing.LatestVersion
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 757 - 765, In UpdateSkill,
after you lock and load the row (tables.TableSkill) inside the transaction and
before proceeding to update fields (right after existingLatestVersion =
existing.LatestVersion), explicitly reject rename attempts by comparing
skill.Name to existing.Name and returning a clear error when they differ (e.g.,
return ErrRenameNotAllowed or a new ErrImmutableName) so the call fails rather
than silently ignoring the incoming name; perform this check inside the same
Transaction callback right after the row is read and before any updates are
applied.

442-457: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require upload_id for staged upload keys.

When SourceType == upload and UploadID is empty, any skills/uploads/... key passes the scope check and can be attached as long as it is not already referenced by another skill. That lets a leaked or guessed staged object be bound into this skill, and later reads/deletes operate on that foreign object. As per coding guidelines, validate all untrusted input and avoid overly broad access to external stores.

Suggested fix
 case tables.SkillSourceTypeUpload:
-	if file.UploadID != nil && strings.TrimSpace(*file.UploadID) != "" {
-		expectedPrefix := uploadPrefix + strings.TrimSpace(*file.UploadID) + "/"
-		if !strings.HasPrefix(key, expectedPrefix) {
-			return fmt.Errorf("storage_key %q does not belong to upload_id %q", key, strings.TrimSpace(*file.UploadID))
-		}
-		return nil
-	}
-	if !strings.HasPrefix(key, uploadPrefix) {
-		return fmt.Errorf("storage_key %q is not under the staged upload prefix", key)
-	}
+	if file.UploadID == nil || strings.TrimSpace(*file.UploadID) == "" {
+		return fmt.Errorf("upload storage_key requires upload_id")
+	}
+	expectedPrefix := uploadPrefix + strings.TrimSpace(*file.UploadID) + "/"
+	if !strings.HasPrefix(key, expectedPrefix) {
+		return fmt.Errorf("storage_key %q does not belong to upload_id %q", key, strings.TrimSpace(*file.UploadID))
+	}

Also applies to: 484-500

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 442 - 457, When SourceType ==
tables.SkillSourceTypeUpload, do not allow a generic staged key; require a
non-empty UploadID: if file.UploadID is nil or its trimmed value is empty return
a validation error instead of only checking that key starts with uploadPrefix.
Update the check around file.SourceType / UploadID / expectedPrefix / key (and
the similar block later at the other occurrence) to reject missing UploadID
explicitly so only keys under uploadPrefix+UploadID+"/" are accepted.

Source: Coding guidelines


1138-1142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard nil or blank blobs before generating IDs.

This helper dereferences blob.ID unconditionally, so a nil caller panics here. " " also skips regeneration and can be inserted as the primary key. Normalize the ID and reject nil inputs before the create.

Suggested fix
 func (s *RDBConfigStore) CreateSkillFileBlob(ctx context.Context, blob *tables.TableSkillFileBlob) error {
-	if blob.ID == "" {
+	if blob == nil {
+		return fmt.Errorf("skill file blob is required")
+	}
+	if strings.TrimSpace(blob.ID) == "" {
 		blob.ID = uuid.NewString()
 	}
 	return s.DB().WithContext(ctx).Create(blob).Error
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 1138 - 1142, In
CreateSkillFileBlob on RDBConfigStore, guard against a nil blob and
normalize/validate blob.ID before using it: if blob is nil return an error; trim
whitespace from blob.ID and if the trimmed ID is empty generate a new uuid with
uuid.NewString() and assign it; ensure you never dereference blob when nil and
reject blank-only IDs by normalizing before the DB Create call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 10102-10119: Update the Migrate function in the migration (the
Migrate closure that builds stmt using tx.Dialector.Name() and idxName) to
detect and repair an existing invalid/unfinished Postgres index before relying
on "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS": when Dialector.Name() !=
"sqlite" query pg_catalog.pg_index/pg_class (checking pg_index.indisvalid and/or
indisready for the index named idxName) and if the index exists but is not
valid/ready, run "DROP INDEX CONCURRENTLY <idxName>" then recreate it with
"CREATE UNIQUE INDEX CONCURRENTLY <idxName> ON governance_customers (name)";
keep the sqlite branch unchanged and ensure all Exec calls use
tx.WithContext(ctx) as currently done and return wrapped errors like the
existing fmt.Errorf on failure.

---

Duplicate comments:
In `@framework/configstore/skills.go`:
- Around line 757-765: In UpdateSkill, after you lock and load the row
(tables.TableSkill) inside the transaction and before proceeding to update
fields (right after existingLatestVersion = existing.LatestVersion), explicitly
reject rename attempts by comparing skill.Name to existing.Name and returning a
clear error when they differ (e.g., return ErrRenameNotAllowed or a new
ErrImmutableName) so the call fails rather than silently ignoring the incoming
name; perform this check inside the same Transaction callback right after the
row is read and before any updates are applied.
- Around line 442-457: When SourceType == tables.SkillSourceTypeUpload, do not
allow a generic staged key; require a non-empty UploadID: if file.UploadID is
nil or its trimmed value is empty return a validation error instead of only
checking that key starts with uploadPrefix. Update the check around
file.SourceType / UploadID / expectedPrefix / key (and the similar block later
at the other occurrence) to reject missing UploadID explicitly so only keys
under uploadPrefix+UploadID+"/" are accepted.
- Around line 1138-1142: In CreateSkillFileBlob on RDBConfigStore, guard against
a nil blob and normalize/validate blob.ID before using it: if blob is nil return
an error; trim whitespace from blob.ID and if the trimmed ID is empty generate a
new uuid with uuid.NewString() and assign it; ensure you never dereference blob
when nil and reject blank-only IDs by normalizing before the DB Create call.

In `@framework/configstore/store.go`:
- Around line 590-604: The change added many skill-related methods directly to
the exported ConfigStore interface (CreateSkill, GetSkill, GetSkillLean,
GetSkillByName, GetSkillVersion, ListSkillVersions, UpdateSkill, DeleteSkill,
ListSkills, ShiftSkillVersion, GetAllSkillsVersion, BumpAllSkillsVersion,
CreateSkillFileBlob, CleanupOrphanSkillFileBlobs), which is a breaking API
change; instead, extract those methods into a new opt-in interface (e.g.,
SkillsStore or ConfigStoreSkills) and keep the original ConfigStore unchanged,
then update only call sites that need skills to depend on the new interface (or
accept ConfigStore + SkillsStore) and adjust implementing packages to implement
the new interface as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3bb91028-658f-416b-8e7f-402a4389f911

📥 Commits

Reviewing files that changed from the base of the PR and between 32a533f and 5c4ef68.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

♻️ Duplicate comments (4)
framework/configstore/store.go (1)

590-604: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Adding the skills surface directly to ConfigStore is a source-breaking API change.

ConfigStore is an exported interface, so widening it here forces every out-of-package implementation to add all of these methods before it compiles, even if that consumer never uses skills. If this package needs to remain backward-compatible, keep the skills API behind an opt-in secondary interface/capability instead of extending the base contract directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/store.go` around lines 590 - 604, The change added many
skill-related methods directly to the exported ConfigStore interface
(CreateSkill, GetSkill, GetSkillLean, GetSkillByName, GetSkillVersion,
ListSkillVersions, UpdateSkill, DeleteSkill, ListSkills, ShiftSkillVersion,
GetAllSkillsVersion, BumpAllSkillsVersion, CreateSkillFileBlob,
CleanupOrphanSkillFileBlobs), which is a breaking API change; instead, extract
those methods into a new opt-in interface (e.g., SkillsStore or
ConfigStoreSkills) and keep the original ConfigStore unchanged, then update only
call sites that need skills to depend on the new interface (or accept
ConfigStore + SkillsStore) and adjust implementing packages to implement the new
interface as needed.
framework/configstore/skills.go (3)

757-765: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject rename attempts explicitly.

UpdateSkill validates skill.Name but never persists it, so a caller can send a rename and still get success while the row keeps the old name. Since this flow now treats names as immutable, compare the incoming name with existing.Name after the row lock and fail when they differ.

Suggested fix
 		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&existing, "id = ?", skill.ID).Error; err != nil {
 			if errors.Is(err, gorm.ErrRecordNotFound) {
 				return ErrNotFound
 			}
 			return err
 		}
+		if skill.Name != existing.Name {
+			return fmt.Errorf("skill name is immutable")
+		}
 		existingLatestVersion = existing.LatestVersion
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 757 - 765, In UpdateSkill,
after you lock and load the row (tables.TableSkill) inside the transaction and
before proceeding to update fields (right after existingLatestVersion =
existing.LatestVersion), explicitly reject rename attempts by comparing
skill.Name to existing.Name and returning a clear error when they differ (e.g.,
return ErrRenameNotAllowed or a new ErrImmutableName) so the call fails rather
than silently ignoring the incoming name; perform this check inside the same
Transaction callback right after the row is read and before any updates are
applied.

442-457: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require upload_id for staged upload keys.

When SourceType == upload and UploadID is empty, any skills/uploads/... key passes the scope check and can be attached as long as it is not already referenced by another skill. That lets a leaked or guessed staged object be bound into this skill, and later reads/deletes operate on that foreign object. As per coding guidelines, validate all untrusted input and avoid overly broad access to external stores.

Suggested fix
 case tables.SkillSourceTypeUpload:
-	if file.UploadID != nil && strings.TrimSpace(*file.UploadID) != "" {
-		expectedPrefix := uploadPrefix + strings.TrimSpace(*file.UploadID) + "/"
-		if !strings.HasPrefix(key, expectedPrefix) {
-			return fmt.Errorf("storage_key %q does not belong to upload_id %q", key, strings.TrimSpace(*file.UploadID))
-		}
-		return nil
-	}
-	if !strings.HasPrefix(key, uploadPrefix) {
-		return fmt.Errorf("storage_key %q is not under the staged upload prefix", key)
-	}
+	if file.UploadID == nil || strings.TrimSpace(*file.UploadID) == "" {
+		return fmt.Errorf("upload storage_key requires upload_id")
+	}
+	expectedPrefix := uploadPrefix + strings.TrimSpace(*file.UploadID) + "/"
+	if !strings.HasPrefix(key, expectedPrefix) {
+		return fmt.Errorf("storage_key %q does not belong to upload_id %q", key, strings.TrimSpace(*file.UploadID))
+	}

Also applies to: 484-500

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 442 - 457, When SourceType ==
tables.SkillSourceTypeUpload, do not allow a generic staged key; require a
non-empty UploadID: if file.UploadID is nil or its trimmed value is empty return
a validation error instead of only checking that key starts with uploadPrefix.
Update the check around file.SourceType / UploadID / expectedPrefix / key (and
the similar block later at the other occurrence) to reject missing UploadID
explicitly so only keys under uploadPrefix+UploadID+"/" are accepted.

Source: Coding guidelines


1138-1142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard nil or blank blobs before generating IDs.

This helper dereferences blob.ID unconditionally, so a nil caller panics here. " " also skips regeneration and can be inserted as the primary key. Normalize the ID and reject nil inputs before the create.

Suggested fix
 func (s *RDBConfigStore) CreateSkillFileBlob(ctx context.Context, blob *tables.TableSkillFileBlob) error {
-	if blob.ID == "" {
+	if blob == nil {
+		return fmt.Errorf("skill file blob is required")
+	}
+	if strings.TrimSpace(blob.ID) == "" {
 		blob.ID = uuid.NewString()
 	}
 	return s.DB().WithContext(ctx).Create(blob).Error
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 1138 - 1142, In
CreateSkillFileBlob on RDBConfigStore, guard against a nil blob and
normalize/validate blob.ID before using it: if blob is nil return an error; trim
whitespace from blob.ID and if the trimmed ID is empty generate a new uuid with
uuid.NewString() and assign it; ensure you never dereference blob when nil and
reject blank-only IDs by normalizing before the DB Create call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 10102-10119: Update the Migrate function in the migration (the
Migrate closure that builds stmt using tx.Dialector.Name() and idxName) to
detect and repair an existing invalid/unfinished Postgres index before relying
on "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS": when Dialector.Name() !=
"sqlite" query pg_catalog.pg_index/pg_class (checking pg_index.indisvalid and/or
indisready for the index named idxName) and if the index exists but is not
valid/ready, run "DROP INDEX CONCURRENTLY <idxName>" then recreate it with
"CREATE UNIQUE INDEX CONCURRENTLY <idxName> ON governance_customers (name)";
keep the sqlite branch unchanged and ensure all Exec calls use
tx.WithContext(ctx) as currently done and return wrapped errors like the
existing fmt.Errorf on failure.

---

Duplicate comments:
In `@framework/configstore/skills.go`:
- Around line 757-765: In UpdateSkill, after you lock and load the row
(tables.TableSkill) inside the transaction and before proceeding to update
fields (right after existingLatestVersion = existing.LatestVersion), explicitly
reject rename attempts by comparing skill.Name to existing.Name and returning a
clear error when they differ (e.g., return ErrRenameNotAllowed or a new
ErrImmutableName) so the call fails rather than silently ignoring the incoming
name; perform this check inside the same Transaction callback right after the
row is read and before any updates are applied.
- Around line 442-457: When SourceType == tables.SkillSourceTypeUpload, do not
allow a generic staged key; require a non-empty UploadID: if file.UploadID is
nil or its trimmed value is empty return a validation error instead of only
checking that key starts with uploadPrefix. Update the check around
file.SourceType / UploadID / expectedPrefix / key (and the similar block later
at the other occurrence) to reject missing UploadID explicitly so only keys
under uploadPrefix+UploadID+"/" are accepted.
- Around line 1138-1142: In CreateSkillFileBlob on RDBConfigStore, guard against
a nil blob and normalize/validate blob.ID before using it: if blob is nil return
an error; trim whitespace from blob.ID and if the trimmed ID is empty generate a
new uuid with uuid.NewString() and assign it; ensure you never dereference blob
when nil and reject blank-only IDs by normalizing before the DB Create call.

In `@framework/configstore/store.go`:
- Around line 590-604: The change added many skill-related methods directly to
the exported ConfigStore interface (CreateSkill, GetSkill, GetSkillLean,
GetSkillByName, GetSkillVersion, ListSkillVersions, UpdateSkill, DeleteSkill,
ListSkills, ShiftSkillVersion, GetAllSkillsVersion, BumpAllSkillsVersion,
CreateSkillFileBlob, CleanupOrphanSkillFileBlobs), which is a breaking API
change; instead, extract those methods into a new opt-in interface (e.g.,
SkillsStore or ConfigStoreSkills) and keep the original ConfigStore unchanged,
then update only call sites that need skills to depend on the new interface (or
accept ConfigStore + SkillsStore) and adjust implementing packages to implement
the new interface as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3bb91028-658f-416b-8e7f-402a4389f911

📥 Commits

Reviewing files that changed from the base of the PR and between 32a533f and 5c4ef68.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go
🛑 Comments failed to post (1)
framework/configstore/migrations.go (1)

10102-10119: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show context around specified lines
sed -n '10060,10180p' framework/configstore/migrations.go | nl -ba | sed -n '1,140p'

Repository: maximhq/bifrost

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find occurrences of governance_customers (name) unique index migration
rg -n "governance_customers\s*\(name\)" framework/configstore/migrations.go

Repository: maximhq/bifrost

Length of output: 272


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Search for any handling of pg_index.indisvalid / indisvalid repairs
rg -n "indisvalid|indisready|pg_index|pg_class|REINDEX|concurrently.*invalid" framework/configstore/migrations.go

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Search for any other CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS patterns
rg -n "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS" framework/configstore/migrations.go

Repository: maximhq/bifrost

Length of output: 172


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Identify idxName definition in vicinity
sed -n '10080,10130p' framework/configstore/migrations.go | nl -ba

Repository: maximhq/bifrost

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Try to capture surrounding migration entry (maybe within a Migrations slice) for context
sed -n '9960,10240p' framework/configstore/migrations.go | nl -ba | head -n 240

Repository: maximhq/bifrost

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Context 10060-10180 ==="
sed -n '10060,10180p' framework/configstore/migrations.go | nl -ba | sed -n '1,140p'

echo "=== governance_customers(name) occurrences ==="
rg -n "governance_customers\s*\(name\)" framework/configstore/migrations.go || true

echo "=== indisvalid/pg_index/REINDEX patterns ==="
rg -n "indisvalid|indisready|pg_index|pg_class|REINDEX|concurrently.*invalid" framework/configstore/migrations.go || true

echo "=== CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ==="
rg -n "CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS" framework/configstore/migrations.go || true

echo "=== idxName vicinity 10080-10130 ==="
sed -n '10080,10130p' framework/configstore/migrations.go | nl -ba

echo "=== Surrounding migration entry 9960-10240 (first 240 lines) ==="
sed -n '9960,10240p' framework/configstore/migrations.go | nl -ba | head -n 240

Repository: maximhq/bifrost

Length of output: 148


Repair invalid concurrent unique indexes on PostgreSQL to avoid skipping enforcement.

The migration in framework/configstore/migrations.go uses CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS for governance_customers (name) on non-sqlite, but there’s no handling for an existing invalid index from an interrupted concurrent build—so a rerun can hit IF NOT EXISTS and leave governance_customers.name without enforced uniqueness. Add a check of index validity (e.g., pg_index.indisvalid/indisready) and DROP INDEX CONCURRENTLY + recreate when invalid.

🧰 Tools
🪛 OpenGrep (1.22.0)

[ERROR] 10118-10118: SQL query built via fmt.Sprintf or string concatenation passed to a database method. Use parameterized queries with placeholder arguments.

(coderabbit.sql-injection.go-query-format)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/migrations.go` around lines 10102 - 10119, Update the
Migrate function in the migration (the Migrate closure that builds stmt using
tx.Dialector.Name() and idxName) to detect and repair an existing
invalid/unfinished Postgres index before relying on "CREATE UNIQUE INDEX
CONCURRENTLY IF NOT EXISTS": when Dialector.Name() != "sqlite" query
pg_catalog.pg_index/pg_class (checking pg_index.indisvalid and/or indisready for
the index named idxName) and if the index exists but is not valid/ready, run
"DROP INDEX CONCURRENTLY <idxName>" then recreate it with "CREATE UNIQUE INDEX
CONCURRENTLY <idxName> ON governance_customers (name)"; keep the sqlite branch
unchanged and ensure all Exec calls use tx.WithContext(ctx) as currently done
and return wrapped errors like the existing fmt.Errorf on failure.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 10086-10088: The migration's Rollback function currently returns
nil even though this dedup step permanently rewrites customer names; update the
migration to mark this step as non-rollbackable by replacing the no-op Rollback
with an explicit non-rollback signal (e.g., set Rollback to nil or return a
descriptive error) on the same migration struct so callers know this step cannot
be undone, and add a short comment in the migration near Rollback explaining
that customer rows are irreversibly renamed while the subsequent index-drop step
remains reversible.

In `@framework/configstore/skills.go`:
- Around line 1138-1142: The CreateSkillFileBlob method dereferences blob.ID
without checking for nil; add a nil guard at the top of
RDBConfigStore.CreateSkillFileBlob that returns a clear validation/error (e.g.,
fmt.Errorf or a typed error) when blob is nil, and only generate
uuid.NewString() and call s.DB().WithContext(ctx).Create(blob) when blob is
non-nil; reference function CreateSkillFileBlob and type
tables.TableSkillFileBlob in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0cafc539-fb7d-418b-b9de-3a7cc3585d82

📥 Commits

Reviewing files that changed from the base of the PR and between 32a533f and 5c4ef68.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@framework/configstore/migrations.go`:
- Around line 10086-10088: The migration's Rollback function currently returns
nil even though this dedup step permanently rewrites customer names; update the
migration to mark this step as non-rollbackable by replacing the no-op Rollback
with an explicit non-rollback signal (e.g., set Rollback to nil or return a
descriptive error) on the same migration struct so callers know this step cannot
be undone, and add a short comment in the migration near Rollback explaining
that customer rows are irreversibly renamed while the subsequent index-drop step
remains reversible.

In `@framework/configstore/skills.go`:
- Around line 1138-1142: The CreateSkillFileBlob method dereferences blob.ID
without checking for nil; add a nil guard at the top of
RDBConfigStore.CreateSkillFileBlob that returns a clear validation/error (e.g.,
fmt.Errorf or a typed error) when blob is nil, and only generate
uuid.NewString() and call s.DB().WithContext(ctx).Create(blob) when blob is
non-nil; reference function CreateSkillFileBlob and type
tables.TableSkillFileBlob in your change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0cafc539-fb7d-418b-b9de-3a7cc3585d82

📥 Commits

Reviewing files that changed from the base of the PR and between 32a533f and 5c4ef68.

📒 Files selected for processing (5)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go
🛑 Comments failed to post (2)
framework/configstore/migrations.go (1)

10086-10088: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback still reports success after an irreversible customer-name rewrite.

Lines 10086-10088 return nil even though this step permanently renames persisted customer rows. That makes the migration look rollback-safe when only the later index can actually be reverted. Keep the index rollback in step 2 if you want, but this dedup step itself should be marked non-rollbackable.

Suggested fix
 		Rollback: func(tx *gorm.DB) error {
-			return nil // name renames are not reversed; dropping the index in step 2 restores the invariant
+			return fmt.Errorf("add_customer_name_unique_constraint_dedup is non-rollbackable: renamed customer names are not restored")
 		},

As per coding guidelines, "If a migration cannot be rolled back, explicitly flag it as non-rollbackable." Based on learnings, best-effort rollback paths should still document unreverted data changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

		Rollback: func(tx *gorm.DB) error {
			return fmt.Errorf("add_customer_name_unique_constraint_dedup is non-rollbackable: renamed customer names are not restored")
		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/migrations.go` around lines 10086 - 10088, The
migration's Rollback function currently returns nil even though this dedup step
permanently rewrites customer names; update the migration to mark this step as
non-rollbackable by replacing the no-op Rollback with an explicit non-rollback
signal (e.g., set Rollback to nil or return a descriptive error) on the same
migration struct so callers know this step cannot be undone, and add a short
comment in the migration near Rollback explaining that customer rows are
irreversibly renamed while the subsequent index-drop step remains reversible.

Sources: Coding guidelines, Learnings

framework/configstore/skills.go (1)

1138-1142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard CreateSkillFileBlob against nil input.

This exported helper dereferences blob.ID unconditionally. A nil caller will panic the request path instead of getting a validation error.

Proposed fix
 func (s *RDBConfigStore) CreateSkillFileBlob(ctx context.Context, blob *tables.TableSkillFileBlob) error {
+	if blob == nil {
+		return fmt.Errorf("skill file blob is required")
+	}
 	if blob.ID == "" {
 		blob.ID = uuid.NewString()
 	}
 	return s.DB().WithContext(ctx).Create(blob).Error
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

func (s *RDBConfigStore) CreateSkillFileBlob(ctx context.Context, blob *tables.TableSkillFileBlob) error {
	if blob == nil {
		return fmt.Errorf("skill file blob is required")
	}
	if blob.ID == "" {
		blob.ID = uuid.NewString()
	}
	return s.DB().WithContext(ctx).Create(blob).Error
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/configstore/skills.go` around lines 1138 - 1142, The
CreateSkillFileBlob method dereferences blob.ID without checking for nil; add a
nil guard at the top of RDBConfigStore.CreateSkillFileBlob that returns a clear
validation/error (e.g., fmt.Errorf or a typed error) when blob is nil, and only
generate uuid.NewString() and call s.DB().WithContext(ctx).Create(blob) when
blob is non-nil; reference function CreateSkillFileBlob and type
tables.TableSkillFileBlob in your change.

@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch 2 times, most recently from f3432d9 to 2b50a90 Compare June 12, 2026 07:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.coderabbit.yaml (1)

24-29: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Duplicate reviews.auto_review key makes the config invalid.

reviews.auto_review is declared at both Line 15 and Line 24. This causes a duplicate mapping key parse error and can prevent the settings file from loading.

Suggested fix (merge into one auto_review block)
 reviews:
   high_level_summary_in_walkthrough: true
   fail_commit_status: true
   suggested_labels: false
   auto_assign_reviewers: true
   auto_review:
+    enabled: true
+    drafts: false
+    auto_incremental_review: true
+    base_branches:
+      - ".*"
     auto_pause_after_reviewed_commits: 0
   profile: chill
   request_changes_workflow: true
   high_level_summary: true
   review_status: true
   changed_files_summary: true
   slop_detection:
     enabled: true
-  auto_review:
-    enabled: true
-    drafts: false
-    auto_incremental_review: true
-    base_branches:
-      - ".*"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.coderabbit.yaml around lines 24 - 29, The YAML has duplicate
reviews.auto_review mappings (one at Line 15 and another at Line 24) which makes
the config invalid; locate both occurrences of the reviews.auto_review block and
merge them into a single auto_review mapping that contains the combined keys
(e.g., enabled, drafts, auto_incremental_review, base_branches) preserving
desired values, then remove the duplicate block so only one reviews.auto_review
key exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.coderabbit.yaml:
- Around line 24-29: The YAML has duplicate reviews.auto_review mappings (one at
Line 15 and another at Line 24) which makes the config invalid; locate both
occurrences of the reviews.auto_review block and merge them into a single
auto_review mapping that contains the combined keys (e.g., enabled, drafts,
auto_incremental_review, base_branches) preserving desired values, then remove
the duplicate block so only one reviews.auto_review key exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f5691390-8377-47e8-b16e-4d46b7199bad

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4ef68 and f3432d9.

📒 Files selected for processing (6)
  • .coderabbit.yaml
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/skills.go
  • framework/configstore/store.go
  • framework/configstore/tables/skills.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • framework/configstore/migrations.go
  • framework/configstore/rdb_test.go
  • framework/configstore/tables/skills.go
  • framework/configstore/skills.go

@coderabbitai
coderabbitai Bot requested a review from roroghost17 June 12, 2026 07:16
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 12, 2026
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch 2 times, most recently from da190bf to 1a22c06 Compare June 12, 2026 09:55
@akshaydeo
akshaydeo requested a review from a team as a code owner June 12, 2026 10:36
@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 1a22c06 to 025890c Compare June 12, 2026 13:42

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re: the P1 confidence note about UpdateSkill with serve=true — populateSkillFiles (line 819) populates the response struct, not the version creation. new version files come from skill.Files (line 795) set by the caller — LatestVersion is irrelevant to that path. Phase 3 (line 840) atomically flips LatestVersion = version in the DB. the handler also reloads the full skill via GetSkill after UpdateSkill returns, so intermediate struct state doesn't matter.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

You're right — I misread the data flow. The three points hold up:

  1. files := skill.Files captures the caller's file list before createSkillVersion/createVersionFiles run, so populateSkillFiles on line 819 is entirely orthogonal to version creation.
  2. In the serve=true path, skill.LatestVersion in the struct is a transient intermediate; Phase 3 is the only place that writes LatestVersion to the DB.
  3. Since the handler discards the struct and reloads via GetSkill after UpdateSkill returns, whatever populateSkillFiles does to the in-memory struct in Phase 1 is irrelevant to the final response.

The original concern was unfounded. Thanks for walking through the phases — the logic is sound.

Copy link
Copy Markdown
Collaborator Author

@greptileai

@danpiths
danpiths force-pushed the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch from 025890c to c98c62f Compare June 12, 2026 14:35

akshaydeo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jun 15, 4:44 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 15, 4:44 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit e321f36 into dev Jun 15, 2026
13 of 15 checks passed
@akshaydeo
akshaydeo deleted the 06-10-feat_add_skills_repository_data_model_and_config_store_support branch June 15, 2026 04:44
akshaydeo pushed a commit that referenced this pull request Jun 15, 2026
## Summary

Adds the core Skills Repository persistence layer: database models, migrations,
config store interfaces, CRUD/versioning logic, validators, and tests. This is
the foundation used by the management API, serving layer, config registry, and
dashboard.

## Changes

- Added Skills Repository tables for skills, versions, files, and file blobs.
- Added config store APIs for skill CRUD, versioning, file persistence,
  validation, and orphan blob cleanup.
- Added migrations and indexes for Skills Repository storage.
- Added configstore tests for skill version validation and latest-created
  version ordering.
- Implemented arbitrary relative file paths instead of fixed folder categories.
- Kept files version-level so each skill version owns its file set.
- Centralized skill name, version, file path, frontmatter, and reserved-field
  validation helpers.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

Validate this change with the following command:

```sh
# From bifrost/
direnv exec . go test ./framework/configstore
```

Expected result: configstore tests pass, including Skills Repository version
increment validation and latest-created `highest_version` coverage.

No new configs or environment variables are added in this PR.

## Screenshots/Recordings

N/A — backend persistence-only change.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

This PR adds persistence for skill metadata, version snapshots, file references,
and DB fallback blobs. File path validation rejects absolute paths, backslashes,
trailing slashes, and `.`/`..` segments before storage.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
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.

3 participants