Skip to content

[2026 Templates] Validate templates against the registry schema before use - #842

Open
solaawojobi00-bit wants to merge 1 commit into
Nanle-code:masterfrom
solaawojobi00-bit:fix/issue-686-template-schema-validation
Open

[2026 Templates] Validate templates against the registry schema before use#842
solaawojobi00-bit wants to merge 1 commit into
Nanle-code:masterfrom
solaawojobi00-bit:fix/issue-686-template-schema-validation

Conversation

@solaawojobi00-bit

@solaawojobi00-bit solaawojobi00-bit commented Aug 27, 2026

Copy link
Copy Markdown

Validate templates against the registry schema before use

Problem

templates/registry.schema.json existed 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 to serde_json, so a malformed template failed late and opaquely.

Scenario Before After
Marketplace registry has a bad version Failed to deserialize remote template registry JSON — no field named; the broken body is already cached, replacing a working cache remote 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 untouched
Entry missing source serde error naming no field templates[0].source: required field is missing
Unknown maintenance value Accepted, fails later during scaffolding templates[4].maintenance: 'archived' is not one of: active, maintained, deprecated, unknown
security_review.findings is a number The bundled registry could not be deserialized at all — the offline fallback was dead Loads; the schema, the shipped registry and the Rust type now agree
Template name is ../../etc/passwd Used verbatim as a directory name under the template store Rejected before anything is fetched
Registry written by a newer CLI n/a Loads; unknown fields reported as warnings, not errors

The findings row is worth calling out: SecurityReview::findings was Option<String> while both the schema and templates/registry.json use a number, so serde_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.json

Rewritten to describe the whole registry document rather than a single entry: a root envelope with version/templates, and $defs for templateEntry, source (a oneOf over the git/local/builtin variants), securityReview and changelogEntry.

Two StarForge extensions carry what plain JSON Schema cannot express without regular expressions:

  • x-formatsemver, 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 sees descripton flagged.

findings is ["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 a ValidationReport of {field, message} pairs.

Rationale for a focused validator rather than a JSON Schema crate: it keeps the dependency tree unchanged, and it lets oneOf failures use the type discriminator so a broken git source reports source.url: required field is missing instead of "does not match any subschema".

Two rules span a whole registry and are checked separately: cli_version_min may not exceed cli_version_max, and no two entries may share a name and version (the same template at different versions is fine).

check_template_name is 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.rs

  • parse_registry_checked(raw, origin) — parse, validate, then deserialize. origin names the file or URL so a failure says which registry is malformed as well as which field.
  • fetch_and_cache_remote validates before writing the cache, so a broken marketplace index can no longer replace a working local one.
  • The fresh-cache read, the stale-cache fallback and the bundled fallback all go through the same checked path.
  • save_registry delegates to check_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_template validates the entry on its own first, so the error names the template rather than its eventual index.
  • check_install_name rejects an unusable derived name before the template is fetched.
  • SecurityReview::findings is now Option<u32>.

src/commands/template.rs

New 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; --json emits a machine-readable report. Exits non-zero when invalid.

Documentation

templates/README.md gains 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 quoted findings. TEMPLATE_CONTRIBUTING.md adds a checklist item and a "Validating your entry" section; TEMPLATE_MARKETPLACE.md documents the command.

Regression tests

Acceptance criterion Test File
Primary flow — a valid registry loads a_valid_registry_document_loads, minimal_valid_registry_passes, all_three_source_kinds_pass tests/template_registry_schema.rs, template_schema.rs
Primary flow — the shipped registry satisfies its own schema and deserializes bundled_registry_satisfies_the_schema, bundled_registry_loads_through_the_checked_loader tests/template_registry_schema.rs
Boundary — empty registry, nullable fields, unset timestamps a_registry_with_no_templates_loads, nullable_fields_accept_null, unset_timestamps_are_allowed_but_malformed_ones_are_not both
Boundary — forward compatibility with unknown fields unknown_fields_do_not_block_loading, unknown_fields_warn_instead_of_failing both
Boundary — equal version bounds; same template at two versions version_bounds_may_be_equal, same_template_may_appear_at_different_versions template_schema.rs
Failure — malformed field is named a_malformed_entry_fails_with_the_offending_field, malformed_semver_is_rejected both
Failure — missing required field a_missing_required_field_fails_before_deserialization, missing_required_field_names_that_field both
Failure — every bad field reported at once every_bad_field_in_an_entry_is_reported_at_once, several_bad_fields_are_all_reported both
Failure — invalid JSON reports line and column invalid_json_reports_where_it_broke tests/template_registry_schema.rs
Failure — unsupported source type / bad git remote unknown_source_type_is_reported_once, git_source_rejects_a_non_remote_url template_schema.rs
Failure — duplicate name+version, inverted CLI bounds duplicate_name_and_version_is_rejected, inverted_cli_version_bounds_are_rejected template_schema.rs
Security — a name that escapes the template store a_name_that_would_escape_the_template_store_is_refused, an_install_name_that_escapes_the_template_store_is_refused tests/template_registry_schema.rs, templates.rs
Loader — nothing malformed reaches disk a_malformed_entry_is_refused_before_it_reaches_disk, a_freshly_installed_entry_is_accepted_for_saving templates.rs
CLI — end to end template_validate_accepts_the_bundled_registry, template_validate_reports_the_offending_field tests/cli_smoke.rs

44 tests added in total.

Testing

$ cargo test --lib -- --test-threads=1 utils::template_schema
test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 1336 filtered out

$ cargo test --test template_registry_schema
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

$ cargo fmt --all --check
FMT: clean

$ cargo clippy --all-features --locked -- -D warnings
CLIPPY: clean

$ cargo test --lib -- --test-threads=1
test result: FAILED. 1365 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

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) master did 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_entries in bindings.rs; rusqlite::Transaction mutability in database.rs; duplicated ci_passed field in audit.rs; missing PartialEq on ComplianceSeverity; unregistered ai_doc_qa modules; the dropped InstalledPlugin::description field and its two helpers, which the surviving tests in plugins/registry.rs fully specify; three ai_test_assistant generator functions referenced by tests but absent; stale test helpers left behind by TemplateEntry field additions.

    Rebased onto e1bd085 (Version the plugin ABI and negotiate compatibility #831). That PR independently fixed two of the same items — the MigrationError/thiserror problem (solved there by a hand-written Display impl, which I have taken) and the PluginManifest test helper. Both have been dropped from this branch, so Cargo.toml and Cargo.lock are now untouched by this PR.

  • Format: cargo fmt --all — master fails cargo fmt --check on ~21 files. This is the source of the whitespace-only churn in ai_accessibility.rs, ai_plan.rs, ai_project_planner.rs, ai_doc_qa.rs, deploy.rs, bindings_*.rs and plugin_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::initialize called is_ok() on a Result<Option<_>>, so a fresh database never recorded its schema version (29 tests).
    • ai_telemetry matched the first pricing substring, billing gpt-4o-mini at gpt-4o rates.
    • test_optimizer::batch_tests_by_profile silently discarded every non-I/O test; classify_test treated the universal test_ prefix as a Unit signal, making TestCategory::General unreachable.
    • compliance normalised the risk score by 500 when only 290 was reachable, so RiskLevel::Critical could never be returned.
    • nl listed "show" as a stop word although several intent patterns key on it, and lower-cased contract IDs before checking for an upper-case C prefix.
    • 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.rs needs both the findings type 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:

  1. utils::templates::tests::test_publish_template_versioned_stores_by_version fails on Windows only. It isolates via env::set_var("HOME", …), but dirs::home_dir() on Windows reads the OS known-folder and ignores env vars, so it escapes to the real ~/.starforge. It passes on Linux.
  2. The starforge binary stack-overflows on startup on Windowsstarforge.exe --version exits 0xC00000FD with thread '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.
  3. Eleven failures remain in ai_test_assistant, ai_template_testing and contract_property_tests (entry-point detection, complexity scoring, test priorities, and the shipped escrow/simple-counter examples 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

…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
solaawojobi00-bit force-pushed the fix/issue-686-template-schema-validation branch from 20df195 to 7d575a2 Compare August 27, 2026 04:07
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

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.

[2026 Templates] Validate templates against the registry schema before use

1 participant