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 (gixprepare_clone; no hooks, no submodules), intoProjectDirs::from("dev","",&"rtb").cache_dir()/templates/<host>/<owner>/<repo>@<sha>. SHA-keyed → a pin is reproducible and offline-capable (warm cache hit; cold + offline = clearRepoError-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.rsNFC + 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::clonefetch, SHA-keyed cache, offline behaviour,@<ref>resolution,template updatere-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-gitdependency. (Rejected: shell-out togit, 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 resultcargo checks;template add/list/removeround-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).