Skip to content

Reproducibility and Provenance Pinning

Status: Open — agent brainstorm, not yet reviewed by the operator

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 runs git pull and silently picks 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 = true last month after reading the code at one commit. The remote has since moved 40 commits. The trust flag still says true, but it is now applied to code that has never been reviewed.

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 ~/.config/jackin/config.toml 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 (brainstorm — pending operator approval)

Section titled “Proposed approach (brainstorm — pending operator approval)”

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; the open questions in the previous draft of this page are folded into the implementation sketch below.

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 = 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).

  • 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 trusted = false 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 trusted = true 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.

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 CURRENT_CONFIG_VERSION from v1alpha2 to v1alpha3, add a no-op MigrationStep, ship a new from-v1alpha2/ fixture, and re-bake the existing from-legacy/ and from-v1alpha1/ fixtures so their migration chains end at v1alpha3. Existing operator config files keep parsing without modification because the new fields are Option<String> with serde defaults.

  • 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 trusted = false, 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.

Code:

  • src/config/mod.rs — add commit, version fields to RoleSource.
  • src/config/migrations.rs — version bump and registry step.
  • src/config/roles.rssync_builtin_agents must update fields in place rather than overwriting the whole RoleSource, so a re-pinned built-in survives a binary upgrade.
  • src/runtime/repo_cache.rs — capture-pin helper, fetch+checkout-detached path for pinned commits, --update path, persist callback into config.
  • src/runtime/launch.rs — wire the --update flag, include resolved version/SHA in the launch summary.

Docs:

  • This page — convert from brainstorm to design proposal once approved, then to “Resolved” once shipped.
  • A new operator-facing note for --update on the jackin load page.
  • A new timeline entry on the schema-versions reference page for v1alpha3.