Role Repo Pinning: Design
Status: Open — design proposal, not yet implemented
This is the design record for Reproducibility and provenance pinning. It captures the problem framing, schema sketch, runtime-behaviour sketch, and open questions so the roadmap item itself can stay a lean status/remaining-work tracker.
Problem
When jackin❯ loads a role today (agent-smith, the-architect, or any third-party role), it clones the role repo and tracks whatever sits at the tip of the default branch. Every jackin load fetches and merges, silently picking up whatever the upstream pushed since the last run.
That has three concrete consequences:
- Behaviour drifts between runs without operator awareness. Yesterday
the-architectbehaved one way; today the upstream merged a refactor and now it behaves another. The operator did not request, see, or approve the change. - Past runs are not reproducible. If something worked Tuesday and broke Thursday, nothing on disk records which role-repo commit Tuesday's run actually used. The session state, the launch summary, and the config file all describe "the role" but not "this specific snapshot of the role".
- Trust is granted to a moving target. The operator may have marked a role trusted last month after reading the code at one commit. The remote has since moved 40 commits. The trust flag still says trusted, but it is now applied to code that has never been reviewed.
Why it matters
This is the foundation for treating role repos as supply-chain artefacts rather than "always-latest from the internet". Without pinning, the audit trail in the operator config does not capture what was actually run — only which repo it came from. That is fine for prototyping but rules out reproducing a session, bisecting an agent regression, or trusting a third-party role beyond a single read-through.
Proposed approach
Pin role repos to an immutable commit SHA on first resolve, prefer human-readable tags when present, and require an explicit jackin load --update to advance the pin (which also resets trust so the operator re-confirms against the new code). This is the "Option 3 (hybrid)" sketch from the original brainstorm.
Schema sketch
Add two optional fields to the role-source entry in config.toml:
[roles.agent-smith]
git = "https://github.com/jackin-project/jackin-agent-smith.git"
commit = "a1b2c3d4e5f6…" # resolved SHA, runtime-written
version = "v1.2.3" # tag at HEAD, when one exists
trusted = trueBoth fields are optional and additive. A role that has never been resolved omits both; the runtime fills them in after the first successful clone. commit is the source of truth for what gets checked out; version is purely cosmetic (launch summary line).
Runtime behaviour sketch
- First resolve (no
commitrecorded): clone the repo, then capture HEAD SHA viagit rev-parse HEADand any tag exactly at HEAD viagit describe --tags --exact-match HEAD(best-effort; absence is fine). Persist both back into the operator'sconfig.toml. - Subsequent loads (with
commitset):git fetch(no merge), thengit checkout <commit>in detached-HEAD mode. No auto-pull, no auto-advance. jackin load --update(new flag):git fetch, then re-pin. Resolution rule: if the repo has any tag, pick the highest semver-sortedv*tag; otherwise re-pin to the default branch's HEAD. Reset trust so the operator must re-confirm trust against the new code.--role-branch <name>(existing flag) keeps its current bypass behaviour — does not write a pin, used for ad-hoc PR testing.- Built-in roles follow the same rules; the only special case is that trust is asserted by the binary on every sync.
- Launch summary prints
Role: <key> @ <version-or-short-sha>so the operator sees the resolved version on every run.
Config schema migration
Per the project's pre-release schema rule (see AGENTS.md), any change to a serde-bearing config struct requires a version bump and a migration registry entry, even when the change is purely additive. This work would bump to the next config schema version, add a no-op migration step if the fields are purely additive, ship a fixture from the previous version, and re-bake older fixtures so their migration chains end at that next version. Existing operator config files keep parsing without modification because the new fields would be Option<String> with serde defaults.
Open design questions
- Is the
--updateresolution rule "highestv*tag, fall back to default branch HEAD" the right default? Or should the operator have to opt into tag-following with a separate flag, with the default being "advance to default-branch HEAD"? - Should
--updatealso accept an explicit target — e.g.jackin load --update v1.3.0to pin to a specific tag, orjackin load --update <sha>to pin to a specific commit? - Should re-pinning always reset trust, or only when the new commit is outside the previously-trusted commit's ancestry (i.e. a force-push or unrelated commit)? The current sketch is conservative ("any movement re-prompts trust"); a finer-grained rule is possible but adds complexity.
- For built-in roles, should the binary ship a default pin (so a fresh install lands on a known-good commit rather than HEAD), or continue to resolve on first load? Shipping defaults adds release-engineering work but removes the first-launch network surprise.
Expected code touchpoints
crates/jackin-config/src/schema.rs— addcommit,versionfields toRoleSource.crates/jackin-config/src/migrations.rs— version bump and registry step.crates/jackin-config/src/app_config/roles.rs—sync_builtin_agentsandresolve_agent_sourcemust update fields in place rather than overwriting the wholeRoleSource, so a re-pinned built-in survives a binary upgrade.crates/jackin-runtime/src/runtime/repo_cache.rs— capture-pin helper, fetch+checkout-detached path for pinned commits,--updatepath, persist callback into config.crates/jackin-runtime/src/runtime/launch.rs— wire the--updateflag, include resolved version/SHA in the launch summary.crates/jackin-manifest/src/manifest.rs— version/provenance metadata.