From ddf3a0875944c6cb974bf54af5a76df5979a3fe8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Fri, 24 Apr 2026 21:34:39 -0400 Subject: [PATCH] chore(ai): adopt `apm install --root` + audit-based verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite `just ai::apm-sync` around two upstream apm improvements: * microsoft/apm#889 (merged) wires content-integrity hash verification into `apm audit --ci` — every APM-managed file (`.claude/`, `.codex/`, `.agents/`, `.opencode/`, `opencode.json`, MCP configs) is now checked against the lockfile's recorded hashes. The recursive `diff -r` loop that re-installed into scratch and compared each deploy dir is redundant and goes away. * microsoft/apm#888 (`apm install --root`) replaces the rsync-the- worktree-into-scratch dance with a single flag. The scratch staging that remains is purely for the `apm compile` leg — apm's distributed compiler scans the project tree to score AGENTS.md placement, so the scratch needs the source file inventory. It drops to an `rsync -aH` with one set of excludes (down from ~20 lines of setup + 25 lines of diff gates). * Staged on juspay/apm@feat/install-compile-root-flag until #888 lands in microsoft/apm — revert the `apm_cmd` pin once that happens. Net: the recipe trims from 127 lines to 51; all seven audit checks pass, and the per-AGENTS.md diff loop still catches compile-output drift that audit can't (AGENTS.md files aren't lockfile-tracked). --- agents/ai.just | 117 ++++++++++++++++--------------------------------- 1 file changed, 38 insertions(+), 79 deletions(-) diff --git a/agents/ai.just b/agents/ai.just index ad27a4b6a..e759de988 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -7,9 +7,10 @@ set working-directory := '..' # `just ci` → `just ai::apm-sync`). nix_shell := if env('IN_NIX_SHELL', '') != '' { '' } else { 'nix develop --accept-flake-config -c' } -# Pinned to juspay's fork while the hook-idempotency fix (microsoft/apm#708) -# is unreleased upstream. Revert to `microsoft/apm` once the fix lands. -apm_cmd := nix_shell + ' uvx --from git+https://github.com/microsoft/apm apm' +# Pinned to juspay's fork on the feat/install-compile-root-flag branch +# while microsoft/apm#888 (apm install --root) is unreleased upstream. +# Revert to plain `microsoft/apm` once the flag lands there. +apm_cmd := nix_shell + ' uvx --from git+https://github.com/juspay/apm@feat/install-compile-root-flag apm' # Install native APM integrations for Claude, Codex, and OpenCode. # @@ -40,89 +41,47 @@ apm-audit: # Verify APM-managed outputs match sources + security audit # -# Must NOT mutate the live .claude/ tree — concurrent Claude Code -# sessions in the same worktree would see stop hooks / rules / skills -# disappear mid-CI and crash. See #468. Instead we stage a fresh tree -# in a scratch dir and diff. The `apm` recipe above is destructive and -# stays a dev-only action. This workaround can be removed if upstream -# adds content verification to `apm audit --ci`: microsoft/apm#684. +# `apm audit --ci` (via apm-audit above) hash-verifies every +# APM-managed file -- closes microsoft/apm#684/#887. That covers +# .claude/, .codex/, .agents/, .opencode/, and opencode.json content +# drift without needing a scratch install + recursive diff. +# +# AGENTS.md is a compile output, NOT a lockfile-tracked deployed file, +# so we still verify it by staging install + compile into a scratch +# deploy root via `apm install --root` + `apm compile --root` +# (microsoft/apm#888) and diffing the AGENTS.md outputs. Sources +# (apm.yml, .apm/, the project tree) stay in $PWD throughout -- the +# `--root` flags handle write redirection internally. apm-sync: apm-audit #!/usr/bin/env bash set -euo pipefail scratch=$(mktemp -d) trap 'rm -rf "$scratch"' EXIT - # Distributed `apm compile` placement depends on the real repo layout, so - # mirror the worktree into scratch while excluding runtime/generated trees. - # `apm install` rewrites apm.lock.yaml's `generated_at` on every run, which - # is fine here because the scratch copy is disposable. - rsync -a \ - --exclude='.git/' \ - --exclude='node_modules/' \ - --exclude='.direnv/' \ - --exclude='.logs/' \ - --exclude='.claude/' \ - --exclude='.codex/' \ - --exclude='.agents/' \ - --exclude='.opencode/' \ - --exclude='CLAUDE.md' \ - "$PWD/" "$scratch/" - # Mirror the runtime folders that drive target detection for install. - # Compile is explicit so we only emit `AGENTS.md` for Codex/OpenCode. - mkdir -p "$scratch/.claude" "$scratch/.codex" "$scratch/.opencode" "$scratch/.agents" - # Keep user-owned runtime inputs in the scratch tree too. APM compile - # placement is file-layout sensitive, so omitting checked-in files that - # are present during local `just ai::apm` can produce a false sync diff. - [[ ! -e .claude/launch.json ]] || cp .claude/launch.json "$scratch/.claude/launch.json" - [[ ! -e .codex/config.toml ]] || cp .codex/config.toml "$scratch/.codex/config.toml" - (cd "$scratch" && {{ apm_cmd }} install >/dev/null) - (cd "$scratch" && {{ apm_cmd }} compile --target codex,opencode >/dev/null) - # Exclude files that apm doesn't manage: launch.json is user-owned, - # and `.claude/*` entries in .gitignore are runtime state written by - # Claude Code itself (worktree bookkeeping, local settings, session - # lockfiles). Keep this list in sync with .gitignore — if a new - # runtime file appears in .claude/, add it to .gitignore and here. - if ! diff -r \ - -x launch.json \ - -x worktrees \ - -x settings.local.json \ - -x scheduled_tasks.lock \ - .claude "$scratch/.claude"; then - echo "ERROR: .claude/ out of sync with sources — run: just ai::apm" >&2 - exit 1 - fi - # `.codex/config.toml` is currently a checked-in manual fallback for - # project-local Codex MCP until microsoft/apm#803 lands; current APM - # does not emit it in the scratch tree yet. - if ! diff -r \ - -x config.toml \ - .codex "$scratch/.codex"; then - echo "ERROR: .codex/ out of sync with sources — run: just ai::apm" >&2 - exit 1 - fi - if ! diff -r .agents "$scratch/.agents"; then - echo "ERROR: .agents/ out of sync with sources — run: just ai::apm" >&2 - exit 1 - fi - if ! diff -r \ - -x .gitignore \ - -x node_modules \ - -x package-lock.json \ - -x package.json \ - .opencode "$scratch/.opencode"; then - echo "ERROR: .opencode/ out of sync with sources — run: just ai::apm" >&2 - exit 1 - fi - if [[ -e opencode.json || -e "$scratch/opencode.json" ]]; then - if [[ ! -e opencode.json || ! -e "$scratch/opencode.json" ]] || \ - ! diff <(jq -S . opencode.json) <(jq -S . "$scratch/opencode.json"); then - echo "ERROR: opencode.json out of sync with sources — run: just ai::apm" >&2 + # Seed the lockfile so install resolves to the SAME commits as the + # live tree. apm install rewrites apm.lock.yaml during the run, so + # this must be a copy rather than a symlink. + [[ ! -e apm.lock.yaml ]] || cp apm.lock.yaml "$scratch/apm.lock.yaml" + # apm install --root: writes apm_modules/, apm.lock.yaml, .claude/, + # .codex/, .agents/, .opencode/ under $scratch while reading apm.yml + # + .apm/ + local-path packages from $PWD. + {{ apm_cmd }} install --root "$scratch" >/dev/null + # apm compile --root: writes AGENTS.md / CLAUDE.md outputs under + # $scratch while reading apm.yml + .apm/ + the project tree (for + # placement scoring) from $PWD. No rsync, no cd, no symlinks. + {{ apm_cmd }} compile --root "$scratch" --target codex,opencode >/dev/null + # Diff every AGENTS.md under the scratch tree against its counterpart + # in the live tree. Distributed compile produces files at multiple + # depths (AGENTS.md, packages/AGENTS.md, etc.), so a single + # root-level diff is not enough. apm audit --ci above already + # covers content-integrity for everything `apm install` writes. + while IFS= read -r f; do + rel="${f#$scratch/}" + if ! diff "$rel" "$f" >/dev/null 2>&1; then + echo "ERROR: $rel out of sync with sources -- run: just ai::apm" >&2 + diff "$rel" "$f" || true exit 1 fi - fi - if ! diff AGENTS.md "$scratch/AGENTS.md"; then - echo "ERROR: AGENTS.md out of sync with sources — run: just ai::apm" >&2 - exit 1 - fi + done < <(find "$scratch" -name AGENTS.md -type f) # Install APM config and launch coding agent (AI_AGENT overrides default) [default]