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
166 changes: 166 additions & 0 deletions .github/workflows/sdd-preflight.yml
Original file line number Diff line number Diff line change
@@ -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: component<TAB>mode<TAB>path (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: regex<TAB>component<TAB>mode
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" <<COMMENTEOF
<!-- sdd-preflight -->
## ⛔ 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" <<COMMENTEOF
<!-- sdd-preflight -->
## ⚠️ 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("<!-- sdd-preflight -->")) | .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
86 changes: 86 additions & 0 deletions .specify/constitutions/runner.md
Original file line number Diff line number Diff line change
@@ -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 <TOOL>_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/<name>/`.
- 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.
47 changes: 47 additions & 0 deletions .specify/sdd-manifest.yaml
Original file line number Diff line number Diff line change
@@ -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/<component>.md
# 2. Create its spec in .specify/specs/<component>.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
Loading
Loading