feat(lib): capture client-attested build provenance#454
Open
max-parke-scale wants to merge 7 commits into
Open
feat(lib): capture client-attested build provenance#454max-parke-scale wants to merge 7 commits into
max-parke-scale wants to merge 7 commits into
Conversation
Add agentex.lib.utils.build_provenance — the single producer of source identity for agent builds (git coordinates + a deterministic content hash of the build context). prepare_cloud_build_context now writes build-info.json into the staged context (populates runtime registration_metadata with no server change) and exposes provenance on CloudBuildContext so the upload can send source_* fields. Archive member order is now deterministic via a sorted enumeration shared with the hash. The hash is computed only when there is no clean commit to identify the build (dirty tree or non-git context). First of three surfaces for AGX1-418 (Phase 1, client-attested); the SGP build-record columns and the sgpctl/Gitea uploaders follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Greptile review on the build-provenance capture util: - Always compute working_tree_hash (drop the "skip on clean commit" path). A `git status` clean tree can still contain .gitignore'd-but-not- .dockerignore'd files the commit can't reproduce; an always-present content hash identifies the exact shipped bytes and closes that gap. - Guard the hash (_safe_working_tree_hash) so a permission error or filesystem race degrades to None instead of aborting the build — the module contract is that capture never raises into a build. - Record dirtiness as a first-class `dirty` flag (surfaced as `source_dirty` / `dirty`) rather than overloading hash-presence, matching Go's vcs.modified and Nix's dirtyRev. None outside a git work tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
Addressed both Greptile findings in cf9994d:
Also, per design discussion: dirtiness is now a first-class 🧑💻🤖 — posted via Claude Code |
Greptile (T-Rex repro) showed build-info.json was written to the archive root, which the templates' Dockerfiles don't COPY and the runtime locate_build_info_path() doesn't read — so it never reached the image and the registration_metadata sink stayed empty. Beyond the placement bug, the sink is redundant: AgentexCloudDeploy.build_id is an FK to AgentexCloudBuild, so a deployment's source provenance derives from the build record (the source_* columns this work adds, Surface C) over that join — the same Build->Deploy edge lineage already traverses. No need to denormalize provenance onto registration_metadata/DeploymentHistory (which has had no producer since its read path landed 2025-09, so its git fields have never been populated). #454 now ships only the shared capture util (agentex.lib.build_provenance) plus a deterministic build-archive ordering. Provenance is delivered via the build-record sink; the runtime sink can be revived (correctly placed) if a real consumer for deployment-history provenance ever appears. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ld-provenance-capture
…ld-provenance-capture # Conflicts: # uv.lock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
agentex.lib.utils.build_provenance— the shared capture util for client-attested build provenance: git coordinates (repo/commit/ref/subpath), a deterministicworking_tree_hashover the build inputs (not the tarball), adirtyflag (Govcs.modified/ NixdirtyRevshape), andnormalize_remote. Capture is best-effort and never raises into a build. Also makes the build archive’s member order deterministic via a sorted enumeration shared with the hash.First of three surfaces for AGX1-418 (Phase 1, client-attested). Provenance is delivered via the build-record sink —
source_*columns onPOST /v5/builds(Surface C, scaleapi) consumed by the sgpctl + CI uploaders (Surface B, scaleapi/sgp). This PR lands the util + archive determinism whereagentex.liblives; the uploaders/columns follow.Scope notes
build-info.json/ runtime sink. An earlier revision wrotebuild-info.jsoninto the build context for theregister_agent()→registration_metadatapath. Greptile (T-Rex) correctly flagged it as dead-on-arrival (written to the archive root, which the templates’ Dockerfiles don’t COPY andlocate_build_info_path()doesn’t read). It’s also redundant:AgentexCloudDeploy.build_idis an FK toAgentexCloudBuild, so a deployment’s source provenance derives from the build record over that join — the same Build→Deploy edge lineage already traverses. Dropped; can be revived (correctly placed) if a real consumer for deployment-history provenance ever appears.Identity model
working_tree_hashis always computed (content identity);commit/ref/repoanchor it to source when in a git work tree;dirtyrecords uncommitted changes (Noneoutside git).Tests
20 provenance unit tests (clean/dirty/untracked/detached-HEAD/no-remote/non-git/monorepo-subpath, hash determinism + one-byte/added/exec-bit/symlink sensitivity, and a never-raises-on-hash-failure guard).
ruff/pyrightclean; fulllibsuite green.🧑💻🤖 — posted via Claude Code
Greptile Summary
This PR introduces
agentex.lib.utils.build_provenance, a best-effort utility that captures client-attested build provenance (git coordinates, a deterministicworking_tree_hashover build inputs, and adirtyflag) and makes the build archive's member order deterministic by sharing the same sorted file enumeration between the hash and the tarball.build_provenance.py:capture_build_provenanceprobes git and computes a content hash; all failure modes degrade gracefully (_safe_working_tree_hashwraps the hash,_gitswallows subprocess errors, missing fields fall back toNone).normalize_remotestrips credentials and scheme from remote URLs.source_fields()exposes only the six fields destined forPOST /v5/builds. Thebuild_info()method andauthor_name/author_emailfields are vestigial from the dropped runtime sink and have no current callers, but the author explicitly notes they can be revived correctly if a deployment-history consumer appears.agent_manifest.py:zipped()now delegates enumeration toiter_context_files, which sorts entries by POSIX relative path and also includes dangling symlinks (previously excluded by theis_file()guard). This keeps the archive contents and the hash in lock-step.tests/lib/test_build_provenance.py: 20 unit tests covering clean, dirty, untracked, detached-HEAD, no-remote, non-git, and monorepo-subpath cases, plus hash sensitivity to one-byte changes, added files, exec-bit flips, and symlink target changes, and amonkeypatchguard confirming no exception escapescapture_build_provenance.Confidence Score: 5/5
Safe to merge; the change is additive, all provenance capture is best-effort and isolated from the build path, and the archive ordering fix is a pure determinism improvement.
The new module is well-structured with proper fallback handling at every failure point. The test suite is thorough and covers the edge cases that matter. The one behavioral change to the archive (dangling symlinks now included) is intentional and consistent with the hash semantics. No incorrect data flows, missing guards, or broken contracts were found.
No files require special attention.
Important Files Changed
build_info()is vestigial from the dropped runtime sink (acknowledged in the PR description) andauthor_name/author_emailare collected but not surfaced viasource_fields().zipped()to useiter_context_filesfor deterministic archive ordering; now also includes dangling symlinks in the tarball (previouslyis_file()excluded them) — intentional and consistent with the content hash semantics.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[capture_build_provenance\nrepo_path, context_root] --> B[_safe_working_tree_hash\nhash_root] B --> C{success?} C -->|yes| D[tree_hash = sha256 of sorted inputs] C -->|no, log warning| E[tree_hash = None] D --> F[_git rev-parse --show-toplevel] E --> F F --> G{git repo?} G -->|no| H[BuildProvenance\nworking_tree_hash only] G -->|yes| I[collect git coords\ncommit, ref, remote,\nauthor, subpath] I --> J[git status --porcelain\n-- subpath if monorepo] J --> K{output non-empty?} K -->|yes| L[dirty = True] K -->|no / fail| M[dirty = False] L --> N[BuildProvenance\nall fields populated] M --> N subgraph working_tree_hash P[iter_context_files\nrglob sorted by POSIX relpath] --> Q[for each file/symlink:\ncompute mode + content sha256] Q --> R[sha256 of joined lines] end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[capture_build_provenance\nrepo_path, context_root] --> B[_safe_working_tree_hash\nhash_root] B --> C{success?} C -->|yes| D[tree_hash = sha256 of sorted inputs] C -->|no, log warning| E[tree_hash = None] D --> F[_git rev-parse --show-toplevel] E --> F F --> G{git repo?} G -->|no| H[BuildProvenance\nworking_tree_hash only] G -->|yes| I[collect git coords\ncommit, ref, remote,\nauthor, subpath] I --> J[git status --porcelain\n-- subpath if monorepo] J --> K{output non-empty?} K -->|yes| L[dirty = True] K -->|no / fail| M[dirty = False] L --> N[BuildProvenance\nall fields populated] M --> N subgraph working_tree_hash P[iter_context_files\nrglob sorted by POSIX relpath] --> Q[for each file/symlink:\ncompute mode + content sha256] Q --> R[sha256 of joined lines] endComments Outside Diff (1)
General comment
Reviews (6): Last reviewed commit: "Scope provenance dirtiness to build cont..." | Re-trigger Greptile