Skip to content

feat(lib): capture client-attested build provenance - #454

Open
max-parke-scale wants to merge 9 commits into
nextfrom
maxparke/agx1-418-build-provenance-capture
Open

feat(lib): capture client-attested build provenance#454
max-parke-scale wants to merge 9 commits into
nextfrom
maxparke/agx1-418-build-provenance-capture

Conversation

@max-parke-scale

@max-parke-scale max-parke-scale commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Adds agentex.lib.utils.build_provenance — the shared capture util for client-attested build provenance: git coordinates (repo/commit/ref/subpath), a deterministic working_tree_hash over the build inputs (not the tarball), a dirty flag (Go vcs.modified / Nix dirtyRev shape), and normalize_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 on POST /v5/builds (Surface C, scaleapi) consumed by the sgpctl + CI uploaders (Surface B, scaleapi/sgp). This PR lands the util + archive determinism where agentex.lib lives; the uploaders/columns follow.

Scope notes

  • No build-info.json / runtime sink. An earlier revision wrote build-info.json into the build context for the register_agent()registration_metadata path. Greptile (T-Rex) correctly flagged it as dead-on-arrival (written to the archive root, which the templates’ Dockerfiles don’t COPY and locate_build_info_path() doesn’t read). It’s also redundant: AgentexCloudDeploy.build_id is an FK to AgentexCloudBuild, 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_hash is always computed (content identity); commit/ref/repo anchor it to source when in a git work tree; dirty records uncommitted changes (None outside 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/pyright clean; full lib suite green.

🧑‍💻🤖 — posted via Claude Code

Greptile Summary

Introduces agentex.lib.utils.build_provenance — a best-effort utility that captures git coordinates (repo/commit/ref/subpath), a dirty flag, and a deterministic working_tree_hash over build-context files. It also makes the build archive's member order deterministic by sharing a sorted iter_context_files enumeration between the hash and BuildContextManager.zipped().

  • build_provenance.py: capture_build_provenance degrades gracefully at each step (non-git dir → content hash only; no remote → omit repo; detached HEAD → tag fallback or None); _safe_working_tree_hash wraps the hash to prevent any I/O failure from aborting a build. normalize_remote correctly strips credentials and scheme from both scp-style and HTTPS URLs.
  • agent_manifest.py: zipped() now delegates to iter_context_files, gaining sorted, relpath-stable archive order and correct inclusion of directory symlinks.
  • Tests: 20 unit tests cover clean/dirty/untracked/detached-HEAD/no-remote/non-git/monorepo-subpath cases, hash sensitivity (one-byte, added file, exec-bit, symlink target), and the never-raises guard.

Confidence Score: 5/5

Safe to merge — the capture utility is entirely additive, all build paths degrade gracefully on failure, and the archive-determinism change in zipped() is correct.

The change is well-contained: a new utility module with extensive test coverage, plus a small refactor to BuildContextManager.zipped() that makes archive order deterministic. The best-effort design ensures provenance failures cannot abort builds. The one subtle edge case — git status failure silently producing dirty=False — is a minor accuracy concern under unusual runtime conditions but cannot break any build or produce incorrect behavior downstream.

Files Needing Attention: No files require special attention. The three changed files are consistent and well-tested.

Important Files Changed

Filename Overview
src/agentex/lib/utils/build_provenance.py New utility capturing git coordinates, content hash, and dirty state for build provenance; well-structured best-effort design with a minor edge case where a git status failure silently records dirty=False.
src/agentex/lib/sdk/config/agent_manifest.py Replaces manual rglob+is_file loop in zipped() with iter_context_files; makes archive order deterministic and adds symlink inclusion — a correct, intentional behavioral change.
tests/lib/test_build_provenance.py 20 well-designed unit tests covering clean/dirty/untracked/detached-HEAD/no-remote/non-git/monorepo-subpath, hash sensitivity, and the never-raises guard.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[capture_build_provenance\nrepo_path, context_root] --> B[_safe_working_tree_hash\nhash_root]
    B --> B1{Exception?}
    B1 -->|yes| B2[tree_hash = None\nlog warning]
    B1 -->|no| B3[tree_hash = hex digest]
    B2 --> C
    B3 --> C
    C[_git rev-parse --show-toplevel] --> D{repo_root?}
    D -->|None| E[BuildProvenance\nworking_tree_hash only\nbuild_timestamp]
    D -->|Path| F[_git rev-parse HEAD → commit\n_git symbolic-ref HEAD → ref\nfallback: describe --tags --exact-match\n_git remote get-url origin → remote\n_git log -1 → author]
    F --> G{context_root inside repo root?}
    G -->|yes| H[subpath = relative posix path]
    G -->|no / ValueError| I[subpath = None]
    H --> J[_git status --porcelain -- subpath]
    I --> K[_git status --porcelain]
    J --> L{output is not None?}
    K --> L
    L -->|True| M[dirty = True]
    L -->|False| N[dirty = False\nNOTE: also False on git failure]
    M --> O[BuildProvenance\nrepo, commit, ref, subpath,\nworking_tree_hash, dirty,\nauthor, build_timestamp]
    N --> O
Loading

Reviews (7): Last reviewed commit: "Merge branch 'next' into maxparke/agx1-4..." | Re-trigger Greptile

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>
@max-parke-scale
max-parke-scale changed the base branch from main to next June 30, 2026 01:04
Comment thread src/agentex/lib/utils/build_provenance.py Outdated
Comment thread src/agentex/lib/utils/build_provenance.py Outdated
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>
@max-parke-scale

Copy link
Copy Markdown
Contributor Author

Addressed both Greptile findings in cf9994d:

  1. Ignored files lose hashing — fixed by removing the "skip hash on clean commit" path entirely: working_tree_hash is now always computed over the staged context, so a .gitignored-but-not-.dockerignored file is captured by the content hash regardless of git status. (Identity = the always-present hash; commit anchors it to source. Dedupe is unaffected since the hash is deterministic.)
  2. Hash failures abort builds — wrapped the computation in _safe_working_tree_hash, which degrades to None and logs on any error, honoring the “capture never raises into a build” contract.

Also, per design discussion: dirtiness is now a first-class dirty flag (surfaced as source_dirty / dirty) rather than implied by hash-presence — matching Go’s vcs.modified and Nix’s dirtyRev; None outside a git work tree.

🧑‍💻🤖 — posted via Claude Code

Comment thread src/agentex/lib/cli/handlers/agent_handlers.py Outdated
max-parke-scale and others added 2 commits June 29, 2026 22:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants