Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ jobs:
run: make sdk-ts-build

- name: Verify generated TypeScript client is current
# GitHub deliberately withholds repository secrets from fork pull requests.
# Keep every auth-free SDK check above running, and run regeneration when
# Speakeasy credentials are available on trusted branches.
if: env.SPEAKEASY_API_KEY != ''
run: make sdk-ts-generate-check

- name: Verify generated method naming conventions
Expand Down
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,54 @@ Note: This starts server without API keys configured which is dangerous for any

Set appropirate env vars to override defaults like:
* Exposed ports
* Agent and admin API keys
* Bootstrap admin API keys (member SDK/UI keys are created in Agent Control)
* Postgres DB Password

```bash
export AGENT_CONTROL_SERVER_HOST_PORT=18000
export AGENT_CONTROL_DB_HOST_PORT=15432
export AGENT_CONTROL_API_KEY_ENABLED=true
export AGENT_CONTROL_API_KEYS="agent-api-key"
export AGENT_CONTROL_ADMIN_API_KEYS="admin-api-key"
export AGENT_CONTROL_ADMIN_API_KEYS="bootstrap-admin-key"
export AGENT_CONTROL_CORS_ORIGINS="https://agent-control.example.com"
export AGENT_CONTROL_POSTGRES_PASSWORD="postgres-password"

curl -L https://raw.githubusercontent.com/agentcontrol/agent-control/refs/heads/main/docker-compose.yml | docker compose -f - up -d
```

#### Create scoped users

`AGENT_CONTROL_ADMIN_API_KEYS` provides the initial administrator credential.
`AGENT_CONTROL_API_KEYS` is not accepted. With authentication enabled,
non-admin SDK and UI keys must be created through access management so each
credential is tied to an enabled user and that user's explicit control grants.
Authenticated deployments must set `AGENT_CONTROL_CORS_ORIGINS` to an
explicit UI-origin allowlist; credentialed wildcard CORS is rejected.

Sign in to the dashboard with that key, open **Access management**, and then:

1. Create a named user. Agent Control atomically issues the user's first API
key and shows the secret once.
2. Assign the control buckets that user may use.
3. Rotate or revoke the user's single active key when needed. Rotation keeps
the user's bucket assignments and Monitor history intact.

The user supplies the same key to the Python SDK:

```bash
export AGENT_CONTROL_API_KEY="<user-api-key>"
```

The key can also be entered in the dashboard login dialog. A non-admin session
can inspect its assigned controls and their enforcement history, but cannot
create, edit, enable, disable, attach, or delete controls. Server-side
authorization scopes SDK policy refresh, observability ingestion, event
queries, and statistics to the user's assigned buckets; hiding edit buttons in
the UI is not the security boundary.

Disabling a user or revoking a key invalidates both SDK requests and existing
browser sessions. Store generated keys in a secret manager and never put them
in `config.yaml` or source control.

Verify it is up:

```bash
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ services:
AGENT_CONTROL_PORT: 8000
# API authentication (override via host env or server/.env; see server/.env.example)
AGENT_CONTROL_API_KEY_ENABLED: ${AGENT_CONTROL_API_KEY_ENABLED:-false}
AGENT_CONTROL_API_KEYS: ${AGENT_CONTROL_API_KEYS:-}
AGENT_CONTROL_ADMIN_API_KEYS: ${AGENT_CONTROL_ADMIN_API_KEYS:-}
AGENT_CONTROL_SESSION_SECRET: ${AGENT_CONTROL_SESSION_SECRET:-}
AGENT_CONTROL_CORS_ORIGINS: ${AGENT_CONTROL_CORS_ORIGINS:-http://localhost:4000}
Expand Down
10 changes: 10 additions & 0 deletions evaluators/contrib/defenseclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ pip install "agent-control-evaluators[defenseclaw]"
The configuration contracts and JSON Schemas are implemented and discoverable. Both evaluator
classes intentionally execute as no-ops: they return `matched=False` without contacting or
installing any DefenseClaw runtime or OSS package.

The complementary DefenseClaw watcher can emit post-decision
`ControlExecutionEvent` records through the Agent Control SDK. The agent Monitor
shows aggregate enforcement counts and a **Recent executions** drill-down with
trace/span/request correlation, control and rule identity, action, and duration.
When the DefenseClaw integration is enabled, it includes exact blocked input,
raw request body, and enforcement reason by default. Monitor labels those spans
`UNREDACTED` and renders the content in the execution drill-down. DefenseClaw
operators can explicitly select metadata-only delivery. Treat access to these
events as access to sensitive workload data.
1 change: 0 additions & 1 deletion sdks/python/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ jobs:
sleep 5
env:
AGENT_CONTROL_DB_URL: postgresql+psycopg://postgres:postgres@localhost/agent_control_test
AGENT_CONTROL_API_KEYS: test-api-key-ci
AGENT_CONTROL_ADMIN_API_KEYS: test-api-key-ci

- name: Run tests
Expand Down
15 changes: 6 additions & 9 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@ AGENT_CONTROL_PORT=8000
# When set to true, API key auth is required for protected endpoints.
AGENT_CONTROL_API_KEY_ENABLED=false

# Comma-separated list of valid API keys for standard access.
# Example: "my-ui-key,my-sdk-key"
AGENT_CONTROL_API_KEYS="my-ui-key"

# OPTIONAL: comma-separated list of admin API keys (subset with elevated
# privileges). Only needed if you use admin-only routes; for most local
# testing a single AGENT_CONTROL_API_KEYS entry is enough.
# Comma-separated bootstrap admin keys. Use one of these keys to create users,
# database-managed SDK/UI keys, and per-control grants through
# /api/v1/admin/access. AGENT_CONTROL_API_KEYS is intentionally not accepted:
# every non-admin key must belong to an enabled DB user and have explicit grants.
AGENT_CONTROL_ADMIN_API_KEYS="my-admin-key"

# Secret used to sign session JWTs for browser logins.
Expand All @@ -33,8 +30,8 @@ AGENT_CONTROL_SESSION_SECRET="change-me-to-a-long-random-string"

# Allowed origins for CORS. When using the Next.js dev server on :4000,
# set this to the exact UI origin so cookie-based auth works cross-origin.
# In production (static UI served by FastAPI on the same origin), this can
# usually be left as "*".
# Authenticated deployments reject "*"; set the public same-origin UI URL in
# production even when FastAPI serves the static UI itself.
AGENT_CONTROL_CORS_ORIGINS="http://localhost:4000"

#########################################
Expand Down
15 changes: 15 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,18 @@ psycopg's bundled binary package.
Server configuration is driven by environment variables (database, auth, observability, evaluators). For the full list and examples, see the docs.

Full guide: https://docs.agentcontrol.dev/components/server

### Scoped API-key access

When API-key authentication is enabled, configure at least one bootstrap admin
key with `AGENT_CONTROL_ADMIN_API_KEYS`. Administrators can then use
`/api/v1/admin/access` or the dashboard's **Access management** page to create
users with one active API key and assign control buckets to each user. Creating
a user atomically issues the first key; rotation revokes and replaces that key
without changing the user's grants or enforcement history.

Generated secrets are returned only by create, issue, and rotate responses. Agent
Control stores a one-way digest, revalidates database-backed browser sessions
on every request, and applies the user's grants to effective agent controls and
all observability reads and writes. Non-admin keys never receive control
mutation privileges.
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""access users, single-active API keys, and user-owned control grants

Revision ID: f7c2a9d4e1b8
Revises: e2b7f4a9c6d1
Create Date: 2026-07-09 00:00:00.000000

"""

from __future__ import annotations

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision = "f7c2a9d4e1b8"
down_revision = "e2b7f4a9c6d1"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"access_users",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column(
"namespace_key",
sa.String(length=255),
server_default=sa.text("'default'"),
nullable=False,
),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("role", sa.String(length=32), server_default=sa.text("'member'"), nullable=False),
sa.Column("enabled", sa.Boolean(), server_default=sa.text("TRUE"), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.CheckConstraint("role IN ('admin', 'member')", name="ck_access_users_role"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("namespace_key", "id", name="uq_access_users_namespace_id"),
sa.UniqueConstraint("namespace_key", "name", name="uq_access_users_namespace_name"),
)

op.create_table(
"api_key_credentials",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column(
"namespace_key",
sa.String(length=255),
server_default=sa.text("'default'"),
nullable=False,
),
sa.Column("user_id", sa.String(length=36), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("key_prefix", sa.String(length=24), nullable=False),
sa.Column("key_hash", sa.String(length=64), nullable=False),
sa.Column("enabled", sa.Boolean(), server_default=sa.text("TRUE"), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.ForeignKeyConstraint(
["namespace_key", "user_id"],
["access_users.namespace_key", "access_users.id"],
name="api_key_credentials_user_fkey",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("namespace_key", "id", name="uq_api_key_credentials_namespace_id"),
sa.UniqueConstraint("key_hash", name="uq_api_key_credentials_key_hash"),
)
op.create_index(
"idx_api_key_credentials_user",
"api_key_credentials",
["namespace_key", "user_id"],
)
op.create_index(
"uq_api_key_credentials_one_live_per_user",
"api_key_credentials",
["namespace_key", "user_id"],
unique=True,
postgresql_where=sa.text("enabled IS TRUE AND revoked_at IS NULL"),
)

op.create_table(
"access_user_control_grants",
sa.Column(
"namespace_key",
sa.String(length=255),
server_default=sa.text("'default'"),
nullable=False,
),
sa.Column("user_id", sa.String(length=36), nullable=False),
sa.Column("control_id", sa.Integer(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.ForeignKeyConstraint(
["namespace_key", "user_id"],
["access_users.namespace_key", "access_users.id"],
name="access_user_control_grants_user_fkey",
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["namespace_key", "control_id"],
["controls.namespace_key", "controls.id"],
name="access_user_control_grants_control_fkey",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"namespace_key",
"user_id",
"control_id",
name="access_user_control_grants_pkey",
),
)
op.create_index(
"idx_access_user_control_grants_control",
"access_user_control_grants",
["namespace_key", "control_id"],
)

op.add_column(
"control_execution_events",
sa.Column("access_user_id", sa.String(length=36), nullable=True),
)
op.add_column(
"control_execution_events",
sa.Column("api_key_id", sa.String(length=36), nullable=True),
)
op.create_foreign_key(
"control_execution_events_access_user_fkey",
"control_execution_events",
"access_users",
["namespace_key", "access_user_id"],
["namespace_key", "id"],
ondelete="RESTRICT",
)
op.create_index(
"ix_events_namespace_user_agent_time",
"control_execution_events",
["namespace_key", "access_user_id", "agent_name", sa.text("timestamp DESC")],
)
op.create_foreign_key(
"control_execution_events_api_key_fkey",
"control_execution_events",
"api_key_credentials",
["namespace_key", "api_key_id"],
["namespace_key", "id"],
ondelete="RESTRICT",
)
op.create_index(
"ix_events_namespace_credential_time",
"control_execution_events",
["namespace_key", "api_key_id", sa.text("timestamp DESC")],
)


def downgrade() -> None:
op.drop_index("ix_events_namespace_credential_time", table_name="control_execution_events")
op.drop_constraint(
"control_execution_events_api_key_fkey",
"control_execution_events",
type_="foreignkey",
)
op.drop_index("ix_events_namespace_user_agent_time", table_name="control_execution_events")
op.drop_constraint(
"control_execution_events_access_user_fkey",
"control_execution_events",
type_="foreignkey",
)
op.drop_column("control_execution_events", "access_user_id")
op.drop_column("control_execution_events", "api_key_id")
op.drop_index(
"idx_access_user_control_grants_control",
table_name="access_user_control_grants",
)
op.drop_table("access_user_control_grants")
op.drop_index(
"uq_api_key_credentials_one_live_per_user",
table_name="api_key_credentials",
)
op.drop_index("idx_api_key_credentials_user", table_name="api_key_credentials")
op.drop_table("api_key_credentials")
op.drop_table("access_users")
Loading
Loading