Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agent Governance-as-Code

Agent policies defined alongside agent code, versioned together in Git, and automatically validated, deployed, and enforced as part of CI/CD — so the running agent and its governance record can never silently diverge the way they do today.

This repo is a complete, runnable reference implementation, not a design doc. Everything described below has been executed end-to-end and is covered by an automated test suite (45 tests, all passing).

git log --oneline
cfafcef Add bonus: environment promotion workflow (dev/staging/prod) ...
49f7a32 Revert "agent v1.1.0: raise max_tokens to 8192, tighten HITL risk_threshold to high"
2963cbf agent v1.1.0: raise max_tokens to 8192, tighten HITL risk_threshold to high
0b0e655 Initial commit: support-triage-agent v1.0.0 + policy v1

That log is itself a demo: commit 2963cbf is a real "agent update" (code + policy changed together), and 49f7a32 is a real "agent rollback" (git revert) — restoring both the agent behavior and its governance record in the exact same commit. That's the core problem this framework fixes.


1. Why this design

The failure mode in the prompt — "the agent's code is versioned in Git, the policy is not; the agent is updated or rolled back, the policy isn't" — happens because the policy lives in a separate system from the code (a governance platform UI, a wiki, a spreadsheet) with its own independent lifecycle. The fix is structural, not procedural:

  1. The policy file lives in the agent's own repo, next to its code, and is versioned by the same commits, the same PRs, the same branches, the same reverts. There is no separate "governance record" to forget to update — updating the agent is updating the policy, because they're in the same diff.
  2. Git is the source of historical truth. The versioning endpoint doesn't maintain its own history table (which could itself drift) — it reads directly from Git's object database (git show <sha>:<path>). Any commit that ever existed can be queried, forever.
  3. The enforcement runtime is a separate, mutable system by necessity (something has to actually gate the agent's tool calls in real time), which reintroduces exactly one seam where drift can creep back in: someone editing the runtime directly. The drift detector exists specifically to police that seam.

2. What's in the repo

agent/
  src/agent_main.py                 # stand-in agent code
  policy-by-env/{dev,staging,prod}/ # one policy file per environment — single
                                     # source of truth for deploy, versioning,
                                     # drift-check, AND promotion (bonus)
agents.yaml                         # agent_id -> per-environment policy path registry
schema/policy.schema.yaml           # the policy schema (data, not code)
governance/
  schema.py            # validator (required fields, types, enums, cross-field rules)
  git_store.py          # policy versioning endpoint logic (reads Git history)
  registry.py            # agent_id + environment -> policy path lookups
  enforcement_store.py    # simulated policy enforcement runtime
  drift.py                 # drift detector (enforced vs Git HEAD)
  promotion.py              # bonus: faithful-promotion check (dev/staging/prod)
  api.py                     # REST API exposing the above over HTTP
cli/
  validate_policy.py    # CI step 1: block on invalid/incomplete policy
  deploy_policy.py        # CI step 2: push validated policy to the runtime
  check_drift.py            # scheduled/on-demand drift check -> alert
  promote_policy.py          # bonus: generate/verify environment promotions
.github/workflows/
  deploy-agent-policy.yml   # validate + deploy on push to main
  drift-check.yml            # scheduled drift detection (cron)
  policy-promotion.yml        # bonus: PR check gating promotion to prod
.github/CODEOWNERS           # bonus: mandatory reviewers for prod policy changes
tests/                        # 29 unit tests, stdlib unittest, no network needed

Dependency footprint: PyYAML only. Everything else (the validator, the diff engine, the HTTP API) is Python stdlib. This is a deliberate choice, not a limitation of the design — governance/enforcement_store.py and governance/api.py are the only two modules that would change in a real deployment (swap the JSON file for a real DB/API client, swap http.server for FastAPI if you want one), and nothing else in the framework depends on either of them directly.


3. The policy format

schema/policy.schema.yaml defines the schema as data, so schema changes are themselves reviewable Git diffs. A policy file (agent/policy-by-env/prod/agent-policy.yaml) declares, per the challenge spec:

Requirement Field(s)
Approved model list models.approved, models.default
Allowed tools and scopes tools[].name, tools[].scopes, tools[].requires_hitl
Guardrail rules guardrails.input_filters, guardrails.output_filters, guardrails.max_tokens_per_request, guardrails.pii_handling
HITL thresholds hitl.risk_threshold, hitl.confidence_threshold, hitl.escalation_channel
Data retention rules data_retention.conversation_logs_days, .pii_retention_days, .deletion_on_request
Regulatory framework tags regulatory_frameworks (e.g. SOC2, GDPR)

The validator also enforces cross-field governance rules a plain required/type schema can't express, e.g.:

  • models.default must be a member of models.approved
  • hitl.confidence_threshold must be in [0.0, 1.0]
  • every tool must declare at least one scope
  • environment: prod requires approvals.requires_review_for_prod: true

4. Running it yourself

pip install pyyaml   # the only dependency

# 1) Validate (this is the CI gate)
python cli/validate_policy.py --policy agent/policy-by-env/prod/agent-policy.yaml

# 2) Deploy (push validated policy to the enforcement runtime)
python cli/deploy_policy.py \
  --agent-id support-triage-agent --policy agent/policy-by-env/prod/agent-policy.yaml \
  --repo . --environment prod

# 3) Query the versioning endpoint for any historical commit
python3 -c "
from governance.git_store import get_policy_at_commit
r = get_policy_at_commit('.', 'support-triage-agent', 'cfafcef', environment='prod')
print(r.policy['data_retention']['pii_retention_days'])  # -> 30 (before promotion)
r = get_policy_at_commit('.', 'support-triage-agent', '7bb23c1', environment='prod')
print(r.policy['data_retention']['pii_retention_days'])  # -> 14 (after promotion)
"

# 4) Check drift
python3 cli/check_drift.py --agent-id support-triage-agent --environment prod --repo .

# 5) Run the HTTP API
python3 governance/api.py   # listens on :8088
curl "localhost:8088/agents/support-triage-agent/policy-at-commit/0b0e655"
curl "localhost:8088/agents/support-triage-agent/drift?environment=prod"
curl "localhost:8088/agents"   # list of registered agents + environments

# The governance dashboard is a separate, independently-deployed static
# site (github.com/ashwin937/agent-governance-dashboard) — a read-only
# client of this API, not part of it. This API sends CORS headers
# (Access-Control-Allow-Origin: *) on every response specifically so a
# dashboard hosted on a different origin can call it. See that repo's
# README for how to point it at a given deployment via ?api=<url>.

# 6) Run the full test suite
python3 -m unittest discover -s tests -p "test_*.py" -v

5. Success criteria — how each one is met

"Policy file deploys automatically on push to the main branch." .github/workflows/deploy-agent-policy.yml runs on every push to main: it validates the policy, then (only if valid) calls cli/deploy_policy.py, which pushes it to the enforcement runtime tagged with the triggering commit SHA. Demonstrated locally in tests/test_cli_blocks_invalid.py::test_deploy_succeeds_for_valid_policy and manually against the demo repo (3 real commits, each deployed).

"Policy versioning endpoint returns the correct policy for any historical commit." governance/git_store.get_policy_at_commit(repo, agent_id, sha), exposed over HTTP as GET /agents/{id}/policy-at-commit/{sha}. Verified against the demo repo's real 3-commit history (v1 → v1.1 update → rollback) — each SHA returns the exact policy that was active at that commit, and v1 and the rollback commit return identical policy content from two different SHAs, proving rollback correctness. See tests/test_versioning.py::test_retrieves_correct_policy_across_multiple_versions.

"Drift detection correctly alerts when the enforced policy is manually modified out-of-band, simulating an admin change bypassing Git." governance/enforcement_store.EnforcementStore.admin_override() simulates exactly this (as distinct from push_policy(), the CI-only path). governance/drift.detect_drift() deep-diffs the enforced policy against Git HEAD, with name-matched diffing for lists like tools so the report pinpoints the exact field. Verified live against the demo repo (an admin override raising max_tokens_per_request to 32000 and disabling HITL on the refund tool was caught precisely) and in tests/test_drift.py::test_admin_out_of_band_edit_is_detected.

"An invalid policy file with missing required fields fails the CI step and blocks the deployment." cli/validate_policy.py exits 1 with every missing/invalid field listed. The GitHub Actions deploy-policy job has needs: validate-policy, so a failed validation job structurally prevents the deploy job from ever running — not just a convention, a hard DAG dependency. deploy_policy.py also independently re-validates before pushing, so even a misconfigured pipeline that skipped the validate step can't push a bad policy. Both paths are tested in tests/test_cli_blocks_invalid.py.


6. Bonus: policy promotion workflow

Policies flow dev -> staging -> prod as separate files under agent/policy-by-env/<env>/agent-policy.yaml. Promoting means opening a PR that copies the (already-validated) staging file's content into the prod path — cli/promote_policy.py --write generates that diff.

Two independent gates protect a promotion to prod:

  1. governance/promotion.check_promotion(), run as a required CI check in .github/workflows/policy-promotion.yml, verifies the prod file in the PR is byte-for-byte identical to the current staging file, aside from fields that are legitimately environment-specific (environment, approvals.requires_review_for_prod). This stops a PR labeled "promote to prod" from smuggling in an extra, unreviewed change — see tests/test_promotion.py::test_smuggled_change_fails, demonstrated live by patching in a max_tokens_per_request: 999999 change and confirming the check fails.
  2. Human review, enforced natively by GitHub: .github/CODEOWNERS requires designated reviewers to approve any change under agent/policy-by-env/prod/, and the promotion job is scoped to the prod GitHub Environment, which you configure with required reviewers in repo Settings → Environments → prod. GitHub then blocks the job — and therefore the merge, once it's a required status check — until that approval lands. (This half is a repo setting, not expressible in YAML, so it's documented here rather than faked.)

7. What would change for a real production deployment

  • governance/enforcement_store.py — swap the JSON file for a client against the real policy enforcement runtime (OPA, Cedar, or a governance platform's own API). Nothing else in the framework touches storage directly.
  • governance/api.py — swap stdlib http.server for FastAPI/Flask if you want request validation, OpenAPI docs, and auth middleware for free. It exists on stdlib here only because this sandbox has no network access to install packages; every other module is already framework-agnostic.
  • Auth — the CI→runtime push and the admin-override path both need real authn/authz in production (service identity for CI, human SSO for admin edits) so the audit trail (deployed_by, origin) is trustworthy rather than a caller-supplied string.
  • Multi-agent scaleagents.yaml already supports registering many agents; a real mono-repo would likely generate this registry rather than hand-maintain it.
  • Registry path lookups use the current tree, not the historical oneget_policy_at_commit resolves which file to read from today's agents.yaml, then reads that file's content as of the requested commit. If a policy file is ever moved (as happened here: the original single-file layout was replaced by policy-by-env/), querying a commit from before the move will correctly fail with "no policy file at that path" rather than silently returning nothing or the wrong file — but it does mean the versioning endpoint can't answer for commits that predate a path migration. A production version would resolve the path historically too (e.g. git log --follow) if that mattered.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages