Portfolio project: governance retrofitted onto existing agents

Agent Control Tower

Policy, budget, approval, and audit controls for autonomous agents

A lightweight, auditable governance layer for AI agents: cost tracking with budget caps (checked before each call, so a single call can overshoot), a hash-chained, tamper-evident (not tamper-proof) audit log with a verify command, and a human-approval gate for write-side-effect actions. It is a small prototype, and the controls are opt-in.

See what is measuredView the repoWhat is not built

  • 42 unit tests passing (latest CI run)
  • 5 committed audit events
  • Prototype: opt-in, not an enforcement boundary
  • Retrofit claims partly unverified
  • No approver identity or authentication

01

The problem

Useful agents can still create unmanaged cost, opaque decisions, or unauthorized external side effects.

Every other project in this series adds a new agent. This one governs the agents that already exist, which is a different skill from designing governance on a blank whiteboard.

  • A gate must not break unattended runs

    slack-daily-agent runs unattended every morning via launchd. A governance layer that blocks on an approval prompt would silently break that guarantee, so the policy is scoped per action type, not global.

  • Implicit behavior is a hazard

    Approval mode is explicit (interactive=True/False), never inferred from whether stdin is a TTY.

02

How it works

A wrapped agent either makes a Claude call through a governed client (budget check, cost ledger, audit record) or attempts a write action through an approval gate that looks the action up in a declarative policy table.

Stages, one at a time

Stage 1 The wrapped agent

An existing agent opts in by importing this repo. Two agents in this portfolio do (slack-daily-brief and exec-status-rollup); if the repo is not found they run ungoverned.

The two-agents claim is based on those repos' imports, not independently verified end to end.

Stage 2 Model calls: GovernedClient

GovernedClient mirrors the exact call shape of anthropic.Anthropic(), so retrofitting is a one-line change. It checks the budget in ledger.py before each call and logs the call in audit.py.

Stage 3 Write actions: the policy table

policy.py is a declarative dict. llm_call: never gated. slack_post_recurring: auto-approved. slack_post_adhoc: requires approval. jira_write and email_send: require approval, always. The enforcement logic in approval.py just looks the action type up.

Stage 4 The human gate

For actions the policy marks approval: an interactive y/N prompt, or an async queue approved with cli.py approve. Async approval is consume-on-next-run, not resume; approvals are single-use and expire (24h by default).

Approval is bound to the description string, not the real call arguments, and has no approver identity.

Stage 5 The audit log

Every call and approval state transition is a hash-chained record; python cli.py verify detects edited, deleted, inserted or reordered records.

03

What is measured

There is no benchmark here. The only committed evidence is the repo's small run data and its unit tests. The table carries its caveat directly underneath.

Committed run data

data/ledger.json and data/audit.jsonl are committed to the repo and, per the README, came from running the two integrated agents against Slack and Jira.

SourceWhat the committed file holds
data/ledger.json, slack-daily-agent, 2026-08-252 calls, 3446 input / 6447 output tokens, cost 0.107043
data/audit.jsonl, slack-daily-agent2 llm_call events
data/audit.jsonl, exec-status-rollup3 events: write_action_denied, then write_action_approved, then write_action_executed (the synchronous approval gate exercised on both branches)
tests/42 unit tests (CI on Python 3.9 and 3.12)

Caveat Not independently reproducible from this repo; the file holds only 5 events. The async queue path (approve, then consume on the next run) is covered by unit tests only; the committed data has no run of it. The unattended launchd claim in the other repo was not re-checked here. Lines written before chaining form an unprotected legacy prefix.

Source: data/ledger.json, data/audit.jsonl, tests/

04

Architecture pattern

Pattern Governance wrapper/gate: wraps other agents, contains no agent of its own and makes no model calls.

Everything here is deterministic code; the model only runs inside the wrapped agents. governed_client.GovernedClient checks a budget and logs each Claude call, and approval.governed_action gates write actions using the declarative table in policy.py. The controls are opt-in: an agent is governed only if its code imports this repo.

  1. Wrapped agentslack-daily-agent summarize(), exec-status-rollup Slack post
  2. GovernedClientmodel calls: pre-call budget check, then the Claude API
  3. Ledgerledger.json: per-agent cost and budget caps
  4. governed_actionwrite actions: policy lookup in policy.py
  5. Human gateinteractive y/N, or an async pending queue; single-use, expiring
  6. Executethe wrapped action runs, for example a Slack post
  7. Hash-chained audit logaudit.jsonl; cli.py verify checks the chain

05

Competencies demonstrated

  • AI governance

    Separates model inference, policy evaluation, approval, and execution.

  • Systems integration

    Retrofitted through compatibility wrappers instead of application rewrites.

  • Operational resilience

    Preserves unattended scheduled execution while gating higher-risk actions.

  • Financial stewardship

    Per-agent ledgers and daily budget caps (pre-call check) track cost.

  • Auditability

    Audit event records (hash-chained; cli.py verify checks it) and explicit approval state transitions.

06

What is not built or not verified yet

Taken from the repository's own Known limitations. This is part of the pitch: the gaps are known, written down and not rounded away.

  • limit

    Not an enforcement boundary

    The controls are opt-in: an agent is governed only if its code imports this repo, and the integrations fall back to ungoverned if it is missing.

  • not built

    Authentication or approver identity

    No authentication or approver identity on cli.py approve (or the interactive y/N prompt): anyone who can run it, or write to data/pending/, can approve, and audit records do not say who approved.

  • limit

    Audit log is tamper-evident, not tamper-proof

    Someone with write access can still recompute the whole chain or truncate the tail undetected (no signature or external anchor).

  • limit

    Approvals are bound to the description, not the arguments

    Async approval is single-use and expires, but it is unauthenticated and bound to the description string, not to the real call arguments behind it.

  • limit

    Locking is advisory and single-host

    fcntl.flock protects concurrent runs on one machine, is a no-op on platforms without fcntl, and is unreliable on network filesystems.

  • limit

    Budget cap is a pre-call check

    A call is blocked only if spend is already at or above the cap; the call that crosses it still happens.

  • limit

    Budget caps are opt-in per caller

    GovernedClient(daily_budget=None) enforces nothing; slack-daily-brief currently constructs it without a budget.

  • not verified

    Retrofit claims

    The two agents import this repo optionally; the committed data holds 5 audit events and the live runs could not be reproduced here.

07

Roadmap

  • DoneDeclarative action policy with explicit approval modes
  • DonePer-agent cost ledger with a pre-call budget cap
  • DoneHash-chained audit log with a verify command
  • DoneAsync approval: single-use and expiring
  • Not doneOpen gap, no plan stated in the README: authenticated approver identity (not built)
  • Not doneOpen gap, no plan stated in the README: signed or externally anchored audit log (not built)