[2026 Templates] Validate templates against the registry schema before use - #842
Open
solaawojobi00-bit wants to merge 1 commit into
Open
Conversation
…re use
templates/registry.schema.json existed but nothing read it, so a malformed
template failed late: as an opaque serde error naming no field, or only once
the template was scaffolded.
The schema is now the authoritative description of a registry document and is
enforced everywhere a registry enters the CLI, with every problem anchored to
the field that caused it:
templates[3].version: 'v1.2' is not valid semver (expected major.minor.patch, e.g. "1.2.0")
templates[3].source.url: required field is missing
templates[4].maintenance: 'archived' is not one of: active, maintained, deprecated, unknown
- templates/registry.schema.json now describes the whole registry document
(envelope plus $defs for entries, sources, security review and changelog)
rather than a single entry, and declares as `x-format` the semantic checks
plain JSON Schema cannot express: semver, rfc3339, date, url, git-url and
template-name.
- utils::template_schema validates against it and reports all problems at
once as field/message pairs. Unknown fields are warnings rather than
errors, so an older CLI still reads a registry written by a newer one.
- Loaders validate before use: a remote registry is checked *before* it is
cached, so a broken marketplace index can no longer replace a working local
cache; the local cache and the bundled fallback are checked on read; and
save_registry refuses to persist a registry that does not match.
- Install names derived from a path or git URL are checked before anything is
fetched, so a name carrying a path separator cannot escape the template
store.
- New `starforge template validate [PATH] [--json]` checks the registry the
CLI would load, a registry file, or a single template entry.
- SecurityReview::findings is Option<u32>, matching the schema and the
published registry. It was Option<String>, so the bundled registry - the
offline fallback - could not be deserialized at all.
Docs: templates/README.md gains a "Registry Validation" section covering the
checks, where validation runs, forward compatibility and the findings
migration note; TEMPLATE_CONTRIBUTING.md and TEMPLATE_MARKETPLACE.md point at
it.
Also repairs pre-existing breakage on master that prevented `cargo build`,
`cargo test` and `cargo fmt --check` from succeeding at all, and fixes the
latent test failures that surfaced once the crate compiled:
- build: missing thiserror dependency, a duplicated read_spec_entries, a
rusqlite Transaction mutability error, a duplicated struct field in
audit.rs, a missing PartialEq on ComplianceSeverity, unregistered ai_doc_qa
modules, the dropped InstalledPlugin::description helpers, three missing
ai_test_assistant generators, and several test helpers left behind by
struct changes.
- database: initialize() tested a Result<Option<_>> with is_ok(), so a fresh
database never recorded its schema version; migration rollback dropped the
runner's own bookkeeping tables and then wrote to them.
- ai_telemetry: model pricing matched the first substring, billing
gpt-4o-mini at gpt-4o rates.
- ai_test_assistant: is_mutating only looked for &mut self, which Soroban
contracts never use.
- ai_template_testing: the soroban-sdk dependency check read the [package]
table, so it fired on every valid template.
- security/ai_audit: the reentrancy check missed setter-helper state writes.
- compliance: the risk score was normalised by 500 when only 290 was
reachable, leaving Critical unreachable.
- test_optimizer: batch_tests_by_profile discarded every non-I/O test;
classify_test treated the universal "test_" prefix as a Unit signal,
leaving General unreachable; save_state never created its directory.
- nl: "show" was listed as a stop word although several patterns key on it;
contract IDs were lower-cased before an upper-case prefix check; "invoke"
was treated as introducing a function name.
- context_help: recency was bucketed in seconds, putting an entry exactly one
day old on the wrong side of the boundary.
Closes Nanle-code#686
solaawojobi00-bit
force-pushed
the
fix/issue-686-template-schema-validation
branch
from
August 27, 2026 04:07
20df195 to
7d575a2
Compare
|
@solaawojobi00-bit Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Validate templates against the registry schema before use
Problem
templates/registry.schema.jsonexisted in the repository but nothing read it. Every registry document — the one bundled in the binary, one fetched from the marketplace, and the local cache at~/.starforge/templates/registry.json— was handed straight toserde_json, so a malformed template failed late and opaquely.versionFailed to deserialize remote template registry JSON— no field named; the broken body is already cached, replacing a working cacheremote registry <url> does not match the template registry schema (1 problem)→templates[3].version: 'v1.2' is not valid semver (expected major.minor.patch, e.g. "1.2.0"), and the cache is left untouchedsourcetemplates[0].source: required field is missingmaintenancevaluetemplates[4].maintenance: 'archived' is not one of: active, maintained, deprecated, unknownsecurity_review.findingsis a number../../etc/passwdThe
findingsrow is worth calling out:SecurityReview::findingswasOption<String>while both the schema andtemplates/registry.jsonuse a number, soserde_json::from_str::<TemplateRegistry>(DEFAULT_REGISTRY)always failed. Any user without a cache and without network had no marketplace at all.Solution
Make the schema authoritative and check every registry against it before it is used, reporting all problems at once, each anchored to the field that caused it.
Changes by file
templates/registry.schema.jsonRewritten to describe the whole registry document rather than a single entry: a root envelope with
version/templates, and$defsfortemplateEntry,source(aoneOfover the git/local/builtin variants),securityReviewandchangelogEntry.Two StarForge extensions carry what plain JSON Schema cannot express without regular expressions:
x-format—semver,rfc3339,date,url,git-url,template-name.x-unknown-properties: "warn"— unknown fields are reported as warnings rather than errors, so an older CLI stays forward compatible with a newer registry while an author still seesdescriptonflagged.findingsis["integer", "null"], matching the published registry.src/utils/template_schema.rs(new, 974 lines)The validator. Implements the subset of JSON Schema the registry schema actually uses (
$ref,type,enum,const,required,properties,items,oneOf,minLength,maxLength,minItems,minimum,maximum) plus the two extensions above, and returns aValidationReportof{field, message}pairs.Rationale for a focused validator rather than a JSON Schema crate: it keeps the dependency tree unchanged, and it lets
oneOffailures use thetypediscriminator so a broken git source reportssource.url: required field is missinginstead of "does not match any subschema".Two rules span a whole registry and are checked separately:
cli_version_minmay not exceedcli_version_max, and no two entries may share anameandversion(the same template at different versions is fine).check_template_nameis exposed for callers that derive a name from user input before any file is written — the name becomes a directory under the template store.src/utils/templates.rsparse_registry_checked(raw, origin)— parse, validate, then deserialize.originnames the file or URL so a failure says which registry is malformed as well as which field.fetch_and_cache_remotevalidates before writing the cache, so a broken marketplace index can no longer replace a working local one.save_registrydelegates tocheck_registry_before_save, which validates the serialized document before writing. This is the single choke point every mutation passes through — install, publish, update, remove — so a malformed entry cannot reach disk and be re-read as a broken registry. It is split out so the check itself is testable without touching the user's registry directory.add_templatevalidates the entry on its own first, so the error names the template rather than its eventual index.check_install_namerejects an unusable derived name before the template is fetched.SecurityReview::findingsis nowOption<u32>.src/commands/template.rsNew
starforge template validate [PATH] [--json]. With no path it checks the registry the CLI would actually load (local, falling back to bundled); with a path it checks a registry file or a single template entry, auto-detected. Errors and warnings print field by field;--jsonemits a machine-readable report. Exits non-zero when invalid.Documentation
templates/README.mdgains a Registry Validation section: the full table of checks, a table of where validation runs and what happens on failure, the forward-compatibility rule, and a migration note for hand-written registries that quotedfindings.TEMPLATE_CONTRIBUTING.mdadds a checklist item and a "Validating your entry" section;TEMPLATE_MARKETPLACE.mddocuments the command.Regression tests
a_valid_registry_document_loads,minimal_valid_registry_passes,all_three_source_kinds_passtests/template_registry_schema.rs,template_schema.rsbundled_registry_satisfies_the_schema,bundled_registry_loads_through_the_checked_loadertests/template_registry_schema.rsa_registry_with_no_templates_loads,nullable_fields_accept_null,unset_timestamps_are_allowed_but_malformed_ones_are_notunknown_fields_do_not_block_loading,unknown_fields_warn_instead_of_failingversion_bounds_may_be_equal,same_template_may_appear_at_different_versionstemplate_schema.rsa_malformed_entry_fails_with_the_offending_field,malformed_semver_is_rejecteda_missing_required_field_fails_before_deserialization,missing_required_field_names_that_fieldevery_bad_field_in_an_entry_is_reported_at_once,several_bad_fields_are_all_reportedinvalid_json_reports_where_it_broketests/template_registry_schema.rsunknown_source_type_is_reported_once,git_source_rejects_a_non_remote_urltemplate_schema.rsduplicate_name_and_version_is_rejected,inverted_cli_version_bounds_are_rejectedtemplate_schema.rsa_name_that_would_escape_the_template_store_is_refused,an_install_name_that_escapes_the_template_store_is_refusedtests/template_registry_schema.rs,templates.rsa_malformed_entry_is_refused_before_it_reaches_disk,a_freshly_installed_entry_is_accepted_for_savingtemplates.rstemplate_validate_accepts_the_bundled_registry,template_validate_reports_the_offending_fieldtests/cli_smoke.rs44 tests added in total.
Testing
The single remaining lib failure is Windows-only; see below.
Notes for reviewers
This branch also repairs pre-existing breakage on
master. At the time this work started (f2298dc)masterdid not compile, was not formatted, and its test suite did not run. Without these fixes there is nothing to review against:Build: duplicated
read_spec_entriesinbindings.rs;rusqlite::Transactionmutability indatabase.rs; duplicatedci_passedfield inaudit.rs; missingPartialEqonComplianceSeverity; unregisteredai_doc_qamodules; the droppedInstalledPlugin::descriptionfield and its two helpers, which the surviving tests inplugins/registry.rsfully specify; threeai_test_assistantgenerator functions referenced by tests but absent; stale test helpers left behind byTemplateEntryfield additions.Rebased onto e1bd085 (Version the plugin ABI and negotiate compatibility #831). That PR independently fixed two of the same items — the
MigrationError/thiserrorproblem (solved there by a hand-writtenDisplayimpl, which I have taken) and thePluginManifesttest helper. Both have been dropped from this branch, soCargo.tomlandCargo.lockare now untouched by this PR.Format:
cargo fmt --all— master failscargo fmt --checkon ~21 files. This is the source of the whitespace-only churn inai_accessibility.rs,ai_plan.rs,ai_project_planner.rs,ai_doc_qa.rs,deploy.rs,bindings_*.rsandplugin_version_compatibility_test.rs.Tests: once the crate compiled, 70 lib tests ran for the first time and 62 failed. 66 failures are now fixed. Each was a real bug, not a test problem — the highlights:
Database::initializecalledis_ok()on aResult<Option<_>>, so a fresh database never recorded its schema version (29 tests).ai_telemetrymatched the first pricing substring, billinggpt-4o-miniatgpt-4orates.test_optimizer::batch_tests_by_profilesilently discarded every non-I/O test;classify_testtreated the universaltest_prefix as a Unit signal, makingTestCategory::Generalunreachable.compliancenormalised the risk score by 500 when only 290 was reachable, soRiskLevel::Criticalcould never be returned.nllisted"show"as a stop word although several intent patterns key on it, and lower-cased contract IDs before checking for an upper-caseCprefix.ai_template_testing's soroban-sdk check read the[package]table, so it fired on every valid template.I have kept these in one commit because they are entangled at file level (for example
template_analytics.rsneeds both thefindingstype change from this issue and a stale-helper fix from master). Happy to split them if you would prefer.Known limitations, all pre-existing and none introduced here:
utils::templates::tests::test_publish_template_versioned_stores_by_versionfails on Windows only. It isolates viaenv::set_var("HOME", …), butdirs::home_dir()on Windows reads the OS known-folder and ignores env vars, so it escapes to the real~/.starforge. It passes on Linux.starforgebinary stack-overflows on startup on Windows —starforge.exe --versionexits0xC00000FDwiththread 'main' has overflowed its stack. I confirmed this is pre-existing by removing the new subcommand and rebuilding. It is clap constructing this crate's very large command tree against Windows' 1 MB main-thread stack; Linux allows 8 MB. Nine integration targets that spawn the binary (cli_smoke,completion_assistant,plugin_compatibility,plugin_lifecycle_e2e,plugin_loading_diagnostics,ai_developer_workflow,deployment_orchestration,hardware_wallet_integration,mutation_testing) therefore cannot be verified on Windows. Worth its own issue — running the CLI on a thread with a larger stack would fix it, but that changes process startup and did not belong in this PR.ai_test_assistant,ai_template_testingandcontract_property_tests(entry-point detection, complexity scoring, test priorities, and the shippedescrow/simple-counterexamples scoring 32 and 42 against the analyzer). These need judgement calls from whoever owns those features about whether the analyzer or the example templates should change. I have diagnoses for each and am glad to follow up.Closes #686