feat: keyless Foundry auth for the ADO LLM gates (Fold E) - #48
Merged
Conversation
First slice of Azure DevOps repo support (plan: docs/proposals/ado-repo-support-matrix.md). - profiles/ado-enterprise/ — microsoft-enterprise's stack (C#/.NET 8 + Angular 17 + Azure, SOC 2 gates) with platform: azure-devops, so /sdlc-setup composes the existing Azure Pipelines CI/CD pack. Purely additive; auto-discovered. - docs/proposals/ado-repo-support-matrix.md — the folded fix matrix (Folds 0/A-E): installer platform-awareness, pack-aware doctor, PR-flow rails, prose sweep, keyless Foundry+RBAC gate auth; additive guarantee + platform-switch runbook. - Profile added to the five curated enumerations (sdlc-setup command, README, SKILL.md, docs/commands.md, docs/profiles.md, docs/architecture.md tree). - New TestOnDiskProfiles guard: every profiles/*/profile.yaml must pass the real schema and profile_id must equal the directory name (the compliance-gate lookup contract). Future profiles get CI coverage the moment the directory exists. - Fixed fictional plugin.json profile-registry docs (architecture.md 1.1/1.2, profiles.md Step 5): profiles are directory-listing auto-discovered; the real .claude-plugin/plugin.json is metadata-only. Verified: 818 tests pass; fresh-repo e2e drill (init -> harness install -> gates -> upgrade dry-run: 0 CONFLICT / 0 RETIRED); byte-level diff vs sibling profile clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (Fold A) _copy_core now consults stack.ci_cd.platform via _CORE_LAYOUT_BY_PLATFORM: azure-devops installs drop the GitHub-only payload (workflows/, rulesets, CODEOWNERS, apply-branch-protection.sh) and redirect the neutral governance content (rubrics, eval-bypasses, dependency-exceptions ledgers) to .azuredevops/rails/ — where the ADO pipelines actually read them (fixes the live bug where <<RUBRIC_DIR>> pointed at a path nothing created). rails-telemetry.schema.json intentionally stays at .github/: both packs' telemetry pipelines commit their report to .github/rails-telemetry.json on purpose (one canonical path so the operator-side fleet collector reads a mixed GitHub/Azure fleet); the schema stays beside its report. github-actions and core-only installs fall through to the identity layout — byte-for-byte unchanged, proven by the untouched enterprise-tree.txt golden. New: ado-enterprise golden-repo test + tree snapshot, synthetic core-layout unit tests, upgrade-idempotency coverage. Suite: 834 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
installed_platform() reads the manifest's composed pack ids; on cicd/azure-devops the platform-facing checks follow the platform: - Tools: require az (+ azure-devops extension), never gh — demanding gh on an ADO repo is telling the user to fix a working setup - Repo secrets: required_variable_groups() reads `- group: NAME` references from the installed pipelines (quote-aware — exotic legal names survive; <<VARIABLE_GROUP>>/sentinel/template expressions are unfinished setup, not missing groups) and checks them via az pipelines variable-group list - Branch protection: az repos policy list; enforcing = isEnabled AND isBlocking (an enabled-but-optional policy is advice, not a gate) - Residual tokens: .azuredevops/ joined the scan roots — <<GATED_PATHS>> in security.yml was invisible before az hardening from adversarial review: shutil.which resolution (az.cmd on Windows), cwd=repo so org/project detection reads --repo's remote not the doctor's CWD, --only-show-errors so az stderr chatter never corrupts the JSON parse, list-shape guard, TypeError-safe manifest read. GitHub/core-only installs are byte-identical (verified against the HEAD doctor on scratch installs: offline+online outputs diff-clean); the gh check bodies are renamed originals. Existing tests unmodified; +30 beside them. Suite: 867 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iter (Fold C)
- review-gate.{sh,ps1}: `az repos pr create` joins git push / gh pr create as
a gated trigger (segment-anchored; a mention inside echo/quotes still walks
free). Shared-payload hardening that also strengthens the GitHub twins,
stated plainly: leading VAR=value prefixes are stripped before matching
(AZURE_DEVOPS_EXT_PAT=... az repos pr create is a documented az auth
pattern and was a bypass — as was FOO=1 git push), and the ps1 twin now
splits segments on newlines like the sh twin always did (a multi-line
command with the trigger on a later line walked past it on Windows).
- New ADO pack settings fragment merged into .claude/settings.json:
read-only az queries allowed (az repos pr SHOW — az has no 'pr view'; the
adversarial review caught that gh-vocabulary bug after unit tests had
pinned it), mutating az + configure-branch-policies.sh behind ask,
.azuredevops/** gated like .github/**. GitHub installs get none of it
(starter-install test pins the absence).
- pr-writer skill names both platforms (az repos pr create, --labels
risk:high — flag verified against the live CLI).
Verified: install+upgrade agree byte-for-byte on the merged settings (68/68
IDENTICAL dry-run); GitHub control matches the golden tree exactly; the real
.sh hook drilled against a 14-case trigger table incl. adversarial forms
(pwsh twin runs the same table where pwsh exists). Suite: 886 passed.
Deferred, documented: wrapper/subshell forms (env/nohup/bash -c) remain the
parser's coverage boundary, in parity with git push since day one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…forms (Fold D) One editorial rule applied everywhere: prose that named only the GitHub mechanism now names the ADO analogue that already exists. Setup Step 7 offers configure-branch-policies.sh beside apply-branch-protection.sh; sdlc-doctor.md describes the pack-aware doctor (gh/az); the harness tour (HARNESS.md, README.md install map, CLAUDE.md.template examples) covers the .azuredevops/ layout the platform-aware installer actually produces; phases 03/08/09 pair Key Vault+variable groups, Azure Artifacts, ADO Boards; the close-phase harness audit checks either pipelines home; doctor's DEV_ENVIRONMENT token owner text goes platform-neutral. New: "Switching CI platform" runbook in sdlc-upgrade.md — the profile edit → dry-run → RETIRED/NEW swap report → human deletes retired files → doctor + shakedown drills flow, clean since Fold A made the core platform-aware. Also: sdlc-setup.md's stale <<ADO_ORGANIZATION>> references corrected to the ADO_ORGANIZATION_NOT_SET sentinel the pack actually installs (argv-safe form, per the pack's own docs). The .github/RAILS.md path is deliberately unchanged everywhere — the ADO pack overlays that path by design. Plan doc updated to match reality: folds 0/A–D consolidated in PR MCKRUZ#47 (one commit per fold), E separate and spike-first; per-fold DONE markers with landed SHAs. Suite: 886 passed, command-contract lint green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows CI (first run of the new review-gate hook tests) checked out review-gate.sh with CRLF via autocrlf; bash rejects `set -uo pipefail\r` and exits 1 before the hook runs — every sh trigger test failed at the exit-0 assertion. Same class as d67db30, but here the shipped artifact itself was at risk: a plugin cloned on Windows would install broken hooks and rails scripts. .gitattributes pins *.sh to LF everywhere; .ps1 stays platform-default (pwsh is CRLF-tolerant — its 15 Windows tests passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Windows CI leg's `bash` resolves to C:\Windows\System32\bash.exe — the
WSL shim, which with no distro installed exits 1 ("Windows Subsystem for
Linux has no installed distributions") before the hook ever runs; all 17 sh
trigger tests failed on the exit-0 assertion. Git Bash is the bash a real
Windows install executes hooks with, so _bash() finds it explicitly
(ProgramFiles, then the git.exe-relative path) and never falls back to the
shim. Runners now also surface stderr in every assertion so the next
environmental failure names itself instead of reading "got 1".
(The prior .gitattributes fix stands on its own: a CRLF checkout would break
the hooks for real Windows installs regardless of which bash runs them.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n end-to-end Ran the gate against a real ADO repo (RheemDevOps/Expedition_One/test-repo): installed hook denied az repos pr create + git push without receipts, allowed both with receipts, and a real PR (#36931) opened via az then was abandoned + branch deleted. Client project left clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…is one RBAC grant
Live spike against a sandbox Foundry deployment using the exact rail
invocation: end-to-end completion proven (FOUNDRY-SPIKE-OK) with the
CLAUDE_CODE_USE_FOUNDRY config surface; the Entra credential chain
authenticates (the 401 without a role is authZ, not authN — token minted
and presented). Keyless needs exactly one data-plane RBAC grant, and the
role in this tenant is 'Cognitive Services User' ('Azure AI User' does not
exist here). Desktop-env inheritance gotcha documented. Verdict: GO.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…en (Fold E)
The three Claude gates (grader/correctness/security) can now authenticate
with a per-run Entra token minted from a WIF service connection instead of
a stored ANTHROPIC_API_KEY. Opt-in per pipeline via four compile-time
FOUNDRY_* YAML variables; all empty = the emitted steps and the script's
behavior are byte-identical to today (proven: fresh-install e2e + upgrade
dry-run idempotent + GitHub control matches the golden tree with zero
FOUNDRY_ leakage).
- run-claude-review.sh: foundry guard — CLAUDE_CODE_USE_FOUNDRY=1 lifts the
key requirement; fails CLOSED with actionable messages on a missing
resource or the model pin for the alias in use (the spike proved Foundry
has no alias resolution); unresolved ADO $(macro) literals are scrubbed
so a foundry-less pipeline can never leak one into the CLI env.
- claude-review.yml: ${{ if }}-guarded AzureCLI@2 step mints the token
(az account get-access-token --resource cognitiveservices, issecret=true).
continueOnError so a transient mint failure cannot turn the ADVISORY
grader blocking — the blocking rails still fail closed downstream (no
token -> no verdict -> enforce exits 1). Env insertion uses two sibling
documented-form inserts, no nested ${{ if }}.
- RAILS.md/README: provisioning runbook — Cognitive Services User grant
("Azure AI User" does not exist in every tenant), service-connection
pre-authorization, the queue-time failure shape, the desktop-env local
drill gotcha. Doctor needs no change: it reads what pipelines reference,
so a dropped variable group stops being demanded by design.
- 13-case fake-npx behavior suite (LF-pinned shims — the Windows lesson).
ADO template-expression semantics verified against Microsoft docs
(compile-time insertion, secret-variable handoff, service-connection
input resolution). Suite: 899 passed.
Spike evidence in docs/proposals/ado-repo-support-matrix.md: chain
authenticates end-to-end (FOUNDRY-SPIKE-OK), 401-without-role is authZ not
authN, pins mandatory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
splashthree
marked this pull request as ready for review
August 10, 2026 01:43
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.
Fold E of the ADO support plan — stacked on #47 (merge order: #47 → this; until then the diff shows both). Kit mirror: intent-driven-development #46 (itself stacked on idd#45) — the
harness == kitcheck here greens only after both kit PRs merge.What
Opt-in keyless auth via Microsoft Foundry for the ADO pack's three LLM gates. Gate auth becomes a choice: the
ANTHROPIC_API_KEYvariable group (default — byte-identical to today), or a per-run Entra token minted from a WIF service connection — no static Claude credential anywhere, revocation/audit through Entra,Cognitive Services Userrole scoped to the Foundry resource.One commit, spike-first per the plan:
run-claude-review.sh: foundry guard (fail-closed on missing resource/model-pin with actionable messages; ADO$(macro)-literal scrubbing; no-flag path is the originalelifbyte-for-byte)claude-review.yml: compile-timeFOUNDRY_*parameters;continueOnErrorAzureCLI@2 token-mint step (issecret=true) — a transient mint failure cannot turn the advisory grader blocking, while blocking rails still fail closed downstreamFOUNDRY_*YAML variables + pass-throughsCognitive Services User), connection pre-authorization, queue-time failure shape, local-drill gotchascripts/tests/test_run_claude_review.py: 13-case fake-npx behavior suiteEvidence
FOUNDRY-SPIKE-OKend-to-end; Entra chain authenticates; pins mandatory; findings indocs/proposals/ado-repo-support-matrix.mdFOUNDRY_leakage; suite 899 passedDeferred to the client engagement (documented as RAILS.md §Keyless step 6): the live pipeline drill with a real WIF service connection.
🤖 Generated with Claude Code