Skip to content

fix(contributor-check): vendor AGT scripts and fail on execution faults - #46

Merged
imran-siddique merged 4 commits into
agentrust-io:mainfrom
Mayur021:fix/contributor-check-vendor-and-returncode
Sep 30, 2026
Merged

imran-siddique merged 4 commits into
agentrust-io:mainfrom
Mayur021:fix/contributor-check-vendor-and-returncode

Conversation

@Mayur021

Copy link
Copy Markdown
Member

What this fixes

An infrastructure fault was being reported as a contributor-risk level. _run_check called subprocess.run without reading returncode, went straight to json.loads(result.stdout), and the bare except Exception turned the resulting error into "UNKNOWN". Because RISK_ORDER is deliberately fail-closed, UNKNOWN outranks LOW, so a check that could not run labelled established contributors as more suspicious than genuinely low-risk ones. That is the five false flags in #27.

This is item 2 of the issue. It does not close #27: item 1, the bounded retry around the profile lookup, still belongs in _api inside contributor_check.py. Vendoring makes that easier rather than harder, because the retry can now be backported here instead of waiting for an upstream reconciliation.

The change

_run_check reads the return code. A check that could not be executed raises CheckExecutionError, main exits non-zero with an ::error annotation, and no comment and no label are applied. UNKNOWN keeps its upstream meaning of a check that ran and could not determine an answer, and still labels.

Before, at upstream 359a6b8:

_run_check(...)                     -> 'UNKNOWN'
_aggregate_risk('UNKNOWN', 'LOW')   -> 'UNKNOWN'

After:

_run_check(...)  raises CheckExecutionError:
  contributor_check.py exited 1: ModuleNotFoundError: No module named 'agent_compliance'

A check that exits 0 reporting {"risk": "UNKNOWN"} returns UNKNOWN in both.

Vendoring

The scripts now live in .github/actions/contributor-check/vendor/agt/, copied from microsoft/agent-governance-toolkit at 359a6b8cf453f95d6bc9caf932057e1e78ccffd9, MIT, licence headers intact. action.yml no longer checks out that repository at runtime. vendor/agt/README.md records the source commit and a SHA-256 per file, and a CI job verifies those checksums, so a vendored file drifting from upstream fails the build rather than going unnoticed.

Five files, not the two named in the issue, and this is the part worth a second opinion. contributor_check_action.py resolves the checks it runs as siblings of itself through Path(__file__).resolve().parent. The action runs profile,credential by default and profile,credential,cluster on dispatch, so credential_audit.py is invoked on every run and cluster_detect.py on some. contributor_check.py reads contributor_check_allowlist.json from its own directory and silently grants no exemptions if it is absent. Vendoring only two would leave those siblings missing, and with the return-code change that is a hard failure on every run instead of a silent UNKNOWN. If you would rather keep a narrower vendor and resolve the rest some other way, say so and I will rework it.

Tests

.github/actions/contributor-check/tests/test_contributor_check_action.py, 14 tests, run by a new workflow on any change under the action directory.

The regression case is the scripts-only shape: a check that exits non-zero with empty stdout, which is what a missing sibling produces and what a sparse-checkout: scripts produced against an upstream shim importing a package outside scripts/. It asserts CheckExecutionError rather than UNKNOWN.

The rest cover a missing script, a zero exit with no output, a genuine UNKNOWN surviving unchanged, computed levels passing through, NONE still normalising to LOW, the output file still being written on failure, the fail-closed ordering itself being untouched, every invokable sibling being present, licence headers intact, and the allowlist parsing and shipping empty.

Checked that the tests are load-bearing: restoring the unmodified upstream entrypoint makes 5 of the 14 fail.

Not changed

contributor_check.py, credential_audit.py, cluster_detect.py and the allowlist are byte-identical to upstream. The risk ordering, the allowlist behaviour and the comment and label paths are untouched. Once this lands, the three issues in the original report are worth re-running to confirm they resolve to a real level.

Refs agentrust-io#27

Signed-off-by: Mayur Agnihotri mayur.agnihotri0021@gmail.com
Refs agentrust-io#27

Signed-off-by: Mayur Agnihotri <mayur.agnihotri0021@gmail.com>
Refs agentrust-io#27

Signed-off-by: Mayur Agnihotri <mayur.agnihotri0021@gmail.com>
Refs agentrust-io#27

Signed-off-by: Mayur Agnihotri <mayur.agnihotri0021@gmail.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

❔ Contributor Check: UNKNOWN

Check Result
Profile UNKNOWN
Credential LOW
Overall UNKNOWN

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:UNKNOWN Contributor check flagged UNKNOWN risk label Sep 30, 2026
@Mayur021

Copy link
Copy Markdown
Member Author

@imran-siddique CI is green: tests and vendor-integrity both pass. The Policy check is the maintainer-approval gate and stays red until review.

Worth noting for the thread: this PR was itself labelled needs-review:UNKNOWN by the action on main, with Profile: UNKNOWN and Credential: LOW. That is the same signature as the three rows in the issue table. Whether that particular run hit the transient path or genuinely could not determine a profile is not visible from the outside, and that is the distinction this change is about, because today both produce the same label.

@imran-siddique
imran-siddique merged commit 79cf0e4 into agentrust-io:main Sep 30, 2026
6 of 7 checks passed
imran-siddique pushed a commit that referenced this pull request Oct 2, 2026
…bound (#49)

`_api` already looped three times, but only a 403 continued the loop. A
5xx raised on the first attempt, and `URLError` was not caught at all, so
the `timeout=15` socket timeout, a DNS failure, a reset or a TLS abort
propagated straight out of the loop. `URLError` was not even imported.

`_count_maintainer_merged` alone walks up to ten PR-detail calls for one
candidate, paced half a second apart, and the run makes more calls
besides. One blip anywhere in that walk ended the whole check. Since #46
that no longer mislabels anyone, which was the `needs-review:UNKNOWN`
symptom in #27; it fails the action instead. But a transient blip still
ends a check a retry would have passed.

The existing loop now covers the transient cases, with full-jitter
exponential backoff capped at 60s, and the bound stays at three attempts:

- 404 returns None on the first attempt and is never retried. It is an
  answer, not a failure. Moved above the 403 branch for readability; the
  codes are distinct, so behaviour is unchanged.
- 403 with Retry-After is honoured, clamped to 5-60s, as before. If it
  outlasts the bound it raises, and now says it was a rate limit.
- 5xx and URLError back off, then raise as themselves.
- Every other status raises on the first attempt, unchanged.

Each exhausted path prints a line naming its own cause before re-raising.
`_run_check` in contributor_check_action.py takes the last stderr line as
the CheckExecutionError detail, and upstream's 403 path raised silently,
so an exhausted rate limit used to reach a maintainer as a bare `exited
1` with nothing to say whether to wait or re-run.

This is a second declared local modification to the vendored copy, so
contributor_check.py moves out of the checksum table and the three
remaining vendored files stay hash-pinned in CI. vendor/agt/README.md
records both modifications and drops the upstream sync framing: AGT is
not being invested in and this copy is owned here.

24 tests pass. The eight added cover both outcomes of each transient
path, the 404 short-circuit, the attempt bound on every exhausted path,
and the backoff cap. Each was checked against a mutated source to confirm
it fails when the behaviour it names is removed.

Refs: #27

Signed-off-by: Mayur021 <mayur.agnihotri0021@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:UNKNOWN Contributor check flagged UNKNOWN risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

contributor-check returns UNKNOWN on transient failure with no retry, producing false needs-review flags

2 participants