# Role Repo Pinning: Design (https://jackin.tailrocks.com/reference/research/security/supply-chain/role-repo-pinning-design/)



**Status**: Open — design proposal, not yet implemented

This is the design record for [Reproducibility and provenance pinning](/roadmap/reproducibility-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 [#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:

1. **Behaviour drifts between runs without operator awareness.** Yesterday `the-architect` behaved one way; today the upstream merged a refactor and now it behaves another. The operator did not request, see, or approve the change.
2. **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".
3. **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 [#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 [#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 [#schema-sketch]

Add two optional fields to the role-source entry in `config.toml`:

```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 = true
```

Both 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 [#runtime-behaviour-sketch]

* **First resolve** (no `commit` recorded): clone the repo, then capture HEAD SHA via `git rev-parse HEAD` and any tag exactly at HEAD via `git describe --tags --exact-match HEAD` (best-effort; absence is fine). Persist both back into the operator's `config.toml`.
* **Subsequent loads** (with `commit` set): `git fetch` (no merge), then `git 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-sorted `v*` 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 [#config-schema-migration]

Per the project's pre-release schema rule (see <RepoFile path="AGENTS.md">AGENTS.md</RepoFile>), 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 [#open-design-questions]

* Is the `--update` resolution rule "highest `v*` 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 `--update` also accept an explicit target — e.g. `jackin load --update v1.3.0` to pin to a specific tag, or `jackin 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 [#expected-code-touchpoints]

* <RepoFile path="crates/jackin-config/src/schema.rs">crates/jackin-config/src/schema.rs</RepoFile> — add `commit`, `version` fields to `RoleSource`.
* <RepoFile path="crates/jackin-config/src/migrations.rs">crates/jackin-config/src/migrations.rs</RepoFile> — version bump and registry step.
* <RepoFile path="crates/jackin-config/src/app_config/roles.rs">crates/jackin-config/src/app\_config/roles.rs</RepoFile> — `sync_builtin_agents` and `resolve_agent_source` must update fields in place rather than overwriting the whole `RoleSource`, so a re-pinned built-in survives a binary upgrade.
* <RepoFile path="crates/jackin-runtime/src/runtime/repo_cache.rs">crates/jackin-runtime/src/runtime/repo\_cache.rs</RepoFile> — capture-pin helper, fetch+checkout-detached path for pinned commits, `--update` path, persist callback into config.
* <RepoFile path="crates/jackin-runtime/src/runtime/launch.rs">crates/jackin-runtime/src/runtime/launch.rs</RepoFile> — wire the `--update` flag, include resolved version/SHA in the launch summary.
* <RepoFile path="crates/jackin-manifest/src/manifest.rs">crates/jackin-manifest/src/manifest.rs</RepoFile> — version/provenance metadata.
