DEV Community

Jordan Huang
Jordan Huang

Posted on

The Agent Ran pip. You Still Don't Have Provenance.

Did your agent just claim the build is fixed?
How do you know what it actually fetched?

I keep getting the same Slack ping at review time.
Someone merged because a free remote box went green.

A green remote box is still not provenance.
That success is one network call that landed.

Why this FAQ exists

Agents now install packages without a real pause.
Free remote servers make that habit feel cheap.

It is not cheap in review cost.
You still own the full resolved graph.

I wrote this as a myth-busting FAQ.
Each myth gets a correction and a check.

No fake benchmarks. No vendor leaderboard.
Just files you can hash after the agent leaves.

Where scratch compute actually helps

I sometimes need a disk that is not my laptop.
A dirty resolve should not pollute my local cache.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode offers free model access and a free server option.
I treat that server as scratch compute, not as an artifact registry.

The capture workflow below is the real payload.
It still works if you never use that product.

Myth 1: The agent installed it, so the lockfile is current

Did the agent edit requirements.txt or uv.lock?
Or did it only run pip install against today's index?

Those are different acts with different evidence.
One mutates your contract. One mutates a temp disk.

Corrected model: a lockfile is a pinned graph you kept.
An agent install is a one-shot resolver run you watched.

Run this before you trust the chat summary:

git status --short
git diff -- requirements.txt uv.lock poetry.lock \
  package-lock.json pnpm-lock.yaml Cargo.lock go.sum
Enter fullscreen mode Exit fullscreen mode

If those files are clean, you did not pin anything.
You only warmed a cache on some machine.

Ask one more question in the pull request.
Which committed file changed because of this agent?

Myth 2: pip freeze is already a lockfile

Is pip freeze a lockfile in disguise?
Only if you treat a dump as a contract.

pip freeze captures what sits in that environment.
It does not capture markers, extras, or hashes by default.

Corrected model: freeze is a snapshot of one interpreter.
A lockfile is resolver output you can replay later.

Proposed check. Label: you must run it yourself.

python -m pip freeze > /tmp/freeze-a.txt
# after the agent "helps"
python -m pip freeze > /tmp/freeze-b.txt
diff -u /tmp/freeze-a.txt /tmp/freeze-b.txt
Enter fullscreen mode Exit fullscreen mode

A diff is evidence you can paste into review.
A chat sentence like "deps are fine" is not evidence.

For npm, freeze thinking fails the same way.
npm ls is not package-lock.json under review.

npm ls --all --package-lock-only
git diff -- package-lock.json
Enter fullscreen mode Exit fullscreen mode

Myth 3: Same language version means the graph matches

Same Python minor on both hosts?
Maybe. Same ABI, libc, and wheel tag? Often not.

A free remote server is another OS image.
Wheels that install there may not install here.

Corrected model: the platform is part of the resolve.
Omit it, and you compare two different problems.

python - <<'PY'
import platform, sys
print("python", sys.version.replace("\n", " "))
print("impl", platform.python_implementation())
print("platform", platform.platform())
print("machine", platform.machine())
print("executable", sys.executable)
PY
Enter fullscreen mode Exit fullscreen mode

Save that next to the freeze file.
Otherwise you cannot compare two green runs.

Node has the same trap with optional natives.
Record node -p process.platform and process.arch too.

node -p 'process.version+" "+process.platform+" "+process.arch'
Enter fullscreen mode Exit fullscreen mode

Myth 4: No new package names means no supply-chain change

Did the top-level version string stay put?
The wheel, tarball, or tag might not have.

Yanked files get replaced on public indexes.
Mirrors drift. Rebuilds happen. Transitives move.

Corrected model: identity is name plus version plus hash.
Version equality is a weak claim, not a bill of materials.

Proposed inventory dump. Still not an SBOM tool.

python - <<'PY'
from importlib import metadata
for dist in sorted(metadata.distributions(), key=lambda d: (d.metadata["Name"] or "").lower()):
    name = dist.metadata["Name"]
    ver = dist.version
    n = len(list(dist.files or []))
    print(f"{name}=={ver} files={n}")
PY
Enter fullscreen mode Exit fullscreen mode

Want a stronger check? Hash what the installer fetched.
Do not stop at the version column in pip list.

python -m pip download -d /tmp/wheels -r requirements.txt --no-deps
sha256sum /tmp/wheels/*
Enter fullscreen mode Exit fullscreen mode

--no-deps is incomplete on purpose here.
It shows whether you even know your direct pins.

Myth 5: The scratch server is air-gapped enough

Is it air-gapped, or just unpaid?
You probably still hit a public index.

Agents love --upgrade and "install the missing extra."
They also love a second index URL you never reviewed.

Corrected model: a scratch server is still a network client.
Egress is part of the threat model, always.

Minimum questions before you keep the environment:

  1. Which index URL did the resolver hit?
  2. Was --require-hashes actually on?
  3. Did the agent add another extra index?
  4. Are tokens sitting in that process environment?
env | grep -E 'PIP_|UV_|NPM_|POETRY_|CARGO_|TWINE_|TOKEN|GITHUB_' || true
python -m pip config list
Enter fullscreen mode Exit fullscreen mode

If that prints secrets, stop immediately.
Rotate them. Do not paste the output into a model chat.

I do not want a helpful agent to "debug auth."
I want the credential out of that box first.

Myth 6: Green tests mean the graph is mergeable

Tests exercise behavior on one interpreter.
They do not exercise provenance of every byte.

A swapped package can still pass unit tests.
A slightly newer transitive dependency can too.

Corrected model: merge when the pin, hash, and tests agree.
Not when a model replies with the word done.

Ask the PR author for three artifacts, not one screenshot:

  • the lockfile diff, even if it is empty
  • host and interpreter identity from both machines
  • the test command that actually ran, copied verbatim

Missing any of those? The merge is a vibe check.
Vibe checks do not belong on the default branch.

The artifact: a ten-minute capture workflow

I want a folder I can keep.
Not a screenshot of a terminal from a chat log.

Label: this is a proposed local workflow.
Run it on your laptop. Then run it on the scratch host.

#!/usr/bin/env bash
# capture-graph.sh — proposed provenance snapshot
set -euo pipefail
out="${1:-./graph-capture}"
mkdir -p "$out"

{
  date -u +"utc=%Y-%m-%dT%H:%M:%SZ"
  echo "host=$(hostname)"
  echo "who=$(whoami)"
  uname -a
} > "$out/host.txt"

python - <<'PY' > "$out/python.txt"
import platform, sys
print("executable", sys.executable)
print("version", sys.version)
print("platform", platform.platform())
print("machine", platform.machine())
PY

for f in requirements.txt uv.lock poetry.lock \
         package-lock.json pnpm-lock.yaml Cargo.lock go.sum; do
  [[ -f "$f" ]] && cp "$f" "$out/"
done

python -m pip freeze > "$out/pip-freeze.txt" || true
python -m pip list --format=freeze > "$out/pip-list.txt" || true
command -v node >/dev/null && node -p 'process.version' > "$out/node.txt" || true

if command -v sha256sum >/dev/null; then
  sha256sum "$out"/* > "$out/SHA256SUMS" || true
elif command -v shasum >/dev/null; then
  shasum -a 256 "$out"/* > "$out/SHA256SUMS" || true
fi

echo "wrote $out"
Enter fullscreen mode Exit fullscreen mode

Then fill this table without lying to yourself.

Question Laptop Scratch host Merge if disagree?
Interpreter version identical? No
OS / arch recorded? No
Lockfile hash identical? No
Freeze diff empty? Investigate
Require-hashes used? No for audited trees
Index URL explicit? No
Tests green on both? Not sufficient alone

If a merge cell says no, do not ship.
Fix the pin first. Then rerun the capture.

Compare the two folders like any other artifact:

diff -u laptop/graph-capture/pip-freeze.txt \
        scratch/graph-capture/pip-freeze.txt
diff -u laptop/graph-capture/SHA256SUMS \
        scratch/graph-capture/SHA256SUMS
Enter fullscreen mode Exit fullscreen mode

Empty diffs are a story you can defend.
"The agent said it worked" is not.

What this does not prove

This script does not give you SLSA provenance.
It does not attest a builder identity.

It does not pin hashes by itself.
It does not replace uv lock, npm ci, or cargo fetch.

It will not catch a compromised index serving the same hash.
It will not catch malware that never shows in pip list.

It also does not freeze the remote image.
Tomorrow's scratch host may not be today's scratch host.

Who should skip this approach

Skip it if you already have hermetic CI with hashed pins.
Skip it if you cannot run commands on the scratch host.

Skip it if policy forbids unknown remote boxes.
Skip it if you needed a real SBOM pipeline yesterday.

Do not use a free scratch server as your release builder.
Do not paste internal package names into a public model chat.

Do not let an agent export PIP_INDEX_URL "as a convenience."
That convenience is how credentials leak into logs.

A corrected mental model

The agent is a noisy intern with root on a temp disk.
The free server is another intern's unmarked laptop.

You are still the maintainer of the graph.
Provenance is a file you keep, not a sentence you read.

Did the agent help you move faster?
Maybe. Did it document the graph? Only if you captured it.

Keep the capture folder beside the lockfile.
Or store it next to the build, not in a chat transcript.

Your call.
Just stop calling a successful pip install a lockfile.

Top comments (0)