Skip to content

Scaffolder custom-template overlays

Status: APPROVED — GTB-parity gap from the v0.22.0 audit (Phase 1). Custom templates confirmed an in-scope goal (why-rtb.md was silent; now decided: build it). Design is complete; delivery is phased (§8). Decisions resolved 2026-06-23 (§9). Ready for TDD on Phase A.

Resolutions (binding): - Custom templates are a goal, not a non-goal (Q-1). - Git fetch via rtb-vcs Repo::clone (gix, anonymous/inert) (Q-2). - Descriptor file **rtb-template.yaml** (YAML, matches .rtb/manifest.yaml) (Q-3). - Phased: A = local sources + overlay engine + descriptor + security + manifest; B = git fetch + SHA-keyed cache + offline (Q-4).

1. Motivation

The scaffolder ships only built-in minimal/cli presets (rust-embed + minijinja). GTB (v0.18) added external/custom templates: fetch a template from a git repo or local path and overlay it on the generated skeleton, with a descriptor, a cache, and a security model. This is GTB's highest-value scaffolder capability — it's how downstream teams share house-style scaffolding — and RTB needs it.

GTB reference surface: generate project --template <src>@<ref> + gtb template add/update/remove/list; source-spec parsing (local vs git); git clone into an XDG cache keyed by SHA (offline/pin-reproducible, inert clones); local-source SHA-256 fingerprint; overlay layering over the embedded skeleton in manifest order (last-writer-wins); descriptor gtb-template.yaml (contract, description, replaces); a security model (char-class validation, traversal/protected-path denial, per-file caps, restricted overlay func map, secret-free contract data, re-validation on regenerate).

2. Surface

rtb generate project --template <src>[@<ref>] [--template <src2> ...]
rtb template add <src>[@<ref>] [--name <handle>]
rtb template update [<handle>...]      # re-fetch / re-pin
rtb template remove <handle>
rtb template list

--template is repeatable; sources overlay in declaration order. The template verbs edit the manifest templates: block; regenerate is the source of truth (add/remove → manifest edit → re-render), mirroring GTB.

3. Source spec

<src>[@<ref>][--name <handle>] parsed (new template/source.rs): - local — an fs path (absolute/relative); content SHA-256 fingerprinted for change detection. - git — URL / git@… / forge path (owner/repo); optional @<ref> (branch/tag/SHA); resolved to a concrete commit SHA on fetch. - --name overrides the derived handle (else inferred from the repo/dir).

All fields validated via validate.rs char-classes (name/ref/SHA/path) extended with template-source classes; NFC-normalised first.

4. Fetch + cache (Phase B)

  • git: rtb_vcs::Repo::clone(url, dst) — anonymous/inert (gix prepare_clone; no hooks, no submodules), into ProjectDirs::from("dev","",&"rtb").cache_dir()/templates/<host>/<owner>/<repo>@<sha>. SHA-keyed → a pin is reproducible and offline-capable (warm cache hit; cold + offline = clear RepoError-style diagnostic).
  • local: no fetch; read in place, fingerprint recorded.

5. Overlay engine (Phase A)

Each source is rendered over the embedded preset skeleton in manifest (layer) order, last-writer-wins, through the existing minijinja env + escapers + the hash/conflict/// rtb:* marker + .rtb ignore write path (reused, not rebuilt). Override events are logged. A source without a descriptor is a pure overlay (files copied/rendered over the base).

6. Descriptor rtb-template.yaml (Phase A)

Optional at the template root: - contract: (version int; a max enforced — reject unknown-future contracts) — forward-compat guard. - description: — shown in template list. - replaces: — list of GTB-style suppressible aliases the overlay takes over so the embedded skeleton's version is omitted. RTB's suppressible set: gitlab-ci (the cicd-component .gitlab-ci.yml), README, etc. (RTB-owned allowlist — overlays can't suppress arbitrary files). Missing descriptor → pure overlay; malformed → hard error.

7. Security model (Phase A — non-negotiable, §1 engineering-standards)

  • Reuse validate.rs NFC + char-class validation for every source-derived field; length caps.
  • Path traversal denied (.., absolute escapes); overlay output confined to the project tree.
  • Protected output paths — overlays may NOT write security-critical files (.rtb/manifest.yaml, deny.toml, CI secrets, .git/…); a denylist mirroring GTB's protected paths.
  • Per-file byte cap + total-overlay cap (DoS guard).
  • Restricted overlay func map — overlay templates get a reduced minijinja function set + a secret-free TemplateContractData (tool name/description/preset only; never config/env/credentials).
  • Re-validation on every regenerate — invalid manifest template entries are skipped (logged), not fatal.
  • Update docs/development/template-security.md (per CLAUDE.md) with the new fields/classes.

8. Phased delivery

  • Phase A (M) — local sources only: source-spec parser (local), fingerprint, overlay engine, descriptor parser/validator, security gate, manifest templates: block, --template <localpath> + template add/remove/list (local), regenerate integration. Ships the whole engine + security with zero network surface.
  • Phase B (M→L) — git sources: Repo::clone fetch, SHA-keyed cache, offline behaviour, @<ref> resolution, template update re-pin.

9. Resolutions (resolved 2026-06-23)

  • [Q-1] Goal vs non-goal → GOAL. Build it (user-confirmed).
  • [Q-2] Git transport → rtb-vcs (gix) anonymous/inert clone. Already in tree; controllable (no hooks/submodules); avoids a host-git dependency. (Rejected: shell-out to git, HTTPS-tarball — lose pin/ref ergonomics + add surface.)
  • [Q-3] Descriptor → rtb-template.yaml (YAML, matches manifest).
  • [Q-4] Delivery → phased (§8): local engine first, git fetch second.

10. Manifest schema change

Add to Manifest (crates/rtb-cli-bin/src/manifest.rs, currently #[serde(deny_unknown_fields)]):

#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub templates: Vec<TemplateSource>,   // handle, kind(local|git), location, ref, sha/fingerprint, contract

Bumps the manifest contract; regenerate reads it to re-apply overlays.

11. Testing (TDD)

  • Unit: source-spec parser (local vs git, @ref, --name); descriptor parse/validate (contract max, malformed→error); overlay layering order
  • last-writer-wins; security gates (traversal, protected-path, caps, func-map restriction) each reject.
  • E2E (assert_cmd): generate project --template <localdir> overlays + the result cargo checks; template add/list/remove round-trips via manifest; a malicious overlay (../ escape, protected-path write) is rejected; regenerate re-applies + skips an invalidated entry. Phase B: a local-git-fixture clone + SHA-cache hit + offline warm-cache success.

12. Out of scope

  • Template registries / discovery (just direct sources).
  • Non-git remote transports (http tarballs) beyond Phase B git.
  • Running template-provided hooks/scripts (clones stay inert — security).