Thank you for contributing. This document describes the Git workflow, branch naming conventions, commit message standards, and pull request process for this project.
See also: AGENTS.md for AI-agent contributor standards and GIT_WORKFLOW_STANDARDS.md for detailed branch lifecycle.
- Quick Setup
- Branch Naming Conventions
- Commit Message Standard
- Pull Request Process
- Merge Strategy
- Release & Tagging Process
- Protected Branches
- Container Image Builds
- Architecture Extension Guidelines
After cloning, run the hook installer to enforce commit standards locally:
bash scripts/setup_git_hooks.shThis installs:
- A commit message template (
.gitmessage) shown in your editor on everygit commit - A commit-msg hook that rejects non-Conventional-Commits messages
- A pre-push hook that checks the refs you push, not the branch you have checked out: it refuses any push to
mainor to a branch name the CI branch validator rejects (rc-v*included), and lets tags through
| Purpose | Pattern | Example |
|---|---|---|
| New feature | feat/<short-description> |
feat/redis-rate-limiter |
| Bug fix | fix/<short-description> |
fix/oscal-uuid-collision |
| Documentation | docs/<short-description> |
docs/stpa-control-diagram |
| Refactor | refactor/<short-description> |
refactor/gateway-middleware |
| CI / tooling | ci/<short-description> |
ci/pin-actions-sha |
| Hotfix on release | hotfix/<version>-<description> |
hotfix/2.0.1-redis-timeout |
| Release candidate | rc-v<semver> |
rc-v3.2.0 |
| Experiment / spike | spike/<short-description> |
spike/cbf-formal-proof |
| Dependency updates, tooling | chore/<short-description> |
chore/update-deps |
| Test additions | test/<short-description> |
test/cbf-chaos-suite |
Rules:
- Use lowercase kebab-case only — no underscores, no uppercase
- Keep descriptions short (≤ 30 chars after the prefix)
- Delete branches after merge
This project follows Conventional Commits v1.0.0.
<type>(<scope>): <short summary>
[optional body]
[optional footer(s)]
| Type | When to use |
|---|---|
feat |
A new feature visible to users or operators |
fix |
A bug fix |
docs |
Documentation only |
style |
Formatting, whitespace — no logic change |
refactor |
Code restructuring — no feature or fix |
perf |
Performance improvement |
test |
Adding or correcting tests |
chore |
Build system, dependency updates |
ci |
CI/CD pipeline changes |
revert |
Reverts a previous commit |
Use one of: gateway, compliance, infra, governance, tests, docs, ci, agentsight, advisor, nemo, opa, ftra, finance, healthcare, security, imports
- Subject line ≤ 72 characters
- Use imperative mood: "add", not "added" or "adds"
- No period at end of subject line
- Separate subject from body with a blank line
- Body explains what and why, not how
- Breaking changes:
!after type/scope, plus aBREAKING CHANGE:footer (both must be present together).
feat(gateway): add Redis-backed rate limiter for OPA policy calls
Implements token-bucket rate limiting at the gateway layer to prevent
OPA from being overwhelmed during burst traffic. Limit is configurable
via GOVERNANCE_THRESHOLDS_PATH.
Closes #42
fix(compliance): correct OSCAL component UUID collision on re-export
UUIDs were regenerated on every export, breaking idempotency checks
in the Lula validation pipeline. Now uses deterministic UUID v5
derived from component name + system identifier.
chore(ci)!: pin actions/checkout to SHA for supply-chain hardening
BREAKING CHANGE: Workflow callers must update their local cache.
- Branch from the current integration branch (
rc-v<next>ormain) - Commit using Conventional Commits (the hook enforces this)
- Push your branch and open a PR against the integration branch
- Fill in the PR template completely
- Ensure CI passes — all checks must be green before merge
- Request review from at least one maintainer
- Squash-merge — use "Squash and merge" so each PR becomes one clean commit on the integration branch
The PR title becomes the squash-merge commit message. It must follow Conventional Commits format:
feat(gateway): add Redis rate limiter
Squash merge is the only permitted strategy for all PRs into main, including
release integration branches (rc-v<version>). This applies uniformly regardless
of scenario:
| Scenario | Strategy |
|---|---|
| Feature / fix PR → integration branch | Squash merge |
Integration branch → main (release) |
Squash merge |
Hotfix → main + integration branch |
Cherry-pick, each landing via its own squash-merged PR |
Deprecated: An earlier version of this document described release integration merges as "merge commit (preserves release boundary)". That guidance is superseded — a
squash-merge-guardCI job (.github/workflows/ci.yml) now fails the build on any two-parent merge commit reachingmain. The release boundary is preserved by the annotated git tag, not by commit topology.
Never force-push to main or rc-v* branches.
- Freeze the integration branch (
rc-v<version>) - Update
CHANGELOG.md— add release date under the version header - Create an annotated tag:
git tag -a v3.2.0 -m "release: v3.2.0 — Cybernetic Governance Engine" git push origin v3.2.0 - Open a PR from the integration branch into
mainand squash-merge it (see Merge Strategy above — merge commits are not permitted, even for releases) - Create a GitHub Release from the tag, copying the CHANGELOG section as the body
v<MAJOR>.<MINOR>.<PATCH>[-<pre-release>]
Examples: v3.2.0, v3.2.0-rc.1, v3.2.0-dev.1
GitHub enforces three rulesets on main: one approving review (repository admins bypass
it, so the maintainer's own merges are admin merges; POAM-2026-115), the eight required
status checks, each pinned to GitHub Actions, and no force push or deletion. No ruleset
covers rc-v* or release/**; create one before creating such a branch. Release tags
(v*, client-v*) cannot be deleted, moved or force-pushed.
The local pre-push hook refuses pushes to main, and to rc-v* through its branch-name check, whichever branch is checked out. Tags pass, so step 3 of the release process works from main. The hook runs only where it is installed; the rulesets are what GitHub enforces.
.github/branch-protection-rules.md is the one
description of the repository's protection: the rulesets, the required check names, merge
settings, security features and workflow permissions, each as the GitHub API returned it,
with the commands that read it. repo-rules.yml compares its rulesets and required checks
with the live configuration on every pull request, every push to main and daily. Change
the ruleset, scripts/check_repo_rules.py and that document together.
This repo uses a single generic Cloud Build template (deployment/docker/cloudbuild.image.yaml) and a single canonical attestation script (scripts/attest_image.sh) executed under the dedicated least-privilege Cloud Build identity cage-cloudbuild-<env>@<project>.iam.gserviceaccount.com (infra/targets/gcp-gke/iam.tf).
Path A — Single-service build via cloudbuild.image.yaml
| File | Scope | Required Substitutions |
|---|---|---|
cloudbuild.image.yaml |
gateway, governed-financial-advisor, compliance-bridge, nemo-guardrails, agentsight-ui, cage-opa |
_IMAGE_NAME, _DOCKERFILE, _SHORT_SHA |
cloudbuild.vllm.yaml |
vllm-streamer (E2_HIGHCPU_32, 40-minute timeout) |
_SHORT_SHA |
cloudbuild.lula.yaml |
lula (0.9.5) |
— |
Every Cloud Build config enforces:
serviceAccount: projects/$PROJECT_ID/serviceAccounts/cage-cloudbuild-${_ENVIRONMENT}@$PROJECT_ID.iam.gserviceaccount.com— dedicated build identity (POAM-2026-083)--no-cache— prevents stale Docker layer cache from masking dependency changes- A single immutable
:<_SHORT_SHA>tag — no mutable:latesttag; the SHA tag is the audit-traceable reference required by NIST RMF and ISO 42001 scripts/attest_image.sh— resolves the pushed@sha256digest, signs it withgcloud beta container binauthz attestations sign-and-createusing thebinauthz-attestorkey incage-signing-<env>, and fails closed by verifying the attestation viagcloud beta container binauthz attestations listlogging: CLOUD_LOGGING_ONLY— structured log routing to Cloud Logging (mandatory with a custom Cloud Build SA)
When to use: Rebuilding a single service after a targeted change, or when a GCP trigger fires automatically on main push.
# Rebuild compliance-bridge manually:
ONLY_IMAGE=compliance-bridge PROJECT_ID=<PROJECT_ID> bash scripts/build_images.sh
# Or invoke the template directly:
gcloud builds submit --config=deployment/docker/cloudbuild.image.yaml \
--project=<PROJECT_ID> \
--service-account="projects/<PROJECT_ID>/serviceAccounts/cage-cloudbuild-staging@<PROJECT_ID>.iam.gserviceaccount.com" \
--substitutions="_IMAGE_NAME=compliance-bridge,_DOCKERFILE=src/compliance_bridge/Dockerfile,_SHORT_SHA=$(git rev-parse --short HEAD)" .scripts/build_images.sh builds first-party services in parallel by submitting deployment/docker/cloudbuild.image.yaml (and deployment/docker/cloudbuild.vllm.yaml) across name|dockerfile pairs. IMAGES selects which; the default is all six: advisor, vLLM streamer, gateway, AgentSight UI, compliance bridge and NeMo Guardrails. It captures the short git SHA from git rev-parse --short HEAD and refuses to run outside a git checkout. MIRROR_THIRD_PARTY_IMAGES (default true) also mirrors and attests third-party images via scripts/mirror_and_attest_images.sh.
deploy_all.sh calls this script for every gcp-gke deploy with the images the profile deploys. It then renders their digests with scripts/render_image_digests.sh into a gitignored var file, and only then plans. No digest is committed (POAM-2026-131).
When to use: Full-stack deploys, CI pre-deploy steps, or when you need all services rebuilt from a clean state.
export PROJECT_ID=<your-gcp-project>
bash scripts/build_images.sh
scripts/render_image_digests.sh # writes infra/targets/gcp-gke/image_digests.<short_sha>.tfvars.jsonThey are not in conflict — both submit deployment/docker/cloudbuild.image.yaml:
| Concern | Path A (single service) | Path B (build_images.sh) |
|---|---|---|
| Trigger | ONLY_IMAGE=<name> or GCP Console trigger |
Developer / deploy_all.sh |
| Scope | One service | The selected services in parallel |
| Service account | cage-cloudbuild-<env>@<project>.iam.gserviceaccount.com |
cage-cloudbuild-<env>@<project>.iam.gserviceaccount.com |
| SHA source | _SHORT_SHA substitution (required, no default) |
git rev-parse --short HEAD (required) |
| Use case | Targeted single-service rebuild | Full-stack pre-deploy fan-out |
- Create
src/<service>/Dockerfilewith the repo root as build context. - Add
"<service>|src/<service>/Dockerfile"toFIRST_PARTY_IMAGESand<service>toALL_IMAGESinscripts/build_images.sh, and add<service>toFIRST_PARTY_IMAGESinscripts/render_image_digests.sh. - Add
<service>tolocal.required_image_digestsininfra/targets/gcp-gke/main.tf, gated like the module that deploys it, and todeploy_all.sh's image selection. Add it to the test-only fixtureimage_digests.tfvarstoo.test_image_digests_rendered.pyfails until the renderer, the builder and Terraform agree. - If the service needs an independent GCP Console trigger, point the trigger at
deployment/docker/cloudbuild.image.yamlwith_IMAGE_NAMEand_DOCKERFILEsubstitutions.
- Python ≥ 3.10, < 3.13
- uv (recommended package manager) or
pip - Docker and Docker Compose (for local infrastructure)
- Node.js ≥ 18 and npm (for
src/agentsight-ui/frontend)
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Clone the repository
git clone https://github.com/google/cybernetic-agent-governance-engine.git
cd cybernetic-agent-governance-engine
# Install all dependencies including dev extras
uv sync --all-groups --all-extras
# Configure environment
cp .env.example .env
# Edit .env and fill in required values (see README.md for variable descriptions)# Set up integration test environment (establishes kubectl port-forward tunnels to GKE — NOT for local unit testing)
bash scripts/setup_test_env.sh
# Run the full unit test suite
uv run pytest tests/ -m "local or unit" -n auto --dist loadscope --no-cov -p no:langsmith --tb=short
# Or use: make test-fast
# Run with integration tests (requires a live cluster)
uv run pytest tests/ --run-integration -v --timeout=120
# Run a specific test file
uv run pytest tests/test_defer_queue.py -v| Target | Purpose |
|---|---|
make test-fast |
Run local + unit tests across multiple workers |
make test-coverage |
Run tests with coverage reporting (70% minimum) |
make lint |
Ruff linting + lockfile check |
make security |
Bandit SAST + pip-audit + Semgrep |
make docs-check |
Documentation integrity gates — broken links, dangling paths, stale symbols, vendor brand and domain literal leakage (mirrors CI gate G9) |
make update-nemo-configmap |
Sync NeMo Guardrails config to K8s ConfigMap |
# Start all services locally (OPA, Gateway, App)
docker compose up
# Start with hot-reload dev overlay (do NOT use in staging/production)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# Verify gateway health
curl http://localhost:8080/healthcd src/agentsight-ui
npm install
npm run devThis project uses ruff for linting and formatting, and mypy for type checking.
# Lint and auto-fix
uv run ruff check src/ tests/ --fix
# Format
uv run ruff format src/ tests/
# Type check
uv run mypy src/ --ignore-missing-importsAll Python source files under src/ must include the Apache 2.0 license header (see License Headers below). The CI license-check job enforces this.
The src/agentsight-ui/ frontend uses ESLint with the config in src/agentsight-ui/eslint.config.js.
cd src/agentsight-ui
npm run lintAll new .py, .ts, .tsx, and .js files under src/ must begin with the Apache 2.0 header:
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.Use scripts/patch_license.py to add missing headers automatically:
uv run python scripts/patch_license.pyPlease use GitHub Issues to report bugs or request features. When filing a bug report, include:
- A clear description of the problem
- Steps to reproduce
- Expected vs. actual behaviour
- Your environment (OS, Python version, deployment target)
- Relevant log output or error messages
Do not report security vulnerabilities through public GitHub issues.
Please follow the process described in SECURITY.md. Security reports are handled with coordinated disclosure and a 5-business-day response SLA.
Contributions to this project must be accompanied by a Contributor License Agreement (CLA). You (or your employer) retain the copyright to your contribution; the CLA gives Google permission to use and redistribute your contributions as part of the project.
If you or your current employer have already signed the Google CLA, no further action is needed. If not, please sign the individual or corporate CLA:
- Individual CLA: https://cla.developers.google.com/about/google-individual
- Corporate CLA: https://cla.developers.google.com/about/google-corporate
The CLA is checked automatically on pull requests via the CLA bot. PRs from contributors who have not signed the CLA will be blocked until the CLA is signed.
Note: This is not an officially supported Google product. The CLA requirement applies to contributions to this repository regardless of its support status.
CAGE provides a domain-agnostic governance kernel (Layer 1) and delegates all domain specifics to optional plugins (Layer 2). When extending the architecture, contributors must adhere to strict boundary rules.
A domain or jurisdiction tier subclasses exactly one of the two base classes in contracts.py: ReadOnlyTier or MutatingTier (ADR-009). The kind is nominal: a tier never declares phase, it is derived from the base class. The governor refuses a tier that subclasses neither, and refuses a ReadOnlyTier that defines commit, rollback or confirm. Plugins declare api_version = "2.0".
Phase 1 tiers must be strictly read-only. They may inspect the request, query external systems, execute causal refutations, or require human approval, but they must not mutate state.
evaluate()returnslist[Violation]. An empty list represents allowance; non-empty lists contain structuredViolationobjects. TheSymbolicGovernorevaluates violations and determines the terminal governance decision.- A HARD violation in Phase 1 halts the pipeline immediately, ensuring no Phase 2 mutations occur.
Phase 2 tiers (e.g. the CBF and fiscal tiers) reserve state. They implement evaluate() as a side-effect-free preview plus three hooks:
commit()reserves atomically and returns(list[Violation], CommitReceipt | None). The receipt is not None if and only if state was mutated.rollback(receipt)releases the reservation. It runs LIFO if a later Phase 2 tier fails, and after a sealed action that was not carried out.confirm(receipt)makes the reservation permanent. It runs only after the actuator accepted the action (SymbolicGovernor.settle(seal, executed=True)). A tier whose commit is already final implements it as a no-op.- A reservation that is never settled (the process died between seal and actuation) must expire on the tier's own clock.
All integrations and operational tooling (Layer 3) must communicate with the core kernel (Layer 1) exclusively through the seam contracts defined under src/gateway/governance/seams/. Direct circular imports into src/gateway/ are prohibited.
NormativeProvider: Interface for synchronous gate adapters (e.g., retrieving limit baselines, validating dynamic bounds).AttestationProvider: Interface for evidence attestation, requiring cryptographic signature verification against a key manifest (e.g., Ed25519 CER checks). Implementations must fail closed on unknown keys.ExecutionActuator: Interface for the final dispatch of theALLOWstate. Actuators receive the evaluated payload wrapped in a routing seal and must fail closed if therecord_hashis missing or tampered.