Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 67 additions & 51 deletions .claude/agents/iyarc-prune.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,31 @@ over an unsafe or unverified bump.

## Environment notes

- For a manual run of [the workflow](../../.github/workflows/iyarc-prune.yml#L29-L42),
select `master` in **Use workflow from**. The AWS login uses the
[workflow's branch identity](https://docs.github.com/en/actions/reference/security/oidc#filtering-for-a-specific-branch),
not the checkout ref. Merge workflow changes before a live run; local agent
invocations are unaffected.
- This is a Lerna + Yarn (v1, `1.22.22`) workspaces monorepo with ~116 packages
under `modules/`. Node and Yarn are already provisioned in this runner.
- The release audit gate is:
- Workflow operations live in the dependency-free
[JavaScript helper](../../scripts/iyarc-prune.js), not inline shell programs.
Run their [regression tests](../../scripts/tests/iyarc-prune.test.js) with
`yarn test:scripts`; the tests use isolated temporary Git repositories.
- The workflow installs `osv-scanner` v2.3.8, matching the release audit.
It writes JSON results:
```
osv-scanner --config=osv-scanner.toml --severity=HIGH --severity=CRITICAL ./
osv-scanner --config=osv-scanner.toml --format=json --output-file="$RUNNER_TEMP/osv-results.json" ./
```
This is the EXACT command the release pipeline runs, so it is your source of
truth for "fixed".
For a local run, substitute a temporary directory for `$RUNNER_TEMP`.
The scanner has no `--severity` flag. Exit code 1 means findings, not
necessarily a failed release gate. Evaluate the JSON using the shell step in
`.github/actions/osv-severity-gate/action.yml`, with `SCAN_OUTCOME=success`
for exit code 0 or `SCAN_OUTCOME=failure` for exit code 1, `RESULTS_FILE` set
to that output path, and `CVSS_THRESHOLD=7.0`. CVSS (Common Vulnerability
Scoring System) scores of 7.0 or above block release. Exit codes above 1
are scanner errors and MUST fail validation. Read the current release steps
in `.github/workflows/npmjs-release.yml` before running the audit.
- IMPORTANT: nearly every entry in `osv-scanner.toml` is a TRANSITIVE dependency
(e.g. tar, minimatch, ws, form-data, protobufjs, tmp, sjcl, sanitize-html,
esbuild), pinned in the root `package.json` `resolutions` block — NOT a direct
Expand All @@ -52,7 +69,7 @@ behind, still exists.

Run this check and decide on the parsed state, never on "a PR row exists":

gh pr list --state open --json number,state,headRefName --jq '.[] | select(.headRefName | startswith("osv-scanner-prune/"))'
gh pr list --state open --limit 1000 --json number,state,headRefName --jq '.[] | select(.headRefName | startswith("osv-scanner-prune/"))'

- Non-empty output: an open prune PR exists — stop and report — do not open a
second. Name the open PR number(s) and head branch(es) in the report.
Expand All @@ -65,6 +82,7 @@ from merged prune PRs linger until deleted and must not trigger this early exit.
## Read context first

Before changing anything, read:

1. `osv-scanner.toml` — the full ignore list and every justification comment.
2. The root `package.json` `resolutions` block.
3. `scripts/upgrade-workspace-dependency.ts` (the `yarn upgrade-dep` tool).
Expand Down Expand Up @@ -103,8 +121,9 @@ For each `[[IgnoredVulns]]` entry in `osv-scanner.toml`:

After each attempted fix, run the SAME gates the release pipeline runs, in this
order:
1. `osv-scanner --config=osv-scanner.toml --severity=HIGH --severity=CRITICAL ./`
It MUST pass with the exclusion removed. Capture the output.

1. Run the pinned scanner and severity gate described above. The gate MUST
pass with the exclusion removed. Capture the scanner and gate output.
2. `yarn check-deps`. It MUST pass — a `resolutions` change can break
cross-workspace version consistency. This is both a release-job step (it runs
immediately after audit in the release workflow) and a PR-CI gate, so a
Expand All @@ -115,58 +134,55 @@ order:
4. If ANY step fails — no compatible fix, audit still flags the advisory,
check-deps fails, build breaks, or tests fail — revert that dependency's
changes and restore its exclusion in `osv-scanner.toml`. Never open a PR with
a red feedback loop. The full test suite still runs in PR CI as a backstop.

## Commit and pull request (only if at least one exclusion was removed with a
fully green feedback loop)

How you finish depends on where you are running:

- **If running in CI** (a `osv-scanner-prune/*` branch exists and the
`mcp__github_file_ops__commit_files` tool is available): commit and open the PR
as described below.
- **If a developer is running you locally:** make the edits, run the full
feedback loop, print the summary table and the "Still blocked" section, and
STOP. Do not commit or open a PR — let the developer review and commit.

CI commit/PR rules:

- Commit message: conventional (commitlint extends `@commitlint/config-conventional`;
`deps` and `root` are valid scopes), e.g.:
`chore(deps): bump <pkg> to <version>, drop <GHSA> from osv-scanner.toml`.
commitlint enforces `references-empty: never`, so the message MUST carry an
issue reference: include `Ticket: HSM-429` in the footer.
- SIGNED COMMIT (important — `master` requires signed commits): this workflow
runs with commit signing enabled. Make your commit using the
`mcp__github_file_ops__commit_files` tool — NOT `git commit`/`git push` —
passing every changed path (`osv-scanner.toml`, `package.json`, `yarn.lock`).
That tool commits through GitHub's API, so the commit is Verified (signed).
Commits made with raw `git` will be UNSIGNED and cannot be merged.
- FALLBACK: if the signing tool fails, commit with `git` anyway and add this
line to the PR body: "⚠️ Commits are unsigned — a maintainer must re-sign
before merge."
- The workflow creates the working branch automatically (prefix `osv-scanner-prune/`);
commit your changes to it, then open a single NON-draft PR against `master`
with `gh pr create`.
- Labels: ensure `automated`, `dependencies`, and `security` exist (create any
missing one with `gh label create <name> --force`), then apply all three.
- Assign the PR to `gokulhost` so it does not get lost:
`gh pr edit <number> --add-assignee gokulhost`. CODEOWNERS reviewers are
assigned automatically and separately.
- PR body must contain:
a red feedback loop. The workflow publishes with GitHub's repository-scoped
workflow token (`GITHUB_TOKEN`). A maintainer may need to approve workflow
runs before continuous integration (CI) starts on the resulting pull request.
Do not assume those checks ran; they must pass before merging.

## Commit and pull request (only after validated exclusion removals)

The workflow owns branch creation, signed commits, and pull request creation.
Do not create a branch, commit, push, or open a pull request yourself.

- **When `IYARC_PRUNE_PR_BODY` is set:** you are running under the publication
workflow, even when the checked-out branch is `master`. Leave the validated
edits uncommitted and write the pull request body to the absolute path in
`IYARC_PRUNE_PR_BODY`. Create this file ONLY after at least one exclusion has
been removed and the entire feedback loop has passed. If an open prune pull
request exists or nothing can be safely removed, do not create this file.
If validation fails, revert the failed changes before finishing.
- **When `IYARC_PRUNE_PR_BODY` is unset:** this is a local run. Make the edits,
run the feedback loop, print the summary table and the "Still blocked"
section, and stop so the developer can review and commit.
- The workflow creates a signed commit with the `Ticket: HSM-429` footer and
a non-draft pull request against `master`, using an `osv-scanner-prune/*`
branch. It applies `automated`, `dependencies`, and `security` labels and
assigns `gokulhost`.
- The [publication checks](../../scripts/iyarc-prune.js#L145-L219)
inspect both working-tree and staged paths, then clear the Git staging area
before committing the validated working-tree contents. The published commit
keeps the original checkout as its parent; it is not rebased onto newer
`master` changes. Required pull request checks must validate the merge.
After the agent runs, the workflow reads its helper from the original
checkout commit so edits to the working copy cannot change those checks.
- The pull request body must contain:
- A table of each removed exclusion: GHSA id, package, old -> new version, the
advisory it resolves.
- The pasted `osv-scanner --config=osv-scanner.toml --severity=HIGH --severity=CRITICAL ./`
and `yarn check-deps` output showing they now pass.
- The pasted scanner, severity-gate, and `yarn check-deps` output showing
they now pass.
- Build/test results for the affected module(s).
- A prominent note asking a maintainer to approve pending workflow runs and
verify required checks pass before merging.
- A "Still blocked" section listing every exclusion that could NOT be removed
and the reason (no upstream fix / incompatible parent pin).
- Only if you hit the signing fallback above: the unsigned-commits note.

## Output rules

- If nothing is safely prunable this run, open no PR and report "no exclusions
prunable this run" in the job summary, including the "Still blocked" breakdown
so the result is auditable.
- Only ever modify `osv-scanner.toml`, dependency manifests (`package.json`), and
`yarn.lock`. Do not modify product/source code.
- Only ever modify `osv-scanner.toml`, `package.json`, `yarn.lock`, and direct
workspace manifests at `modules/<workspace>/package.json` in the repository.
Nested test/example manifests are not allowed. The report at
`IYARC_PRUNE_PR_BODY` and temporary validation output belong outside the
repository. Do not modify product/source code.
155 changes: 70 additions & 85 deletions .github/workflows/iyarc-prune.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,25 @@ jobs:
prune:
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
shell: bash
env:
AWS_REGION: us-west-2
steps:
- name: Require master workflow ref
run: |
set -euo pipefail
if [ "$GITHUB_REF" != 'refs/heads/master' ]; then
echo '::error::Run iyarc-prune from master. The AWS role trusts the workflow ref, not the checkout ref. Merge workflow changes before dispatching them.'
exit 1
fi

- name: Checkout repository
id: checkout
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0

- name: Setup Node
Expand All @@ -36,6 +49,11 @@ jobs:
- name: Enable Corepack (yarn 1.22.22)
run: corepack enable

- name: Install release audit scanner
env:
OSV_SCANNER_SHA256: bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc
run: node scripts/iyarc-prune.js install-scanner

- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6
with:
Expand All @@ -44,41 +62,22 @@ jobs:

- name: Assume Bedrock inference role
id: inference-role
run: |
CREDS="$(aws sts assume-role \
--role-arn arn:aws:iam::168000258654:role/BedrockInferenceRole \
--role-session-name iyarc-prune-session \
--query 'Credentials' \
--output json)"

AWS_ACCESS_KEY_ID="$(echo "$CREDS" | jq -r '.AccessKeyId')"
AWS_SECRET_ACCESS_KEY="$(echo "$CREDS" | jq -r '.SecretAccessKey')"
AWS_SESSION_TOKEN="$(echo "$CREDS" | jq -r '.SessionToken')"

echo "::add-mask::$AWS_SECRET_ACCESS_KEY"
echo "::add-mask::$AWS_SESSION_TOKEN"
{ echo "aws-access-key-id=$AWS_ACCESS_KEY_ID"; echo "aws-secret-access-key=$AWS_SECRET_ACCESS_KEY"; echo "aws-session-token=$AWS_SESSION_TOKEN"; } >> "$GITHUB_OUTPUT"
run: node scripts/iyarc-prune.js assume-inference-role

- name: Load iyarc-prune agent instructions
id: agent
run: |
{
echo "content<<IYARC_PRUNE_EOF"
sed '1,/^---$/d' .claude/agents/iyarc-prune.md
echo "IYARC_PRUNE_EOF"
} >> "$GITHUB_OUTPUT"
run: node scripts/iyarc-prune.js load-instructions

- name: Run Claude Code (Bedrock)
id: claude
uses: anthropics/claude-code-action@8d70a0dd265122bd65b8e09d3d959e65373ae2a1 # v1.0.260
with:
prompt: ${{ steps.agent.outputs.content }}
use_commit_signing: 'true'
branch_prefix: 'osv-scanner-prune/'
claude_args: --allowed-tools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files'
claude_args: --allowed-tools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash'
use_bedrock: 'true'
github_token: ${{ secrets.GITHUB_TOKEN }}
env:
IYARC_PRUNE_PR_BODY: ${{ runner.temp }}/iyarc-prune-pr.md
ANTHROPIC_MODEL: arn:aws:bedrock:us-west-2:168000258654:inference-profile/global.anthropic.claude-sonnet-5-5
# Bedrock: replaying a tool-call turn requires a leading thinking block; disable to avoid 400.
MAX_THINKING_TOKENS: '0'
Expand All @@ -88,70 +87,56 @@ jobs:
AWS_SESSION_TOKEN: ${{ steps.inference-role.outputs.aws-session-token }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# The agent prompt promises a job summary ("report ... in the job summary,
# including the Still blocked breakdown so the result is auditable"), so
# render the agent's own report there instead of leaving it log-only.
# Agent edits must not change the code that validates or publishes them.
- name: Write agent report to job summary
if: always()
if: always() && steps.claude.outcome != 'skipped'
env:
BASE_SHA: ${{ steps.checkout.outputs.commit }}
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
run: |
# Deliberately NOT `set -e`: this step is reporting only and must never
# be the reason the job goes red. Failures degrade to a note instead.
set -uo pipefail
EVENTS=''
if [ -n "${EXECUTION_FILE:-}" ] && [ -f "$EXECUTION_FILE" ]; then
EVENTS="$(jq -c 'if type=="array" then . else [.] end' "$EXECUTION_FILE" 2>/dev/null)"
fi
REPORT=''
META=''
if [ -n "$EVENTS" ]; then
REPORT="$(printf '%s' "$EVENTS" | jq -r '.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text' 2>/dev/null)"
# Allowlist, not del(.result): the execution-file schema is upstream and
# may grow fields we have not audited, so name what we publish.
META="$(printf '%s' "$EVENTS" | jq -S 'map(select(.type=="result"))[-1] // empty
| {is_error, subtype, terminal_reason, stop_reason, num_turns,
duration_ms, total_cost_usd, session_id,
output_tokens: .usage.output_tokens}
| with_entries(select(.value != null))' 2>/dev/null)"
fi
{
echo "## osv-scanner-prune report"
echo
if [ -z "$EVENTS" ]; then
echo "> No readable Claude execution file was produced — see the agent step logs."
elif [ -n "$REPORT" ]; then
echo "$REPORT"
else
echo "> The agent produced no text output — see the agent step logs."
fi
if [ -n "$META" ]; then
echo
echo "<details><summary>Run metadata</summary>"
echo
echo '```json'
echo "$META"
echo '```'
echo
echo "</details>"
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: Surface Claude error and fail on is_error
if: always()
run: git show "$BASE_SHA:scripts/iyarc-prune.js" | node - write-agent-summary

- name: Require successful Claude completion
if: always() && steps.claude.outcome != 'skipped'
env:
BASE_SHA: ${{ steps.checkout.outputs.commit }}
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
run: |
set -euo pipefail
if [ -z "${EXECUTION_FILE:-}" ] || [ ! -f "$EXECUTION_FILE" ]; then
echo "::error::no Claude execution file produced"; exit 1
fi
EVENTS="$(jq -c 'if type=="array" then . else [.] end' "$EXECUTION_FILE")"
echo "=== result event (metadata only) ==="
echo "$EVENTS" | jq -c 'map(select(.type=="result"))[-1] // empty | del(.result)'
echo "=== assistant text turns (no tool output) ==="
echo "$EVENTS" | jq -r '.[] | select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text'
RES="$(echo "$EVENTS" | jq -c 'map(select(.type=="result"))[-1] // empty')"
if [ "$(echo "$RES" | jq -r '.is_error // false')" = "true" ]; then
echo "::error::Claude returned is_error=true"; exit 1
fi
run: git show "$BASE_SHA:scripts/iyarc-prune.js" | node - require-successful-completion

- name: Check changes are ready for a pull request
id: changes
env:
BASE_SHA: ${{ steps.checkout.outputs.commit }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IYARC_PRUNE_PR_BODY: ${{ runner.temp }}/iyarc-prune-pr.md
run: git show "$BASE_SHA:scripts/iyarc-prune.js" | node - prepare-pull-request

- name: Create signed dependency pull request
if: steps.changes.outputs.ready == 'true'
id: pull-request
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
Comment thread
zahin-mohammad marked this conversation as resolved.
Resolved
sign-commits: true
branch: osv-scanner-prune/${{ github.run_id }}
base: master
add-paths: |
osv-scanner.toml
package.json
yarn.lock
:(glob)modules/*/package.json
commit-message: |
chore(deps): prune resolved vulnerability exclusions

Ticket: HSM-429
title: 'chore(deps): prune resolved vulnerability exclusions'
body-path: ${{ runner.temp }}/iyarc-prune-pr.md
labels: automated,dependencies,security
assignees: gokulhost
draft: false

- name: Report pull request
if: steps.changes.outputs.ready == 'true'
env:
BASE_SHA: ${{ steps.checkout.outputs.commit }}
PR_URL: ${{ steps.pull-request.outputs.pull-request-url }}
run: git show "$BASE_SHA:scripts/iyarc-prune.js" | node - report-pull-request
Loading
Loading