Skip to content

feat: keyless Foundry auth for the ADO LLM gates (Fold E) - #48

Merged
MCKRUZ merged 10 commits into
MCKRUZ:masterfrom
splashthree:ado-fold-e-foundry-auth
Aug 10, 2026
Merged

feat: keyless Foundry auth for the ADO LLM gates (Fold E)#48
MCKRUZ merged 10 commits into
MCKRUZ:masterfrom
splashthree:ado-fold-e-foundry-auth

Conversation

@splashthree

Copy link
Copy Markdown
Contributor

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 == kit check 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_KEY variable 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 User role 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 original elif byte-for-byte)
  • claude-review.yml: compile-time FOUNDRY_* parameters; continueOnError AzureCLI@2 token-mint step (issecret=true) — a transient mint failure cannot turn the advisory grader blocking, while blocking rails still fail closed downstream
  • grader/correctness/security: FOUNDRY_* YAML variables + pass-throughs
  • RAILS.md/README: provisioning runbook incl. the tenant nuance ("Azure AI User" doesn't exist everywhere — grant Cognitive Services User), connection pre-authorization, queue-time failure shape, local-drill gotcha
  • scripts/tests/test_run_claude_review.py: 13-case fake-npx behavior suite

Evidence

  • Live spike (sandbox Foundry, exact rail invocation): FOUNDRY-SPIKE-OK end-to-end; Entra chain authenticates; pins mandatory; findings in docs/proposals/ado-repo-support-matrix.md
  • ADO template-expression semantics verified against learn.microsoft.com; adversarial review applied (grader fail-soft fix, macro-scrub coverage, LF-pinned shims)
  • Fresh-install e2e + upgrade idempotency; GitHub control matches the golden tree, zero FOUNDRY_ leakage; suite 899 passed
  • Doctor needs no change: it reads what pipelines reference — a dropped variable group stops being demanded, by design

Deferred 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

splashthree and others added 10 commits August 7, 2026 19:05
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
splashthree marked this pull request as ready for review August 10, 2026 01:43
@MCKRUZ
MCKRUZ merged commit e95c3a3 into MCKRUZ:master Aug 10, 2026
7 of 8 checks passed
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