Skip to content

Add agnostic source resolution for private template families#19

Closed
next-devin wants to merge 6 commits into
mainfrom
arjuna-family
Closed

Add agnostic source resolution for private template families#19
next-devin wants to merge 6 commits into
mainfrom
arjuna-family

Conversation

@next-devin

@next-devin next-devin commented Jun 15, 2026

Copy link
Copy Markdown

What

campaign-init previously fetched starter-template source from one hardcoded public repo, so it couldn't scaffold template families hosted elsewhere (e.g. an outside, access-controlled repo). This adds a source-agnostic resolver + authenticated fetch so any repo/ref/subtree can be a source — while keeping page-kit a generic build tool with zero per-family knowledge.

Design (agnostic by construction)

  • lib/template-source.js (new) — resolveTemplateSource(overrides) overlays caller-supplied {repo, ref, source_path, private} on the public starter-templates default. It names no families and no private repos. Privateness is an explicit caller decision, never inferred from a slug.
  • lib/actions/init.js — new flags: --source-repo, --source-ref, --source-path, --private.
    • Default (no flags): public starter-templates, anonymous — unchanged behavior.
    • --private: authenticated tarball fetch (GITHUB_TOKEN/GH_TOKEN or gh auth token); skips the public picker; requires --template. Token is dropped on the signed redirect, which is pinned to https + a GitHub-owned host.
    • Missing token → exit 5 (MISSING_INPUT, code MISSING_GITHUB_TOKEN); no fetch attempted.
    • A --private source's display name is refined from the source repo's campaigns.json when --name is omitted.

The opinionated part — which family lives in which private repo — stays in the orchestrating layer (campaigns-os / the operator's invocation), not in page-kit.

Verification

  • --template <slug> --source-repo <owner/repo> --private → authenticated fetch, files extracted, name refined from campaigns.json.
  • --private without a token → exits 5.
  • Public --template olympus with no source flags → resolves anonymously (unchanged).
  • 204 tests green (resolver + redirect host-pinning unit tests; existing init tests unchanged).

Pairs with campaigns-os#101 (the Arjuna recognition contracts — repo-agnostic).

campaign-init fetched starter-template source from a single hardcoded
public repo, so it could not scaffold private template families.

- Add lib/template-source-registry.js: the single source of truth mapping a
  family slug to its source repo/ref/path. Public families resolve to the
  public starter-templates repo (anonymous); private families (e.g. arjuna →
  the Adsbranded private template repo) resolve to a private repo and are
  marked private.
- init.js resolves repo/templates/campaigns/tarball URLs per family via the
  registry. Private families are fetched over an authenticated GitHub tarball
  (GITHUB_TOKEN/GH_TOKEN or `gh auth token`); the token is dropped on the
  signed codeload redirect so it never leaks cross-origin. Public families
  stay anonymous. A private family requested without a token fails with a
  clear, actionable error.
- Private families are intentionally absent from any public templates.json,
  so they never surface in the picker; they resolve only by explicit slug.

Verified end-to-end: arjuna pulls 210 files (pages + _includes + _layouts +
assets) authenticated; olympus still resolves anonymously; missing token
yields MISSING_GITHUB_TOKEN. Full test suite green (201 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@next-devin next-devin requested a review from alexphelps June 15, 2026 10:53
@next-devin next-devin marked this pull request as ready for review June 15, 2026 12:57
Comment thread lib/actions/init.js Outdated
Comment thread lib/template-source-registry.js Outdated
Comment thread lib/template-source-registry.js Outdated
Comment thread lib/template-source-registry.js Outdated
Comment thread lib/actions/init.js Outdated
Comment thread lib/actions/init.js
@kilo-code-bot

kilo-code-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

The previous WARNING (init.js:216path.join(tmpDir, repoRoot, sourcePath, slug) using platform-native join while the validator normalized with path.posix.normalize, causing src\foo to pass on Windows and be split on \\ by path.win32.join) is fully resolved by commit 0f9656a:

  1. lib/template-source.js:63 — added source_path.includes('\\') to the rejection predicate, so a backslash-containing path is now rejected at the trust boundary on every platform, before any URL is built or tarball is fetched.
  2. lib/actions/init.js:220-221 — split the join into two steps: path.posix.join(sourcePath, slug) produces a POSIX subtree, then path.join(tmpDir, repoRoot, subtree) treats that whole string as a single native segment. The contract is now truly platform-independent.
  3. test/template-source.test.js:68-70 — the existing traversal/absolute/backslash test ('src\\foo', 'src\\..\\etc') now exercises the new predicate and locks in the cross-platform contract.

The trust-boundary contract ("the resolver validates shape; the consumer can trust it") is now consistently POSIX end-to-end.

Files Reviewed (3 files)
  • lib/actions/init.js - 0 issues (cross-platform WARNING resolved)
  • lib/template-source.js - 0 issues (backslash rejection added)
  • test/template-source.test.js - 0 issues (test for 'src\\\\foo' already covers the new predicate)
Previous Review Summaries (5 snapshots, latest commit 92e7a6b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 92e7a6b)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0

The trust-boundary validation introduced in commit 92e7a6b is a real improvement: assertValidSource rejects traversal/absolute/control-char/odd-char inputs for repo, ref, and source_path before any URL is built or any tarball is fetched, and init.js now surfaces the throw as a clean EXIT.INVALID_INPUT (6) / INVALID_SOURCE rather than a confusing upstream-fetch error. The two WARNINGs from the previous review (init.js:670 for unvalidated --source-* flags feeding path.join, and template-source.js:42 for pickString accepting any non-empty string) are both resolved by this commit — the contract is now "the resolver validates shape; the consumer can trust it".

One cross-platform consistency gap remains and is flagged inline below.

Issue Details (click to expand)

CRITICAL

(none)

WARNING

File Line Issue
lib/actions/init.js 216 path.join(tmpDir, repoRoot, sourcePath, slug) uses platform-native path.join, but the validator normalizes source_path with path.posix.normalize (POSIX-only). On Windows a source_path like src\\foo passes the validator unchanged and is then split on \\ by path.win32.join — same input, different resolved subtree per platform.

SUGGESTION

(none)

Other Observations (not in diff)

None — every pre-existing concern in lib/actions/init.js and lib/template-source.js was either resolved by this PR or carried forward unchanged.

Files Reviewed (4 files)
  • lib/actions/init.js - 1 issue
  • lib/template-source.js - 0 issues (validation added in 92e7a6b resolves prior WARNINGs)
  • test/init.test.js - 0 issues
  • test/template-source.test.js - 0 issues (not directly readable; reviewed via git show 92e7a6b:test/template-source.test.js)

Fix these issues in Kilo Cloud

Previous review (commit bf53ba7)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

The refactor successfully removes per-family knowledge from page-kit: lib/template-source-registry.js is gone, replaced by lib/template-source.js which overlays caller-supplied overrides on the public default. The old PRIVATE_FAMILY_IN_PUBLIC_MANIFEST invariant check is correctly dropped — under the new model privateness is an explicit caller decision, not an inferred one, so there is no longer a registry to misconfigure. The redirect host pinning in isTrustedGithubRedirect is unchanged from e8ed87f and remains solid. New tests in test/template-source.test.js cover the overlay mechanics well.

The two issues below are both about the new trust boundary: caller-supplied --source-repo / --source-ref / --source-path are passed into resolveTemplateSource and ultimately into URL construction and tarball extraction with no shape validation, and pickString only enforces "non-empty".

Issue Details (click to expand)

WARNING

File Line Issue
lib/actions/init.js 670 --source-repo / --source-path from flags are passed straight into resolveTemplateSource with no shape validation; path.join normalization in extractTemplate allows source_path containing .. segments to escape the intended tarball subtree.
lib/template-source.js 42 pickString accepts any non-empty string for o.repo / o.ref / o.source_path — including values with embedded path-traversal segments, query strings, or non-owner/repo shapes. This module is the trust boundary and should self-contain its invariants.
Files Reviewed (5 files)
  • lib/actions/init.js - 1 issue
  • lib/template-source.js - 1 issue
  • test/template-source.test.js - 0 issues
  • lib/template-source-registry.js - 0 issues (deleted)
  • test/template-source-registry.test.js - 0 issues (deleted)

Fix these issues in Kilo Cloud

Previous review (commit e8ed87f)

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Both previously flagged issues on this branch are now resolved:

  • lib/actions/init.js:712 — the misleading defense-in-depth re-resolve is gone; the public-manifest branch now asserts an invariant (PRIVATE_FAMILY_IN_PUBLIC_MANIFESTEXIT.INVALID_INPUT) instead of letting Bearer null slip through. The associated pickedTemplate and token paths are no longer at risk.
  • lib/actions/init.js:130 — the new isTrustedGithubRedirect (lib/actions/init.js:116) pins redirects to github.com, *.github.com, and *.githubusercontent.com, properly handling invalid URLs (new URL() throws), case-insensitive host matching, and suffix-look-alike hosts (github.com.attacker.io is correctly rejected). Covered by test/init.test.js:33-46.
Files Reviewed (2 files)
  • lib/actions/init.js - 0 issues
  • test/init.test.js - 0 issues

Previous review (commit a7ad921)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 2
Issue Details (click to expand)

SUGGESTION

File Line Issue
lib/actions/init.js 712 Defense-in-depth re-resolve (source = resolveTemplateSource(templateSlug) after picker) leaves pickedTemplate from the public manifest and token null if the picked slug ever maps to a private family. Subsequent fetchBufferAuthed(..., null) sends Bearer null. Either drop the re-resolve with an invariant comment, or rebuild pickedTemplate + call resolveGithubToken() on the re-resolve.
lib/actions/init.js 130 New redirect scheme check (/^https:\/\//i) closes the http/downgrade gap but still follows redirects to any HTTPS host. A compromised or malicious api.github.com redirector could send the tarball fetch to https://attacker.example. Pin to the two legitimate hosts (api.github.com, codeload.github.com) for symmetry with the new tarball_url_authenticated vs _anonymous split.
Previously Reported — Resolved in a7ad921 (5)
File Line Resolution
lib/actions/init.js (was 642) Exit-code mismatch for missing token: now EXIT.MISSING_INPUT (5), code: 'MISSING_GITHUB_TOKEN' retained for caller disambiguation.
lib/template-source-registry.js 30 source_path now threaded into extractTemplate(..., source.source_path) (default 'src'); per-family subtree overrides take effect.
lib/template-source-registry.js 38 Same fix as above — arjuna source_path override now honored.
lib/template-source-registry.js 65 Renamed to tarball_url_authenticated (symmetric with tarball_url_anonymous); test + init.js usage updated.
lib/actions/init.js 666 Refine display name block (lines 799-807) upgrades a private family's finalName to the registry name when no --name was passed; comment no longer over-promises.
Previously Reported — Intentionally Kept (1)
File Line Status
lib/template-source-registry.js 74 isPrivateFamily retained as part of the registry's public API for campaigns-os (per author reply on the previous review). init.js uses the already-resolved source.private.
Files Reviewed (3 files)
  • lib/actions/init.js - 2 new issues
  • lib/template-source-registry.js - 0 new issues (all 3 prior issues resolved)
  • test/template-source-registry.test.js - 0 new issues

Fix these issues in Kilo Cloud

Previous review (commit 134b6bd)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
lib/actions/init.js 642 Missing-token failure uses EXIT.UPSTREAM_FETCH_FAILED (4) but the help text reserves exit 4 for "upstream fetch failed"; the inner code is MISSING_GITHUB_TOKEN and the actual problem is missing required input (contract says exit 5). Scripted callers can't distinguish "no token" from "GitHub down" by exit code alone.
lib/template-source-registry.js 30 source_path: 'src/' is declared on every source descriptor and returned by resolveTemplateSource, but extractTemplate (lib/actions/init.js:179) hardcodes src/<slug>/ and never reads it. A future family with a different subtree will silently break extraction.
lib/template-source-registry.js 38 Same source_path issue in the arjuna override — declared but unused, so per-family subtree overrides can't take effect.

SUGGESTION

File Line Issue
lib/template-source-registry.js 65 tarball_url is documented as the private-only path but is also exposed on the public descriptor, and is never used for public families (which always prefer tarball_url_anonymous). The dual-field asymmetry invites a future caller to use the wrong one. Consider renaming to tarball_url_authenticated for clarity.
lib/actions/init.js 649 Synthetic pickedTemplate for a private family uses the slug as the display name, and step 8's registry fetch never updates pickedTemplate.name — so a --non-interactive --template arjuna run produces a campaign literally named arjuna unless --name is passed. Comment promises a refinement that doesn't happen.
lib/actions/init.js 978 isPrivateFamily is exported from both the registry and init.js but never called in main(). Either drop the export or use it (e.g. to short-circuit the public templates.json fetch and skip the picker when an explicit --template slug maps to a private family).
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
lib/actions/init.js 169-188 extractTemplate hardcodes the subtree path as path.join(tmpDir, repoRoot, 'src', slug). The new registry field source_path exists precisely to make this configurable but is never consulted. Threading source.source_path into extractTemplate is a prerequisite for any future family with a non-src/ layout.
lib/actions/init.js 149-161 resolveGithubToken() shells out to gh auth token and trims the result. The token then lives in the main() closure for the rest of the run. No current code path logs or serializes it, but the result object emitted via emitJson and the spinner text path are both potential future leak vectors if anyone adds debug logging — worth a comment to keep token out of structured outputs.
lib/actions/init.js 82-100, 113-139 fetchBuffer and fetchBufferAuthed follow any res.headers.location redirect without scheme or host validation. For a CLI tool with no inbound network risk, the practical impact is low, but restricting redirects to https:// (and ideally the same host) would harden against an upstream serving a malicious redirect.
Files Reviewed (3 files)
  • lib/actions/init.js - 3 issues
  • lib/template-source-registry.js - 3 issues
  • test/template-source-registry.test.js - 0 issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · 150,943 tokens

Kilo Code review on #19:
- init.js: missing GitHub token now exits 5 (MISSING_INPUT) instead of 4
  (UPSTREAM_FETCH_FAILED) — no fetch was attempted, it's a missing precondition;
  the MISSING_GITHUB_TOKEN code string stays so callers can tell it apart from a
  missing --template.
- registry+init.js: thread source_path through extractTemplate (default 'src')
  so the registry field is honored — a future family with a non-src/ layout
  resolves instead of silently failing. Was declared but unused.
- registry: rename tarball_url -> tarball_url_authenticated so the authed
  (api.github.com) vs anonymous (codeload) paths are unambiguous and a caller
  can't accidentally make an authenticated request for a public family.
- init.js: a private family's synthetic descriptor defaulted its display name to
  the slug; now upgraded to the private repo's registry name after the campaigns
  fetch when no --name was passed (verified: --template arjuna -> "Arjuna", slug
  unchanged). Removes the over-promising comment.
- Hardening: fetchBufferAuthed refuses non-https redirects; note that the token
  must never be serialized into result/JSON/spinner output.

201 tests green; verified live (private fetch extracts 210 files, no-token exits 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread lib/actions/init.js Outdated
Comment thread lib/actions/init.js Outdated
next-devin and others added 2 commits June 15, 2026 20:16
Kilo Code review (round 3) on #19:
- init.js: removed the "defense in depth" re-resolve in the public picker
  branch. A slug from the public manifest always resolves to the public source,
  so the re-resolve could only ever (in a misconfiguration) flip `source` to
  private while `token` stayed null — leading to a `Bearer null` fetch. Replaced
  with an explicit invariant: if a public-manifest slug resolves private, fail
  loudly (PRIVATE_FAMILY_IN_PUBLIC_MANIFEST) instead of attempting an
  unauthenticated private fetch.
- init.js: fetchBufferAuthed now pins redirect targets to https AND a
  GitHub-owned host (github.com / *.github.com / *.githubusercontent.com) via a
  new isTrustedGithubRedirect() helper, not just the https scheme. Hardens the
  authed tarball path against a compromised/malicious upstream redirector. Unit
  tested (allows codeload/api/objects/github.com; rejects downgrade, foreign
  hosts, and suffix look-alikes like github.com.attacker.io).

203 tests green; verified the real api.github.com -> codeload redirect still
extracts the private tarball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refactor per review: page-kit should be a generic build tool, not carry a
hardcoded map of which family lives in which (private) repo.

- Replace lib/template-source-registry.js (which hardcoded a family→repo map,
  incl. a specific private family) with lib/template-source.js: a pure resolver
  `resolveTemplateSource(overrides)` over the public starter-templates default.
  No family names, no private-repo names — page-kit holds zero per-family
  source knowledge.
- init.js: add --source-repo / --source-ref / --source-path / --private flags.
  The caller (operator, or an orchestrator like campaigns-os) supplies the
  source; --private drives the authenticated, unlisted fetch path. Privateness
  is an explicit caller decision, never inferred from a slug.
- Remove the now-moot family-map artifacts: the picker re-resolve and the
  PRIVATE_FAMILY_IN_PUBLIC_MANIFEST invariant (a public-branch source can no
  longer be private), and isPrivateFamily/FAMILY_SOURCES.
- Keep all the prior mechanism hardening (exit 5 for missing token, source_path
  threading, authed-redirect host pin, https-only redirects, name refinement).

204 tests green. Verified: `--template arjuna --source-repo <repo> --private`
extracts 210 files (name refined from campaigns.json); --private without a token
exits 5; public `--template olympus` still resolves anonymously with no flags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@next-devin next-devin changed the title Resolve template source per family; support private families Add agnostic source resolution for private template families Jun 15, 2026
Comment thread lib/actions/init.js Outdated
Comment thread lib/template-source.js
Review (kilo-code-bot, 2 WARNINGs): the new --source-repo/--source-ref/
--source-path flags flowed into URL construction and tarball extraction with no
shape validation. Notably --source-path "../../" could let extractTemplate
path.join its way out of the extracted tarball subtree (then fs.cpSync it).

resolveTemplateSource now validates shape (defense in depth — covers programmatic
callers, not just the CLI) and throws INVALID_SOURCE on:
- repo not matching strict "owner/repo" (rejects traversal, query strings, odd chars)
- ref with ".."/leading/trailing slash/odd chars (slashed refs like feature/foo
  and refs/heads/x still allowed)
- source_path that is absolute, "~", or contains ".." (relative subtree only)

init.js surfaces the throw as a clean EXIT.INVALID_INPUT (6) / INVALID_SOURCE
instead of a confusing upstream-fetch error. 208 tests green; verified a bad
--source-path exits 6 and the real private source still validates + extracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread lib/actions/init.js Outdated
Review (kilo-code-bot, WARNING): the validator normalized source_path with
path.posix but extractTemplate joined it with native path.join, so on Windows a
backslash source_path would pass POSIX validation yet be reinterpreted as a
separator by path.win32.join — same input, different result per platform.

- template-source.js: reject backslashes in source_path so it is strictly a
  POSIX subtree (forward slashes only) on every platform.
- init.js extractTemplate: build the source-relative subtree with path.posix.join
  (matches the validator's semantics), then join onto the native tmp dir for the
  real filesystem path. The trust-boundary contract is now platform-independent.

(Also removed a stray NUL byte that had crept into a source_path test fixture.)
208 tests green, incl. backslash-rejection cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@next-devin

Copy link
Copy Markdown
Author

Closing in favor of #20, which carries the identical net change as a single clean commit. #19's history ended up build-then-refactor (a hardcoded-family registry, then refactored agnostic) across several review rounds; #20 presents only the final source-agnostic design. All review feedback from this PR is incorporated there.

@next-devin next-devin closed this Jun 15, 2026
@next-devin next-devin deleted the arjuna-family branch June 16, 2026 06:55
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.

1 participant