Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This repository keeps documentation concise. The full documentation lives on the
- [Controls](https://docs.agentcontrol.dev/concepts/controls) — Define and configure control rules
- [Reference](https://docs.agentcontrol.dev/core/reference) — SDK and server API reference
- [Configuration](https://docs.agentcontrol.dev/core/configuration) — Environment variables, auth, and database settings
- [Server auth contract](auth.md) - Pluggable auth modes, HTTP upstream contract, and runtime JWT claims
- [UI Quickstart](https://docs.agentcontrol.dev/core/ui-quickstart) — Run the dashboard and manage controls visually

## Examples
Expand Down
149 changes: 149 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Server Auth Contract

Agent Control keeps authentication and authorization provider-neutral. The server asks a configured provider whether a request may perform an operation, then scopes all data access with the returned `Principal`.

## Operations

Operations are stable strings. Deployers map them to their own permission model.

```text
controls.read
controls.create
controls.update
controls.delete
policies.read
policies.create
policies.update
agents.read
agents.create
agents.update
control_bindings.read
control_bindings.write
runtime.token_exchange
runtime.use
```

## Principal

Providers return a generic principal. Agent Control treats `namespace_key`, `caller_id`, `target_type`, and `target_id` as opaque strings.

```json
{
"namespace_key": "tenant-a",
"is_admin": false,
"caller_id": "user-or-key-id",
"target_type": "session",
"target_id": "target-123",
"scopes": ["runtime.use"],
"expires_at": "2026-05-11T15:00:00Z"
}
```

`namespace_key` is the tenancy boundary. Server queries filter by it, and namespace-aware foreign keys prevent cross-namespace references.

## Auth Modes

Management auth is selected by `AGENT_CONTROL_AUTH_MODE`.

| Mode | Meaning |
| --- | --- |
| `none` | No credentials required. Intended for local development only. |
| `api_key` | Validate caller credentials locally with `AGENT_CONTROL_API_KEYS`. This is the default. `header` is accepted as a backwards-compatible alias. |
| `http_upstream` | POST each management authorization decision to `AGENT_CONTROL_AUTH_UPSTREAM_URL`. |

Runtime auth is selected by `AGENT_CONTROL_RUNTIME_AUTH_MODE`.

| Mode | Meaning |
| --- | --- |
| unset | Use `jwt` when `AGENT_CONTROL_RUNTIME_TOKEN_SECRET` is set. Otherwise runtime requests fall through to management auth. |
| `none` | No runtime credentials required. Intended for local development only. |
| `api_key` | Validate runtime requests with the same local API-key mechanism. |
| `jwt` | Require target-bound runtime tokens minted by `/api/v1/auth/runtime-token-exchange`. |

Common combinations:

| Management | Runtime | Use case |
| --- | --- | --- |
| `api_key` | unset | Existing standalone deployments. |
| `api_key` | `jwt` | Local management keys with short-lived target-bound runtime tokens. |
| `http_upstream` | `jwt` | External identity or authorization service for management, local token verify for high-volume runtime calls. |
| `none` | `none` | Single-process local development. Do not use in production. |

## HTTP Upstream Contract

When `AGENT_CONTROL_AUTH_MODE=http_upstream`, the server sends:

```http
POST {AGENT_CONTROL_AUTH_UPSTREAM_URL}
```

```json
{
"operation": "control_bindings.write",
"context": {
"target_type": "session",
"target_id": "target-123"
}
}
```

The provider forwards inbound `X-API-Key`, `Authorization`, and `Cookie` headers. Add deployer-specific header names with `AGENT_CONTROL_AUTH_UPSTREAM_EXTRA_FORWARD_HEADERS`, for example:

```text
AGENT_CONTROL_AUTH_UPSTREAM_EXTRA_FORWARD_HEADERS=Vendor-API-Key,X-Workspace-Id
```

If `AGENT_CONTROL_AUTH_UPSTREAM_SERVICE_TOKEN` is set, it is forwarded on `AGENT_CONTROL_AUTH_UPSTREAM_SERVICE_TOKEN_HEADER` or `X-Agent-Control-Service-Token` by default.

A successful upstream response is:

```json
{
"namespace_key": "tenant-a",
"is_admin": false,
"caller_id": "user-or-key-id",
"target_type": "session",
"target_id": "target-123",
"scopes": ["runtime.use"],
"expires_at": "2026-05-11T15:00:00Z"
}
```

Only `namespace_key` is always required. `target_type` and `target_id` must be returned together when present. `expires_at` must include timezone information.

Status handling:

| Upstream status | Agent Control result |
| --- | --- |
| `200` | Parse the principal grant. |
| `401` | Authentication error. |
| `403` | Forbidden error. |
| `404` | Not found error. |
| `429` | `503` with a rate-limit detail and `Retry-After` hint when present. |
| Other statuses or upstream network errors | Fail closed with `503`. |
| Malformed `200` principal response | Fail closed with `502`. |

## Runtime JWT Claims

`/api/v1/auth/runtime-token-exchange` is a management-style request. The configured management provider authorizes `runtime.token_exchange` for the requested target. Agent Control then mints its own HS256 JWT with `AGENT_CONTROL_RUNTIME_TOKEN_SECRET`.

The token payload contains:

```json
{
"iss": "agent-control/server",
"domain": "runtime",
"namespace_key": "tenant-a",
"actor_id": "user-or-key-id",
"target_type": "session",
"target_id": "target-123",
"scopes": ["runtime.use"],
"iat": 1778509800,
"exp": 1778510100,
"jti": "opaque-token-id"
}
```

Verification requires the expected issuer, `domain="runtime"`, a valid signature, an unexpired `exp`, and `runtime.use` in `scopes`. The token is accepted only for requests whose `target_type` and `target_id` match the bound target.

The expiry is the earlier of `AGENT_CONTROL_RUNTIME_TOKEN_TTL_SECONDS` and the upstream grant's `expires_at` when supplied. Runtime token TTLs are capped at 86400 seconds.
3 changes: 1 addition & 2 deletions models/src/agent_control_models/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ class CreateControlBindingRequest(BaseModel):

target_type: ControlBindingTargetField = Field(
...,
description="Opaque attachment kind (caller-defined; e.g. 'env', 'log_stream').",
description="Opaque attachment kind (caller-defined; e.g. 'environment', 'session').",
)
target_id: ControlBindingTargetField = Field(
..., description="Opaque external identifier within the target_type."
Expand Down Expand Up @@ -760,4 +760,3 @@ class DeleteControlBindingByKeyResponse(BaseModel):
),
)


3 changes: 1 addition & 2 deletions sdks/typescript/src/generated/funcs/agents-get-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ import { Result } from "../types/fp.js";
* agent_name: Agent identifier
* evaluator_name: Name of the evaluator
* db: Database session (injected)
* namespace_key: Resolved namespace; agents in another namespace
* return 404 (non-disclosing).
* principal: Authorized request principal
*
* Returns:
* EvaluatorSchemaItem with schema details
Expand Down
3 changes: 1 addition & 2 deletions sdks/typescript/src/generated/funcs/agents-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ import { Result } from "../types/fp.js";
* Args:
* agent_name: Agent identifier
* db: Database session (injected)
* namespace_key: Resolved namespace; agents in another namespace
* return 404 (non-disclosing).
* principal: Authorized request principal
*
* Returns:
* GetAgentResponse with agent metadata and step list
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/src/generated/funcs/agents-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import { Result } from "../types/fp.js";
* Args:
* request: Agent metadata and step schemas
* db: Database session (injected)
* principal: Authorized request principal for the agent create operation
* target_principal: Optional principal from the target binding read check
*
* Returns:
* InitAgentResponse with created flag and the effective controls
Expand Down
3 changes: 2 additions & 1 deletion sdks/typescript/src/generated/funcs/agents-list-controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ import { Result } from "../types/fp.js";
* target_type: Optional opaque target kind (paired with target_id)
* target_id: Optional opaque target identifier (paired with target_type)
* db: Database session (injected)
* namespace_key: Namespace scoping for the resolution (injected)
* principal: Authorized request principal for the agent read operation
* target_principal: Optional principal from the target binding read check
*
* Returns:
* AgentControlsResponse with controls matching the requested state filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ import { Result } from "../types/fp.js";
* cursor: Optional cursor for pagination (name of last evaluator from previous page)
* limit: Pagination limit (default 20, max 100)
* db: Database session (injected)
* namespace_key: Resolved namespace; agents in another namespace
* return 404 (non-disclosing).
* principal: Authorized request principal
*
* Returns:
* ListEvaluatorsResponse with evaluator schemas and pagination
Expand Down
2 changes: 1 addition & 1 deletion sdks/typescript/src/generated/funcs/agents-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Result } from "../types/fp.js";
* limit: Pagination limit (default 20, max 100)
* name: Optional name filter (case-insensitive partial match)
* db: Database session (injected)
* namespace_key: Resolved namespace for the request
* principal: Authorized request principal
*
* Returns:
* ListAgentsResponse with agent summaries and pagination info
Expand Down
1 change: 1 addition & 0 deletions sdks/typescript/src/generated/funcs/agents-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { Result } from "../types/fp.js";
* agent_name: Agent identifier
* request: Lists of step/evaluator identifiers to remove
* db: Database session (injected)
* principal: Authorized request principal
*
* Returns:
* PatchAgentResponse with lists of actually removed items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ import { Result } from "../types/fp.js";
* @remarks
* Mint a short-lived runtime token for the requested target.
*
* The caller's credential is authenticated and authorized by the
* installed default authorizer; the resulting :class:`Principal`
* supplies the actor identity and (when the upstream surfaces it)
* the grant scopes and expiry. This endpoint then mints a local HS256
* token whose lifetime cannot outlive the upstream grant.
* The caller's credential is authenticated and authorized before the
* resolved principal supplies the actor identity, grant scopes, and
* expiry. This endpoint then mints a local HS256 token whose lifetime
* cannot outlive the grant.
*
* Runtime auth must be enabled via
* ``AGENT_CONTROL_RUNTIME_TOKEN_SECRET``; otherwise the endpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ import { Result } from "../types/fp.js";
* @remarks
* Attach a control to an opaque external target.
*
* Each binding row is scoped to the request namespace as resolved by
* ``get_namespace_key``. The auth chain still runs via
* ``require_operation`` for authentication and authorization, but the
* storage namespace is taken from the same resolver the rest of the
* server uses so binding writes and runtime reads stay in lockstep
* until auth-derived namespace resolution lands across every endpoint.
* Each binding row is scoped to the namespace associated with the
* authenticated request.
*/
export function controlBindingsCreate(
client: AgentControlSDKCore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js";
* See the GET-by-id docstring for the authorization scope: this route
* is namespace-wide because the target identifiers are not available
* before the binding is loaded. Use ``POST /by-key:delete`` for
* target-scoped detach that forwards the target to the authorizer.
* target-scoped detach that includes the target in the request context.
*/
export function controlBindingsDelete(
client: AgentControlSDKCore,
Expand Down
5 changes: 2 additions & 3 deletions sdks/typescript/src/generated/funcs/control-bindings-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ import { Result } from "../types/fp.js";
* Read a single control binding by surrogate ID.
*
* Authorization is namespace-wide: the binding's target identifiers
* are not forwarded to the upstream because they are only discoverable
* after the row is loaded, and ``require_operation`` is single-pass.
* are not available until after the row is loaded.
* Callers whose authorization model requires per-target permissions
* should use the natural-key endpoints (``PUT /by-key``,
* ``POST /by-key:delete``) and the target-filtered list endpoint, all
* of which forward ``(target_type, target_id)`` to the authorizer.
* of which include ``(target_type, target_id)`` in the request context.
*/
export function controlBindingsGet(
client: AgentControlSDKCore,
Expand Down
3 changes: 1 addition & 2 deletions sdks/typescript/src/generated/funcs/control-bindings-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ import { Result } from "../types/fp.js";
* cursor-based pagination. Bindings are ordered by ID descending
* (newest first). The cursor is opaque to clients: pass back the
* ``next_cursor`` value verbatim to fetch the following page. The
* storage namespace is resolved by ``get_namespace_key`` so this
* listing stays in lockstep with the rest of the server's reads.
* storage namespace is resolved from the authenticated request.
*/
export function controlBindingsList(
client: AgentControlSDKCore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js";
* See the GET-by-id docstring for the authorization scope: this route
* is namespace-wide because the target identifiers are not available
* before the binding is loaded. Use ``PUT /by-key`` for target-scoped
* upserts that forward the target to the authorizer.
* upserts that include the target in the request context.
*/
export function controlBindingsUpdate(
client: AgentControlSDKCore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type CreateControlBindingRequest = {
*/
targetId: string;
/**
* Opaque attachment kind (caller-defined; e.g. 'env', 'log_stream').
* Opaque attachment kind (caller-defined; e.g. 'environment', 'session').
*/
targetType: string;
};
Expand Down
4 changes: 0 additions & 4 deletions sdks/typescript/src/generated/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ export * from "./init-agent-evaluator-removal.js";
export * from "./init-agent-overwrite-changes.js";
export * from "./init-agent-request.js";
export * from "./init-agent-response.js";
export * from "./json-value-input.js";
export * from "./json-value-input1.js";
export * from "./json-value-output.js";
export * from "./json-value-output1.js";
export * from "./list-agents-response.js";
export * from "./list-control-bindings-response.js";
export * from "./list-control-versions-response.js";
Expand Down
40 changes: 0 additions & 40 deletions sdks/typescript/src/generated/models/json-value-input.ts

This file was deleted.

Loading
Loading