Observed since 20 Oct, 20:20 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
aegize
Infrastructure for autonomous AI agents: identity, policy, permissions, approval workflows, and audit.
Infrastructure for autonomous AI agents.
Aegize is the runtime layer between autonomous AI agents and the tools they use.
It provides identity, policy enforcement, permissions, approval workflows, audit logging, observability, and runtime governance for every AI action.
Every agent action must have identity, permission, policy enforcement, and audit.
One agent attempts three tool calls. Aegize allows the web search, holds the email for approval, blocks the shell command, and writes an audit record for every attempt.
$ python examples/demo_story.py
[1] web_search.search query='Aegize runtime'
ALLOWED -> results for 'Aegize runtime'
[2] email.send to='ceo@example.com'
APPROVAL REQUIRED -> held for human review (not executed)
reason: approval required for tool 'email'
[3] shell.execute cmd='rm -rf /var/data'
DENIED -> blocked before execution
reason: denied by rule for tool 'shell'
Audit trail:
allowed web_search search
execution_succeeded web_search search
approval_required email send
denied shell execute
3 actions attempted · 1 allowed · 1 awaiting approval · 1 denied · 4 audit records written
The gated and denied calls never reach the underlying functions. See Demo to run it yourself.
AI systems are moving from answering questions to taking actions — running shells, sending email, moving money, calling internal APIs. A model that only returns text is easy to contain. An agent that acts is not: it needs an identity, scoped permissions, approvals for high-impact operations, and a record of everything it did.
That governance layer is usually missing today, and the model's own judgment stands in for it. Aegize is the runtime infrastructure that fills the gap, so organizations can let agents take actions without giving up control or visibility. Security is one capability this provides; operability, reviewability, and confidence in deployment are the rest.
Every meaningful AI action should pass through trusted runtime infrastructure before reaching the outside world.
AgentIdentity — a durable identity for each agent (owner, environment,
metadata).PermissionPolicy — a YAML policy engine that returns allow, deny, or
require_approval.GuardedTool / @guarded_tool — the enforcement point: wrap any callable
so it is identified, permissioned, gated, and audited.AuditLog — an append-only JSONL record of every attempt and outcome.pip install aegize
With the MCP policy gateway (see MCP gateway):
pip install "aegize[mcp]" # in a Python 3.10+ environment
uv tool install --python 3.12 "aegize[mcp]" # on any machine (isolated; uv
# fetches the interpreter itself)
The gateway runs as a separate process, so your project's own Python version
does not matter — only the gateway's interpreter needs to be 3.10+ (the MCP
SDK's floor), and an isolated tool install (uv tool install / pipx)
provides that anywhere.
Or from source (for development):
git clone https://github.com/gggaswint/aegize.git
cd aegize
pip install -e ".[dev]"
from aegize import AgentIdentity, PermissionPolicy, GuardedTool, AuditLog
agent = AgentIdentity(
agent_id="research_bot",
name="Research Bot",
owner="Geoffrey",
environment="dev",
)
policy = PermissionPolicy.from_yaml("aegize.yaml")
audit = AuditLog("audit.jsonl")
def web_search(query: str) -> str:
return f"searched: {query}"
safe_web_search = GuardedTool(
tool_name="web_search",
operation="search",
func=web_search,
agent=agent,
policy=policy,
audit_log=audit,
risk_level="low",
)
result = safe_web_search("Aegize runtime")
If the policy allows the action, the function runs and two audit records are
written (authorization + result). If not, Aegize raises PolicyDenied or
ApprovalRequired and the function never executes.
from aegize import PolicyDenied, ApprovalRequired
try:
safe_web_search("Aegize runtime")
except ApprovalRequired as exc:
# route to a human approval workflow
...
except PolicyDenied as exc:
# blocked outright
...
You don't have to wrap every function by hand. Declare a tool once with
@guarded_tool, bundle your agent/policy/audit into a GuardContext, and bind
them together when you have a context.
from aegize import (
AgentIdentity, PermissionPolicy, AuditLog,
GuardContext, guarded_tool, guard, ApprovalRequired,
)
@guarded_tool(tool_name="email", operation="send", risk_level="high")
def send_email(to: str, body: str) -> str:
... # your real implementation
ctx = GuardContext(
agent=AgentIdentity(agent_id="research_bot", name="Research Bot", owner="Geoffrey"),
policy=PermissionPolicy.from_yaml("aegize.yaml"),
audit_log=AuditLog("audit.jsonl"),
)
# Bind to a context -> a plain, signature-preserving callable you can register.
guarded_send = guard(send_email, context=ctx)
# e.g. server.add_tool(guarded_send) # MCP / any tool registry
try:
guarded_send("ceo@example.com", "Q3 numbers")
except ApprovalRequired:
... # gated for human approval; send_email never ran
guard() returns a callable that preserves the original __name__,
docstring, and signature, so tool registries (including MCP servers) that
introspect functions keep working.
Default context. Inside a with ctx: block (or after ctx.activate()),
decorated tools can be called directly:
with ctx:
send_email("ceo@example.com", "Q3 numbers") # uses the active context
Per-call metadata. Pass guard_metadata= to any guarded call to attach
context for policy decisions (e.g. a path for an allowlist) and the audit log.
It is stripped before your function runs:
guarded_read("report", guard_metadata={"path": "./safe_data/report.txt"})
Both styles are fully supported — use GuardedTool(...) directly when you want
explicit objects, or the decorator when you want ergonomics. They share the same
enforcement and audit code.
Policies are per-agent. Each agent has allow, require_approval, and deny
sections. Evaluation order is deny → require_approval → allow → default-deny,
so an explicit deny always wins and anything unlisted is denied.
agents:
research_bot:
allow:
- tool: web_search
operations: ["search"]
risk_level_max: medium # block this rule above 'medium' risk
- tool: file_reader
operations: ["read"]
paths:
- "./safe_data/**" # only inside the allowlisted path
require_approval:
- tool: email
operations: ["send"]
- tool: shell
operations: ["execute"]
deny:
- tool: payments
operations: ["charge"]
- tool: shell
operations: ["rm", "delete"]
Rule fields:
| Field | Applies to | Meaning |
|---|---|---|
tool | all | Tool name to match. |
operations | all | Operations the rule covers. Omit to match every operation. |
risk_level_max | allow | Highest risk this rule permits (low…critical). |
paths | allow | Glob allowlist; a string argument must match one of these. |
Path matching: when a rule has
paths, Aegize checks the string arguments of the call (andmetadata["path"]) against the glob patterns. A call with no matching path is denied.
A policy is only as trustworthy as the behavior you can prove it has. Aegize ships a small CLI to assert that a policy makes the decisions you expect — before it ever gates a real agent. Write the expectations as YAML:
# policy_tests.yaml
tests:
- name: web search is allowed
agent: research_bot
tool: web_search
operation: search
expect: allow
- name: charging a card is denied
agent: research_bot
tool: payments
operation: charge
expect: deny
- name: reading outside the allowlist is denied
agent: research_bot
tool: file_reader
operation: read
metadata: { path: "/etc/passwd" }
expect: deny
Run them against a policy file:
aegize policy test examples/aegize.yaml examples/policy_tests.yaml
PASS web search is allowed (expected allow)
PASS charging a card is denied (expected deny)
PASS reading outside the allowlist is denied (expected deny)
3 passed, 0 failed
The command only evaluates policy — it never executes a tool. It exits 0
when every case passes, 1 when any decision doesn't match, and 2 when a file
is missing or malformed, so it drops straight into CI as a policy regression
gate. Each case takes:
| Field | Required | Meaning |
|---|---|---|
agent | yes | The acting agent_id to evaluate. |
tool | yes | Tool name. |
operation | yes | Operation name. |
expect | yes | Expected decision: allow, deny, or require_approval. |
name | no | Label shown in the report (defaults to test #N). |
risk_level | no | low…critical (default low); exercises risk_level_max rules. |
metadata | no | Extra call context, e.g. { path: ... } for paths allowlists. |
This is the first step of the policy-as-code lifecycle explored in RFC 0008.
aegize-mcp)Put Aegize in front of any local stdio MCP server — no changes to the server, no changes to the host. The gateway launches the real server as a subprocess, mirrors its tools, and routes every tool call through the Aegize runtime; only allowed calls are forwarded (RFC 0009).
MCP Host ──stdio──► Aegize MCP Gateway ──stdio──► Upstream MCP server
(Claude Code, …) (identity · policy · (any local stdio server)
approval · audit)
pip install "aegize[mcp]" # requires Python 3.10+
# See what the upstream server exposes (for writing the policy):
aegize-mcp inspect -- npx -y some-mcp-server
# Serve the host, guarding the upstream:
aegize-mcp proxy \
--policy ./aegize.yaml \
--agent-id claude-code \
--owner you \
--environment development \
--audit-log ./aegize-mcp-audit.jsonl \
-- \
npx -y some-mcp-server
Everything after -- is the upstream server's command. Policies use the
normal Aegize model with MCP tool names and operation: "call":
agents:
claude-code:
allow:
- tool: searchDocs
operations: ["call"]
require_approval:
- tool: createSession
operations: ["call"]
deny:
- tool: deployProduction
operations: ["call"]
Guarantees carry over unchanged: default deny, deny wins, denied and
approval-gated calls never reach the upstream server, and every attempt is
audited (decision before forwarding, outcome after). Sensitive-looking argument
values (passwords, tokens, keys…) are redacted from audit summaries — and for
credential-bearing tool names (e.g. setCredential), every value is; a SHA-256
argument hash keeps calls correlatable.
Keeping the policy current as the upstream server evolves is a one-command story — default deny means new upstream tools are blocked until listed, and the tooling makes that visible instead of silent:
# Print a full-coverage policy skeleton (every tool gated behind approval):
aegize-mcp inspect --emit-policy --agent-id claude-code -- npx -y some-mcp-server
# Diff a policy against the live tool list; exits non-zero on drift (CI-able):
aegize-mcp check --policy ./aegize.yaml --agent-id claude-code -- npx -y some-mcp-server
# Snapshot the tool surface as deterministic JSON — diff it in CI to catch
# tool renames/removals/schema changes (useful even without any policy):
aegize-mcp inspect --json -- npx -y some-mcp-server > tool-surface.json
The gateway also logs uncovered tools to stderr at startup
("N discovered tool(s) … will be default-denied: …"), so drift is
diagnosable straight from the host's MCP logs.
Current scope, stated plainly: local stdio servers only (remote HTTP
proxying is not yet supported), and require_approval is a hard gate — the
call is refused and recorded; a durable approval workflow is future work
(RFC 0004). A worked end-to-end setup lives
in examples/mcp/extentos/.
Every attempt is appended to a JSONL file — one self-contained JSON object per
line, easy to tail, grep, or ship to a SIEM. A single allowed call:
{"timestamp": "2026-06-27T18:00:00+00:00", "event": "allowed", "agent_id": "research_bot", "tool_name": "web_search", "operation": "search", "risk_level": "low", "input_summary": "'Aegize runtime'", "reason": "allowed by rule for tool 'web_search'"}
{"timestamp": "2026-06-27T18:00:00+00:00", "event": "execution_succeeded", "agent_id": "research_bot", "tool_name": "web_search", "operation": "search", "risk_level": "low", "result_summary": "'searched: Aegize runtime'"}
Events: allowed, denied, approval_required, execution_succeeded,
execution_failed. The authorization decision is always written before the
function runs; the result is written after. Reading the log back is one call:
for record in audit.read_all():
print(record["event"], record["tool_name"], record["operation"])
The 60-second story — one agent, three tool calls, three outcomes, all audited:
python examples/demo_story.py
It runs the See it in action flow above against
examples/demo_policy.yaml and prints the path to
the audit log it wrote.
More runnable scripts live in examples/:
python examples/basic_allow.py # allowed web_search runs
python examples/denied_shell.py # denied shell command is blocked
python examples/approval_email.py # email send raises ApprovalRequired
python examples/decorator_usage.py # @guarded_tool + GuardContext (v0.2)
python examples/demo_story.py # the full allow / approve / deny story
allow rule means the action is denied.deny overrides require_approval and allow.deny and require_approval raise before
the wrapped function is called.The operating documents for Aegize — useful for contributors and for understanding where the project is headed.
Direction
Operating
Process & record
@guarded_tool decorator + GuardContext ergonomics.aegize lint CLI.guard() callable).require_approval.dev / staging / prod).pip install -e ".[dev]"
pytest # run the test suite
ruff check . # lint
CI runs the same pytest + ruff checks on every push and pull request across
Python 3.9–3.12.
Contributions are welcome. See CONTRIBUTING.md for the dev setup, the project's scope and design principles, and the bar for a mergeable change.
Before making major changes, read CLAUDE.md and the project documents in
docs/. They are the source of truth for Aegize's direction, positioning, and design. (python scripts/context_check.pyconfirms they're present.)
Aegize governs what agents are allowed to do, so we treat weaknesses in it seriously. Please report vulnerabilities privately — see SECURITY.md. Do not open a public issue for a suspected vulnerability.
MIT — see LICENSE.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
pyyamlEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | pyyaml>=6.0 | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | pytest>=7.0; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | ruff>=0.1; extra == "dev" | runtime |
mcpEnvironment{
"extras": "",
"marker": "python_version >= \"3.10\" and extra == \"mcp\"",
"requires_python": ">=3.9",
"evaluation": "NOT_EVALUATED"
} | mcp<3,>=2.0; python_version >= "3.10" and extra == "mcp" | runtime |
bdist_wheel · 42.08 KB · Python py3
sdist · 56.6 KB · Python source
Observed since 20 Oct, 20:20 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:3fc2b12134d7a8abbcbe32dd9bc0e47d19cd7d1513363c026afe1b9fbb4d6428