| name | resolve-cve-conflict |
| description | Use when a cve-sync sync run (locally, the sync bumper action, or an escalation issue it opened) reports a conflict or a violation. Walks through reading the failure output, inspecting uv tree to find the real cause, deciding between a floor, an override, an exclude, or a manual pin, applying the fix in the right repo (the cve-sync repo's policy/ or the target repo itself), and re-running the gate to confirm it is actually fixed. Triggers on cve-sync conflict, cve-sync violation, a cve-sync-escalation issue, or "resolve-cve-conflict". |
Resolving a cve-sync conflict or violation
cve-sync sync --apply (or the sync bumper action wrapping it) reports one
of three outcomes: clean, conflict, or violation. This skill is for
the latter two, the ones that need a human. Read this repo's README.md
(its "The policy model" section) first if you need the full "why";
this skill assumes you already know the shape (global floors in
policy/floors.toml, global overrides in policy/overrides.toml, global
excludes in policy/excludes.toml, applied to every repo unconditionally;
the gate checks lower bounds and exclusions only, never ceilings).
Step 1: which of the two are you looking at?
- conflict (exit 2):
uv lock itself failed. Printed uv's own
resolver error. Something cannot be resolved at all, the residue and the
target's own dependencies (or two parts of the residue) are mutually
unsatisfiable.
- violation (exit 3): it locked fine, but
cve-sync check still found
a problem afterward, either a floor/override violation, or an excluded
package still present with --exclude-severity error. Since the spliced
residue is always the full policy (there's no lock-dependent subset to
get wrong), this should be rare: uv itself enforces the spliced
constraints/overrides/excludes at lock time, so a lock that succeeds
should already satisfy them. If you hit it anyway, treat it like a
conflict investigation below, just for whichever
package: resolved X, requires Y (kind, source) line it printed instead
of a uv resolver error.
Step 2: read the output uv already gave you
uv's resolver errors usually name the exact conflicting requirement
chain directly, for example:
× No solution found when resolving dependencies:
╰─▶ Because your project depends on openai<2.30.0 and openai>=2.30.0, we can
conclude that your project's requirements are unsatisfiable.
That already tells you the package (openai), one side of the conflict
(the target's own openai<2.30.0), and the other side (a floor or
override of openai>=2.30.0). Sometimes the message names a transitive
dependency instead of something you recognize immediately, that's what
step 3 is for.
Step 3: find out who actually owns the conflicting requirement
In the target repo (the one that failed to sync, not the cve-sync repo
itself), run:
uv tree --package <package> --invert
This inverts the tree and shows every path from the conflicting package
back up to the root project, so you can see exactly who requires it and
through what chain. Example, tracing cryptography back through a real
crewai dependency:
cryptography v49.0.0
├── crewai-cli v1.15.2
│ └── crewai v1.15.2
│ └── your-app v0.1.0
├── crewai-core v1.15.2
│ ├── crewai v1.15.2 (*)
│ └── crewai-cli v1.15.2 (*)
├── pdfminer-six v20260107
│ └── pdfplumber v0.11.10
│ └── crewai v1.15.2 (*)
└── pyjwt v2.13.0 (extra: crypto)
└── mcp[crypto] v1.26.0
└── crewai v1.15.2 (*)
This tells you whether the cap comes from:
- the target repo's own direct dependency declaration (the tree's
root, or one hop below it, e.g.
your-app v0.1.0 requiring the package
directly with a version cap of its own), or
- a third-party package's own metadata (some node in the middle of
the tree, not the root, requiring the version cap on behalf of a stack
the target repo just happens to pull in), or
- the cve-sync policy overreaching (the floor or override
itself asks for a version higher than anything actually released yet;
check by trying to resolve the package alone, e.g.
uv pip install '<package>>=<floor spec>' in a scratch venv, or just
checking PyPI).
Step 4: pick the fix, and the repo it belongs in
- Cap is the target's own direct pin, with no real reason for it:
fix the target repo directly, loosen or remove the pin. Not a
policy change in this repo at all.
- Cap is a third-party package's metadata (the "nvidia-nat caps
cryptography<47" shape this whole tool exists for): this belongs in
policy/overrides.toml in this repo, as an [[override]] entry with a
spec that beats the cap. A package can only live in one of
policy/floors.toml or policy/overrides.toml, never both, so if a
floor already exists for the same package, move it rather than adding
the override alongside it. If the override's lower bound would sit
below the floor you're replacing, that's a red flag: cve-sync validate
rejects both "in both lists" and "override below floor" with a specific
message telling you exactly what's wrong.
- The floor or override itself was bumped past a version that has not
actually been released yet: also a fix in this repo, not the
target repo's. Correct the spec in whichever of
policy/floors.toml /
policy/overrides.toml holds the entry to a real, currently-released
version.
- A package needs to be dropped entirely (unused, or genuinely
incompatible and not worth carrying): an
[[exclude]] in
policy/excludes.toml. Unlike floors and overrides, an exclude applies
to every repo unconditionally, there is no per-stack scoping to fall
back on. Be careful: if you're not confident every repo that resolves
the package is safe without it, this needs a product conversation first,
not just an edit. The default --exclude-severity warn is the safety
net for a mistake here, an excluded package reappearing in some other
repo's lock gets reported, not hard-failed, unless that repo has opted
into error.
- Nothing above works cleanly (a real, narrow incompatibility that
genuinely needs staying on an older version, or a case that needs a
human product decision, not just a version bump): a manual pin in the
target repo's own
[tool.uv], outside the # cve-sync:begin /
# cve-sync:end markers (anything inside those markers gets overwritten
by the next cve-sync sync run). Note clearly, in the PR or the
escalation issue, why this needed a manual decision instead of a policy
fix, so the next person doesn't immediately try the override and hit
the same wall.
Step 5: apply the fix, then prove the gate is actually clean
- Fix lives in this repo (the cve-sync policy): edit
policy/, run
cve-sync validate locally, open a PR here. Once merged, the affected
downstream repos pick it up on their next bumper run, either the pushed
repository_dispatch (same-org subscribers) or their own
check-for-updates cron (any org, see the README). You do not need
push access to the downstream repo to land the actual fix, only to this
repo.
- Fix lives in the target repo (its own pin, or a manual pin outside
the managed markers): edit it there, then re-run
cve-sync sync --apply (or task cve-sync:sync if the repo uses the
Taskfile include) locally, and confirm it reports clean before pushing.
Don't just assume the fix worked, actually re-run the gate.
- Either way, once the target's bumper run (or your manual re-run) reports
clean, the escalation issue can be closed and the rolling PR merged
after normal review. cve-sync never auto-merges anything, that step is
always a human's call.