Skip to content

feat(ci): weekly cooldown-gated dependency updates - #13

Closed
JonZeolla wants to merge 2 commits into
mainfrom
automated-dependency-updates
Closed

feat(ci): weekly cooldown-gated dependency updates#13
JonZeolla wants to merge 2 commits into
mainfrom
automated-dependency-updates

Conversation

@JonZeolla

@JonZeolla JonZeolla commented Sep 3, 2026

Copy link
Copy Markdown
Member

Adds weekly automation that refreshes every pinned dependency here — container image tags, uv.lock files, and GitHub Action SHAs — behind a supply-chain cooldown and an upstream-provenance check, plus the first run of it.

Two commits, reviewable separately: the tooling, then the bumps it produced.

What it guarantees

A 7-day cooldown on every adopted version, so a compromised upstream release has time to be noticed by someone else first (context). Candidates are walked newest-first and the first past the window wins — a 3-day-old release is skipped while an 8-day-old one is still picked up, rather than the pin stalling. Python deps get the same window declaratively via exclude-newer = "7 days" in each [tool.uv], which binds a reader's uv sync too.

Provenance for GitHub-sourced versions. The tag must exist as a ref in the repo it claims to come from — a fork's tags never appear in the parent's refs, which is what the imposter-commit attack lacks, since it references a bare SHA with no ref behind it.

Actions hash-pinned and verified twice. pinact run -u re-pins past its own cooldown, --check --verify-comment confirms each version comment still names the pinned SHA, and verify_action_pins.py confirms that SHA is reachable from a branch upstream — the check pinact has no equivalent for. It also fails any uses: that isn't SHA-pinned, standing in for a per-PR zizmor gate.

supply_chain/ is a trimmed vendoring of zenable_monorepo from next-gen-governance, which is private while this repo is public. ~700 lines rather than ~1,300: only the two sources these pins need, with the security-relevant logic kept close to upstream so a fix there ports as a readable diff.

The first run

Pin Result
Keycloak (a2a, agent-identity) 26.7.1 → 26.7.2. 26.7.3 exists but was 3 days old — correctly held.
Jaeger all-in-one 1.68.0 → 1.76.0, held to the 1.x line: Jaeger v2 is a different image with a different CLI.
agentgateway held at v1.4.1 — see below.
5 × uv.lock refreshed under exclude-newer

Every bump was validated by running the lab, not by checking that the container starts:

  • a2a — Keycloak 26.7.2 healthy, end-to-end.txt regenerated byte-identical
  • agent-identity22/22 negative tests "behaved as documented"; sdjwt-walkthrough.txt byte-identical
  • agentgateway-mcp — drove traffic through the gateway on Jaeger 1.76.0; span list matches what the lab documents, /api/services["agentgateway"]
  • mcp-get-started — builds and runs on fastmcp 4.0.2; slow_shout task tool present
  • ema-mcp7/7 security negatives

evidence/ came from each lab's own capture-evidence.sh. mcp-get-started's is left alone: it was captured on the e2e harness and already records 4.0.2, so overwriting it with laptop output would be a downgrade.

agentgateway v1.5.0 is deliberately not taken

It has cleared the cooldown, and the automation will propose it again next run. Read that PR carefully — v1.5.0 renames the trace spans this lab teaches:

documented (v1.4.1) v1.5.0 emits
POST /* (gone)
delete_session, get_stream DELETE get-started, GET get-started
tools/call get-started tools/call get-started_add, ..._shout

That invalidates two recorded transcripts, the sentence about a tool call naming its target, and the instruction to click a POST /* row to find the parent span — all in agentgateway-mcp.mdx. Lab-content work for an author, not a version bump. update-pins.sh records the reasoning.

fastmcp

Keeps the fastmcp 4 line from #11 and raises the floor to >=4.0.2,<5, resolving against current PyPI rather than the 7-day window. The lab teaches the SEP-2663 tasks extension, which only exists in fastmcp 4 — released days ago — so a cooldown on the lab's own subject makes the lock unsatisfiable, not safe. The [tasks] extra stays: server.py does from fastmcp_tasks import TasksExtension, and base fastmcp pulls only fastmcp-slim[client,server].

READMEs come from next-gen-governance

Every labs/*/README.md is generated from the .mdx in next-gen-governance and carries a "Do not edit by hand" banner, so the Jaeger transcript was recaptured on better-agentgateway (the system of record for this lab, next-gen-governance#6754) and exported here — bde5386dc5. Merge that branch's PR first.

Recapturing rather than editing caught two things a hand-edit would have got wrong: Jaeger 1.76.0 no longer declares the 4317-4318/9411/14250/14268 ports, and mcp-get-started must not show (healthy) — after the rig split it builds from Dockerfile.get-started, which deliberately omits a HEALTHCHECK to keep the STATUS column clean.

The export also brings the rest of better-agentgateway into acp-goose and mcp-get-started, which is the sync doing its job.

check_doc_drift.py reports this drift rather than fixing it, and names which of the two homes applies (evidence/ here vs .mdx upstream). Its *Dockerfile glob is now *Dockerfile*, which had been silently skipping Dockerfile.get-started and Dockerfile.tickets.

Deliberately not automated

  • FROM python:3.13-slim — a floating tag, rebuilt nightly, so its last_updated is perpetually a day old and a release cooldown is structurally unmeasurable. What the pin controls is 3.13-vs-3.14, a compatibility call for a human PR.
  • ceposta/keycloak:id-jag — mutable tag on a third party's personal Docker Hub account, no version, no release feed. Nothing to cooldown or move to, so it's digest-pinned instead.
  • Running the labs. These PRs are not proof a bump works; that's next-gen-governance's task e2e.

Setup before the first scheduled run

Install the Zenable Automation app on this repo with contents / pull-requests / workflows write, and make ZENABLE_AUTOMATION_CLIENT_ID and RELEASE_APP_PRIVATE_KEY visible to it — this repo is public, so org secrets need explicit sharing.

Unrelated bug found along the way

forecast_agent.py's agent card grows its signatures array by one on every fetch (measured 7, 8, 9 across three consecutive curls). The A2A SDK's signer appends, and the agent serves one long-lived AgentCard that the modifier mutates in place. Not touched here — it predates this work and isn't about pinning — but the evidence was captured with the card unfetched so it records 1 signature, matching a reader's first fetch. Worth its own issue.

🤖 Generated with Claude Code

@ai-coding-guardrails ai-coding-guardrails Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! 😎

I didn't find anything of concern

List of skipped files due to configuration

Risk: 🟡 Medium

Reviewed with 🤟 by Zenable

@ai-coding-guardrails ai-coding-guardrails Bot added the zenable/risk:medium Zenable assessed this PR as MEDIUM risk. label Sep 3, 2026
JonZeolla and others added 2 commits September 3, 2026 16:28
Adds the automation that refreshes every pinned dependency in this repo —
container image tags, uv.lock files, and GitHub Action SHAs — behind a
supply-chain cooldown and an upstream-provenance check. Runs Tuesdays at
03:16 UTC and opens two pull requests.

Every adopted version must be at least 7 days old, so a compromised upstream
release has time to be noticed by someone else first. Candidates are walked
newest-first and the first past the window wins, so a 3-day-old release is
skipped while an 8-day-old one is still picked up rather than the pin
stalling. Python dependencies get the same window declaratively, via
exclude-newer in each project's [tool.uv], which binds a reader's uv sync as
well as uv lock --upgrade.

GitHub-sourced versions are verified before anything is written: the tag must
exist as a ref in the repository it claims to come from, which a fork's tags
never do. Actions are hash-pinned by pinact and then checked twice, including
a reachability pass pinact has no equivalent for; that pass also fails any
uses: that is not SHA-pinned, standing in for a per-PR zizmor gate.

supply_chain/ is a trimmed vendoring of zenable_monorepo from
next-gen-governance, which is private while this repo is public. Only what
these pins need is carried, and the security-relevant logic is kept close to
upstream so a fix there ports as a readable diff.

Two jobs rather than one so permission-workflows: write is requested in
exactly one place, and every workflow-file edit lands in its own PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dnn74khJT1rBHyZSgmaroE
The first run of the new update automation, with every bump validated by
actually running the lab it touches rather than trusting that it starts.

Keycloak 26.7.1 -> 26.7.2 in the a2a and agent-identity rigs. 26.7.3 exists
but was 3 days old, so the cooldown correctly held it back. Jaeger all-in-one
1.68.0 -> 1.76.0, held to the 1.x line because Jaeger v2 is a different image
with a different CLI.

agentgateway stays at v1.4.1 deliberately. v1.5.0 has now cleared the
cooldown, but it renames the trace spans this lab teaches: POST /* disappears,
delete_session and get_stream become DELETE/GET get-started, and
tools/call get-started splits per tool. That invalidates two transcripts and
two pieces of navigation prose in agentgateway-mcp.mdx, so it is lab-content
work for an author rather than a version bump. update-pins.sh records why,
and the pin stays automated so the next run proposes it again.

mcp-get-started keeps the fastmcp 4 line from #11 and raises its floor to
4.0.2, resolving against current PyPI rather than the 7-day window. The lab
teaches the SEP-2663 tasks extension, which only exists in fastmcp 4 —
released days ago — so a cooldown on the lab's own subject leaves the lock
unsatisfiable rather than safe. server.py imports fastmcp_tasks directly, so
the [tasks] extra stays: base fastmcp pulls only fastmcp-slim.

evidence/ regenerated by each lab's own capture-evidence.sh, never by hand.
a2a's end-to-end.txt and agent-identity's sdjwt-walkthrough.txt came back
byte-identical, and agent-identity reports 22/22 negative tests behaving as
documented. ema-mcp passes 7/7. mcp-get-started's evidence is left alone: it
was captured on the e2e harness and already records fastmcp 4.0.2.

ema-mcp's Keycloak is now pinned by digest. ceposta/keycloak:id-jag is a
mutable tag on a third party's personal Docker Hub account with no version
and no release feed, so there is nothing for the automation to measure or
move it to; the digest at least fixes the bytes.

The README files are generated from the better-agentgateway branch in
next-gen-governance and exported here, never edited in this repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JonZeolla

Copy link
Copy Markdown
Member Author

Superseded by a different approach. Labs is generated from next-gen-governance, so dependency automation belongs there: it can bump the pin, run the labs to regenerate evidence, and re-export the READMEs in one atomic change, with no vendored copy of zenable_monorepo in a public repo. Reimplementing in next-gen-governance; this repo stays a sync target.

@JonZeolla JonZeolla closed this Sep 4, 2026
@JonZeolla
JonZeolla deleted the automated-dependency-updates branch September 4, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

zenable/risk:medium Zenable assessed this PR as MEDIUM risk.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant