From 187579923132c0d7a6048ede98dcefe7c0a99f0c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 22:19:44 +0000 Subject: [PATCH] feat(sdd): add SDD infrastructure and runner as first managed component Introduces Spec-Driven Development (SDD) enforcement infrastructure: - SDD manifest (.specify/sdd-manifest.yaml) declaring managed components and their paths, constitutions, specs, and enforcement mode - Runner constitution (.specify/constitutions/runner.md) with 6 principles extracted from PR #1091 patterns (version pinning, freshness automation, dependency procedures, layer discipline, schema sync, bridge modularity) - Runner spec (.specify/specs/runner.md) documenting component boundary, current state, maintenance workflows, and change protocol - SDD preflight CI job (.github/workflows/sdd-preflight.yml) that checks PRs for managed-path modifications and warns or blocks accordingly The runner is the first component onboarded to SDD in warn mode. Additional components can be added to the manifest to gradually migrate the codebase. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sdd-preflight.yml | 166 ++++++++++++++++++++++++++++ .specify/constitutions/runner.md | 86 ++++++++++++++ .specify/sdd-manifest.yaml | 47 ++++++++ .specify/specs/runner.md | 104 +++++++++++++++++ 4 files changed, 403 insertions(+) create mode 100644 .github/workflows/sdd-preflight.yml create mode 100644 .specify/constitutions/runner.md create mode 100644 .specify/sdd-manifest.yaml create mode 100644 .specify/specs/runner.md diff --git a/.github/workflows/sdd-preflight.yml b/.github/workflows/sdd-preflight.yml new file mode 100644 index 000000000..62630186b --- /dev/null +++ b/.github/workflows/sdd-preflight.yml @@ -0,0 +1,166 @@ +name: SDD Preflight + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + pull-requests: write + contents: read + +jobs: + check-managed-paths: + name: SDD boundary check + runs-on: ubuntu-latest + timeout-minutes: 2 + # Skip entirely if PR has sdd-exempt label + if: ${{ !contains(github.event.pull_request.labels.*.name, 'sdd-exempt') }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check SDD boundaries + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + MANIFEST=".specify/sdd-manifest.yaml" + if [ ! -f "$MANIFEST" ]; then + echo "No SDD manifest found, skipping" + echo "violation=false" >> "$GITHUB_OUTPUT" + echo "has_findings=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Get changed files in this PR + CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only) + if [ -z "$CHANGED_FILES" ]; then + echo "No changed files, skipping" + echo "violation=false" >> "$GITHUB_OUTPUT" + echo "has_findings=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Parse all managed components in a single yq call: + # Output format: componentmodepath (one line per path) + DEFAULT_MODE=$(yq '.default-mode // "warn"' "$MANIFEST") + COMPONENT_PATHS=$(yq -r ' + .managed-components | to_entries[] | + .key as $comp | + (.value.mode // "'"$DEFAULT_MODE"'") as $mode | + .value.paths[] | + $comp + "\t" + $mode + "\t" + . + ' "$MANIFEST") + + if [ -z "$COMPONENT_PATHS" ]; then + echo "No managed paths defined, skipping" + echo "violation=false" >> "$GITHUB_OUTPUT" + echo "has_findings=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Convert glob patterns to grep regexes and build a lookup file + # Format: regexcomponentmode + PATTERN_FILE=$(mktemp) + while IFS=$'\t' read -r comp mode pattern; do + # Escape regex special chars in the pattern, then convert globs + regex=$(printf '%s' "$pattern" \ + | sed 's/[.+^${}()|[\]]/\\&/g' \ + | sed 's/\*\*/.*/g' \ + | sed 's/\*/[^\/]*/g') + printf '%s\t%s\t%s\n' "$regex" "$comp" "$mode" >> "$PATTERN_FILE" + done <<< "$COMPONENT_PATHS" + + # Match changed files against patterns + VIOLATIONS="" + WARNINGS="" + + while IFS= read -r changed_file; do + [ -z "$changed_file" ] && continue + while IFS=$'\t' read -r regex comp mode; do + if printf '%s' "$changed_file" | grep -qE "^${regex}$"; then + row="| \`${changed_file}\` | **${comp}** | ${mode} |" + if [ "$mode" = "enforce" ]; then + VIOLATIONS="${VIOLATIONS}${row}"$'\n' + else + WARNINGS="${WARNINGS}${row}"$'\n' + fi + break + fi + done < "$PATTERN_FILE" + done <<< "$CHANGED_FILES" + + rm -f "$PATTERN_FILE" + + # Determine result + if [ -n "$VIOLATIONS" ]; then + echo "violation=true" >> "$GITHUB_OUTPUT" + else + echo "violation=false" >> "$GITHUB_OUTPUT" + fi + + if [ -n "$WARNINGS" ] || [ -n "$VIOLATIONS" ]; then + echo "has_findings=true" >> "$GITHUB_OUTPUT" + else + echo "has_findings=false" >> "$GITHUB_OUTPUT" + fi + + # Build comment body and write to a file (avoids shell injection) + BODY_FILE=$(mktemp) + if [ -n "$VIOLATIONS" ]; then + cat > "$BODY_FILE" < + ## โ›” SDD Preflight โ€” Boundary Violation + + This PR modifies files in SDD-managed component(s) that require changes to go through the designated agent workflow. + + | File | Component | Mode | + |------|-----------|------| + ${VIOLATIONS} + **Action required**: These components are in \`enforce\` mode. Please use the component's agent workflow to make these changes, or request an exemption by adding the \`sdd-exempt\` label. + + ๐Ÿ“– See [SDD Manifest](.specify/sdd-manifest.yaml) for details. + COMMENTEOF + elif [ -n "$WARNINGS" ]; then + cat > "$BODY_FILE" < + ## โš ๏ธ SDD Preflight โ€” Managed Paths Modified + + This PR modifies files in SDD-managed component(s). These components are migrating to Spec-Driven Development. + + | File | Component | Mode | + |------|-----------|------| + ${WARNINGS} + **No action required** โ€” these components are in \`warn\` mode. Consider using the component's agent workflow for future changes. + + ๐Ÿ“– Specs: [Runner Spec](.specify/specs/runner.md) ยท [Runner Constitution](.specify/constitutions/runner.md) + COMMENTEOF + fi + + echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT" + + - name: Comment on PR + if: steps.check.outputs.has_findings == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Delete previous SDD preflight comments (identified by HTML marker) + gh api --paginate "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + --jq '.[] | select(.body | contains("")) | .id' \ + | while read -r comment_id; do + gh api -X DELETE "repos/${{ github.repository }}/issues/comments/${comment_id}" 2>/dev/null || true + done + + gh pr comment "$PR_NUMBER" --body-file "${{ steps.check.outputs.body_file }}" + + - name: Enforce SDD boundaries + if: steps.check.outputs.violation == 'true' + run: | + echo "::error::SDD boundary violation detected. See PR comment for details." + echo "::error::Add the 'sdd-exempt' label to bypass this check." + exit 1 diff --git a/.specify/constitutions/runner.md b/.specify/constitutions/runner.md new file mode 100644 index 000000000..7a076e566 --- /dev/null +++ b/.specify/constitutions/runner.md @@ -0,0 +1,86 @@ +# Runner Constitution + +**Version**: 1.0.0 +**Ratified**: 2026-03-28 +**Parent**: [ACP Platform Constitution](../memory/constitution.md) + +This constitution governs the `components/runners/ambient-runner/` component and its supporting CI workflows. It inherits all principles from the platform constitution and adds runner-specific constraints. + +--- + +## Principle R-I: Version Pinning + +All external tools installed in the runner image MUST be version-pinned. + +- CLI tools (gh, glab) MUST use `ARG _VERSION=X.Y.Z` in the Dockerfile and be installed via pinned binary downloads โ€” never from unpinned package repos. +- Python packages (uv, pre-commit) MUST use `==X.Y.Z` pins at install time. +- npm packages (gemini-cli) MUST use `@X.Y.Z` pins. +- The base image MUST be pinned by SHA digest. +- Versions MUST be declared as Dockerfile `ARG`s at the top of the file for automated bumping. + +**Rationale**: Unpinned installs cause non-reproducible builds and silent regressions. Pinning enables automated freshness tracking and controlled upgrades. + +## Principle R-II: Automated Freshness + +Runner tool versions MUST be checked for staleness automatically. + +- The `runner-tool-versions.yml` workflow runs weekly and on manual dispatch. +- It checks all pinned components against upstream registries. +- When updates are available, it opens a single PR with a version table. +- The workflow MUST NOT auto-merge; a human or authorized agent reviews. + +**Rationale**: Pinned versions go stale. Automated freshness checks balance reproducibility with security and feature currency. + +## Principle R-III: Dependency Update Procedure + +Dependency updates MUST follow the documented procedure in `docs/UPDATE_PROCEDURE.md`. + +- Python dependencies use `>=X.Y.Z` floor pins in pyproject.toml, resolved by `uv lock`. +- SDK bumps (claude-agent-sdk) MUST trigger a review of the frontend Agent Options schema for drift. +- Base image major version upgrades (e.g., UBI 9 โ†’ 10) require manual testing. +- Lock files MUST be regenerated after any pyproject.toml change. + +**Rationale**: A structured procedure prevents partial updates, version conflicts, and schema drift between backend SDK types and frontend forms. + +## Principle R-IV: Image Layer Discipline + +Dockerfile layers MUST be optimized for size and cacheability. + +- System packages (`dnf install`) SHOULD be consolidated into a single `RUN` layer. +- Build-only dependencies (e.g., `python3-devel`) MUST be removed in the same layer where they are last used, not in a separate layer. +- Binary CLI downloads (gh, glab) SHOULD share a single `RUN` layer to avoid redundant arch detection. +- `dnf clean all` and cache removal MUST happen in the same `RUN` as the install. + +**Rationale**: Docker layers are additive. Removing packages in a later layer doesn't reclaim space โ€” it only adds whiteout entries. + +## Principle R-V: Agent Options Schema Sync + +The frontend Agent Options form MUST stay in sync with the claude-agent-sdk types. + +- `schema.ts` defines the Zod schema matching `ClaudeAgentOptions` from the SDK. +- `options-form.tsx` renders the form from the schema. +- Editor components in `_components/` MUST use stable React keys (ref-based IDs) for record/map editors to prevent focus loss on rename. +- Record editors MUST prevent key collisions on add operations. +- The form is gated behind the `advanced-agent-options` Unleash flag. + +**Rationale**: Schema drift between SDK and frontend creates silent data loss or validation errors. Stable keys prevent UX bugs in dynamic form editors. + +## Principle R-VI: Bridge Modularity + +Agent bridges (Claude, Gemini, LangGraph) MUST be isolated modules. + +- Each bridge lives in `ambient_runner/bridges//`. +- Bridges MUST NOT import from each other. +- Shared logic lives in `ambient_runner/` (bridge.py, platform/). +- New bridges follow the same directory structure and registration pattern. + +**Rationale**: Bridge isolation enables independent testing, deployment, and addition of new AI providers without cross-contamination. + +--- + +## Governance + +- This constitution is versioned using semver. +- Amendments require a PR that updates this file and passes the SDD preflight check. +- The platform constitution takes precedence on any conflict. +- Compliance is reviewed as part of runner-related PR reviews. diff --git a/.specify/sdd-manifest.yaml b/.specify/sdd-manifest.yaml new file mode 100644 index 000000000..56c942754 --- /dev/null +++ b/.specify/sdd-manifest.yaml @@ -0,0 +1,47 @@ +# SDD Manifest โ€” Spec-Driven Development Enforcement +# +# Components listed here are governed by their spec-kit constitution and spec. +# Changes to managed paths MUST go through the designated agent workflow. +# The sdd-preflight CI job enforces this boundary. +# +# To add a new component: +# 1. Create its constitution in .specify/constitutions/.md +# 2. Create its spec in .specify/specs/.md +# 3. Add an entry below with paths, spec, constitution, and agent +# 4. The preflight job will begin enforcing on the next PR + +version: 1 + +# Platform-wide constitution (all components inherit from this) +platform-constitution: .specify/memory/constitution.md + +# Enforcement mode for new components during migration +# "warn" = comment on PR but don't block; "enforce" = required check +default-mode: warn + +managed-components: + runner: + description: > + Python runner executing Claude Code CLI in Job pods. + Manages AG-UI adapter, MCP integrations, and agent bridges. + paths: + - components/runners/ambient-runner/** + - components/frontend/src/components/claude-agent-options/** + - .github/workflows/runner-tool-versions.yml + constitution: .specify/constitutions/runner.md + spec: .specify/specs/runner.md + mode: warn + added-in-pr: 1091 + # Future: when a GitHub App or bot account is set up for the agent, + # set agent-login to its GitHub username for authorship checks. + # agent-login: ambient-runner-agent[bot] + + # Uncomment to onboard the next component: + # backend: + # description: Go REST API (Gin), manages K8s Custom Resources + # paths: + # - components/backend/** + # constitution: .specify/constitutions/backend.md + # spec: .specify/specs/backend.md + # mode: warn + # added-in-pr: TBD diff --git a/.specify/specs/runner.md b/.specify/specs/runner.md new file mode 100644 index 000000000..bd47f0847 --- /dev/null +++ b/.specify/specs/runner.md @@ -0,0 +1,104 @@ +# Runner Component Spec + +**Version**: 1.0.0 +**Created**: 2026-03-28 +**Constitution**: [Runner Constitution](../constitutions/runner.md) +**Component**: `components/runners/ambient-runner/` + +--- + +## Overview + +The ambient-runner is a Python application that executes AI agent sessions inside Kubernetes Job pods. It bridges AG-UI protocol events to multiple AI providers (Claude, Gemini, LangGraph) and exposes a FastAPI server on port 8001. + +## Component Boundary + +### Managed Paths + +``` +components/runners/ambient-runner/ +โ”œโ”€โ”€ Dockerfile # Runner container image +โ”œโ”€โ”€ main.py # FastAPI entry point +โ”œโ”€โ”€ pyproject.toml # Python dependencies +โ”œโ”€โ”€ uv.lock # Resolved dependency lock +โ”œโ”€โ”€ .mcp.json # MCP server configuration +โ”œโ”€โ”€ ag_ui_claude_sdk/ # Claude AG-UI adapter +โ”œโ”€โ”€ ag_ui_gemini_cli/ # Gemini AG-UI adapter +โ”œโ”€โ”€ ambient_runner/ # Core runner package +โ”‚ โ”œโ”€โ”€ bridges/ # Provider bridges +โ”‚ โ”‚ โ”œโ”€โ”€ claude/ +โ”‚ โ”‚ โ”œโ”€โ”€ gemini_cli/ +โ”‚ โ”‚ โ””โ”€โ”€ langgraph/ +โ”‚ โ”œโ”€โ”€ endpoints/ # FastAPI routes +โ”‚ โ”œโ”€โ”€ middleware/ # Request middleware +โ”‚ โ””โ”€โ”€ platform/ # Platform integration +โ”œโ”€โ”€ tests/ # Test suite +โ””โ”€โ”€ docs/ + โ””โ”€โ”€ UPDATE_PROCEDURE.md # Maintenance procedure + +.github/workflows/ +โ””โ”€โ”€ runner-tool-versions.yml # Automated freshness checks +``` + +### Supporting Frontend Paths + +``` +components/frontend/src/components/claude-agent-options/ +โ”œโ”€โ”€ schema.ts # Zod schema (mirrors SDK types) +โ”œโ”€โ”€ options-form.tsx # Main form component +โ”œโ”€โ”€ index.ts # Barrel exports +โ””โ”€โ”€ _components/ # Per-section editors +``` + +## Current State (as of PR #1091) + +### Base Image +- **UBI 10** (`registry.access.redhat.com/ubi10/ubi@sha256:...`) +- Python 3.12 (system default), Node.js (AppStream), Go (go-toolset) + +### Pinned Tools +| Tool | Dockerfile ARG | Purpose | +|------|---------------|---------| +| gh | `GH_VERSION` | GitHub CLI for repo operations | +| glab | `GLAB_VERSION` | GitLab CLI for repo operations | +| uv | `UV_VERSION` | Python package management | +| pre-commit | `PRE_COMMIT_VERSION` | Git hook framework | +| gemini-cli | `GEMINI_CLI_VERSION` | Google Gemini CLI | + +### Key Dependencies +| Package | Constraint | Role | +|---------|-----------|------| +| claude-agent-sdk | `>=0.1.50` | Claude Code agent SDK | +| anthropic | `>=0.86.0` | Anthropic API client | +| mcp | `>=1.9.2` | Model Context Protocol | +| ag-ui-protocol | `>=0.6.2` | AG-UI event protocol | + +## Maintenance Workflows + +### Weekly: Tool Freshness (`runner-tool-versions.yml`) +- Checks all pinned tools against upstream registries +- Opens a PR if any component has a newer version +- Does not auto-merge + +### Monthly: Dependency Bump (`UPDATE_PROCEDURE.md`) +- Bumps all Python dependencies to latest stable +- Checks for SDK type changes โ†’ syncs Agent Options schema +- Regenerates lock file +- Runs housekeeping (type hints, dead code) + +## Change Protocol + +1. All changes to managed paths MUST go through the SDD workflow when the component is in `enforce` mode, and SHOULD when in `warn` mode (see `sdd-manifest.yaml`). +2. Changes MUST comply with the runner constitution. +3. SDK bumps MUST include a schema sync check. +4. Dockerfile changes MUST maintain version pinning and layer discipline. +5. Test coverage MUST not decrease. + +## Verification Checklist + +- [ ] Container image builds successfully +- [ ] All tests pass (`pytest`) +- [ ] Pre-commit hooks pass +- [ ] `gh version`, `glab version`, `uv --version`, `gemini --version` work in container +- [ ] Agent Options form renders correctly (if schema changed) +- [ ] No `Optional[X]` or `List[X]` style type hints (Python 3.12 uses `X | None`, `list[X]`)