Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex, TableUnique } from 'typeorm';

/**
* Per-agent OpenAI-compatible API keys for agent-controller `/api/openai` routes.
*/
export class CreateClientAgentOpenAiApiKeysTable1766800000000 implements MigrationInterface {
name = 'CreateClientAgentOpenAiApiKeysTable1766800000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'client_agent_openai_api_keys',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'client_id',
type: 'uuid',
isNullable: false,
},
{
name: 'agent_id',
type: 'uuid',
isNullable: false,
},
{
name: 'api_key_encrypted',
type: 'text',
isNullable: false,
},
{
name: 'api_key_hash',
type: 'varchar',
length: '64',
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
}),
true,
);

await queryRunner.createUniqueConstraint(
'client_agent_openai_api_keys',
new TableUnique({
name: 'uq_openai_client_agent',
columnNames: ['client_id', 'agent_id'],
}),
);

await queryRunner.createUniqueConstraint(
'client_agent_openai_api_keys',
new TableUnique({
name: 'uq_openai_api_key_hash',
columnNames: ['api_key_hash'],
}),
);

await queryRunner.createForeignKey(
'client_agent_openai_api_keys',
new TableForeignKey({
columnNames: ['client_id'],
referencedColumnNames: ['id'],
referencedTableName: 'clients',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
}),
);

await queryRunner.createIndex(
'client_agent_openai_api_keys',
new TableIndex({
name: 'IDX_openai_keys_client_id',
columnNames: ['client_id'],
}),
);
await queryRunner.createIndex(
'client_agent_openai_api_keys',
new TableIndex({
name: 'IDX_openai_keys_agent_id',
columnNames: ['agent_id'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('client_agent_openai_api_keys', 'IDX_openai_keys_agent_id');
await queryRunner.dropIndex('client_agent_openai_api_keys', 'IDX_openai_keys_client_id');
const table = await queryRunner.getTable('client_agent_openai_api_keys');
const fk = table?.foreignKeys.find((f) => f.columnNames.indexOf('client_id') !== -1);
if (fk) {
await queryRunner.dropForeignKey('client_agent_openai_api_keys', fk);
}
await queryRunner.dropUniqueConstraint('client_agent_openai_api_keys', 'uq_openai_api_key_hash');
await queryRunner.dropUniqueConstraint('client_agent_openai_api_keys', 'uq_openai_client_agent');
await queryRunner.dropTable('client_agent_openai_api_keys');
}
}
2 changes: 2 additions & 0 deletions apps/backend-agent-controller/src/typeorm.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ClientAgentOpenAiApiKeyEntity,
ProvisioningReferenceEntity,
StatisticsAgentEntity,
StatisticsChatFilterDropEntity,
Expand Down Expand Up @@ -35,6 +36,7 @@ export const typeormConfig: DataSourceOptions = {
entities: [
ClientEntity,
ClientAgentCredentialEntity,
ClientAgentOpenAiApiKeyEntity,
ClientUserEntity,
ProvisioningReferenceEntity,
UserEntity,
Expand Down
1 change: 1 addition & 0 deletions docs/agenstra/api-reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The Agent Controller HTTP API provides:

- Client management (CRUD operations)
- Proxied agent operations (create, update, delete agents)
- **OpenAI-compatible inference** under `/openai/v1/*` (per-agent API keys; SSE streaming where supported)
- Proxied file operations (read, write, create, delete files)
- Proxied version control operations (git status, branches, commit, push, pull, rebase)
- Server provisioning (Hetzner Cloud, DigitalOcean)
Expand Down
16 changes: 14 additions & 2 deletions docs/agenstra/applications/backend-agent-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The application integrates the `@forepath/framework-backend-feature-agent-contro

## API Endpoints

All HTTP endpoints are prefixed with `/api` and protected by Keycloak authentication (or API key authentication if `STATIC_API_KEY` is set).
Most HTTP endpoints are prefixed with `/api` and protected by Keycloak authentication (or API key authentication if `STATIC_API_KEY` is set). **Exception:** **`/api/openai/*`** uses **per-agent OpenAI-compatible keys** (`agenstra_oai_...`) only—platform JWT/static API key is not used there.

### Client Management

Expand All @@ -56,9 +56,21 @@ In api-key mode, users do not play a role; these endpoints are not applicable.

- `GET /api/clients/:id/agents` - List all agents for a client
- `GET /api/clients/:id/agents/:agentId` - Get a single agent by UUID
- `POST /api/clients/:id/agents` - Create a new agent for a client (returns auto-generated password, saves credentials)
- `POST /api/clients/:id/agents` - Create a new agent for a client (returns auto-generated password, saves credentials, and a one-time **OpenAI API key** for `/api/openai`)
- `POST /api/clients/:id/agents/:agentId` - Update an existing agent
- `DELETE /api/clients/:id/agents/:agentId` - Delete an agent (also deletes stored credentials)
- `POST /api/clients/:id/agents/:agentId/openai-api-key/rotate` - Rotate the per-agent OpenAI key (plaintext returned once)

### OpenAI-compatible API (external tools)

Routes under **`/api/openai/v1/*`** mirror a small OpenAI HTTP surface so external clients can call agents without the platform auth model.

- **Auth:** `Authorization: Bearer <per-agent key>` (or `ApiKey <key>`). The key uniquely identifies the agent; the JSON **`model`** field is the model id for that agent.
- **Endpoints:** `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/completions`, `POST /v1/responses`. When `stream: true`, responses use **SSE** (`text/event-stream`).
- **Storage:** Keys are encrypted at rest; **`ENCRYPTION_KEY`** must be set for agent-controller (enforced in production).
- **Not implemented:** embeddings, audio, images, realtime WS, files, batches, fine-tuning, full Responses tool/multimodal parity.

See the Agent Controller OpenAPI spec and `libs/domains/framework/backend/feature-agent-controller/docs/openai-sequence.mmd`.

### Proxied File Operations

Expand Down
26 changes: 23 additions & 3 deletions libs/domains/framework/backend/feature-agent-controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ The library follows Domain-Driven Design (DDD) principles with clear separation
- **Entities**:
- `ClientEntity` - Domain model representing a client (remote agent-manager service)
- `ClientAgentCredentialEntity` - Stores credentials for agents created via proxied requests
- `ClientAgentOpenAiApiKeyEntity` - Per-agent OpenAI-compatible API keys (encrypted at rest, SHA-256 hash for lookup)
- `ClientUserEntity` - Many-to-many relationship between users and clients with per-client roles
- **Repositories**:
- `ClientsRepository` - Data access layer for client operations
- `ClientAgentCredentialsRepository` - Data access layer for agent credentials
- `ClientAgentOpenAiApiKeysRepository` - Data access layer for per-agent OpenAI API keys
- `ClientUsersRepository` - Data access layer for client-user relationships
- **Services**:
- `ClientsService` - Business logic orchestration for clients with permission checks
Expand All @@ -47,13 +49,16 @@ The library follows Domain-Driven Design (DDD) principles with clear separation
- `ClientAgentFileSystemProxyService` - Proxies file system operations to remote agent-manager services
- `ClientAgentEnvironmentVariablesProxyService` - Proxies environment variable operations to remote agent-manager services
- `ClientAgentCredentialsService` - Manages stored agent credentials
- `ClientAgentOpenAiApiKeysService` - Issues and rotates per-agent OpenAI keys; resolves key → agent
- `OpenAiAgentWsProxyService` - Bridges OpenAI HTTP requests to remote agent-manager `chat` / `chatEvent` over Socket.IO
- `KeycloakTokenService` - Handles Keycloak OAuth2 Client Credentials flow with token caching
- **DTOs**: Data transfer objects for API boundaries
- `CreateClientDto` - Input validation for creating clients
- `UpdateClientDto` - Input validation for updating clients
- `ClientResponseDto` - Safe API responses (excludes sensitive data, includes proxied config with agent types from remote agent-manager)
- `CreateClientResponseDto` - Response when creating client (includes API key if applicable)
- **Controllers**: `ClientsController` - HTTP endpoints for client and proxied agent management (protected by Keycloak)
- **Controllers**: `ClientsController` - HTTP endpoints for client and proxied agent management (protected by Keycloak); `OpenAiV1Controller` - OpenAI-shaped routes under `/openai` (per-agent key auth, `@Public()`)
- **Guards**: `OpenAiApiKeyGuard` - Validates `Authorization: Bearer <agenstra_oai_...>` (or `ApiKey`) and attaches agent context
- **Gateways**: `ClientsGateway` - WebSocket gateway for forwarding events to remote agent-manager WebSocket endpoints
- **Modules**: `ClientsModule` - NestJS module wiring all dependencies

Expand All @@ -77,6 +82,7 @@ All diagrams are available in the [`docs/`](./docs/) directory:
- **[WebSocket Forwarding Diagram](./docs/sequence-ws-forward.mmd)** - Sequence diagram for WebSocket connection, client context setup, event forwarding, and auto-login
- **[Chat prompt enhancement](./docs/sequence-chat-enhancement.mmd)** - Sequence for `enhanceChat` / `chatEnhanceResult` (magic-wand flow; statistics only, no `agent_messages`)
- **[Lifecycle Diagram](./docs/lifecycle.mmd)** - End-to-end sequence diagram showing the complete lifecycle from client creation through proxied agent operations to WebSocket event forwarding
- **[OpenAI HTTP bridge](./docs/openai-sequence.mmd)** - Sequence for `/api/openai/v1/*` → DB key lookup → agent-manager `/agents` `chat` / SSE streaming

These diagrams provide comprehensive visual documentation of:

Expand Down Expand Up @@ -182,7 +188,7 @@ When listing clients (`GET /api/clients`), only clients the user has access to a

## API Endpoints

All HTTP endpoints require authentication (except `/api/health` and public auth endpoints). The authentication method depends on `AUTHENTICATION_METHOD`; see [Authentication](#authentication) above.
All HTTP endpoints require authentication (except `/api/health`, public auth endpoints, and **`/api/openai/*`**, which use **per-agent OpenAI keys** only). The authentication method depends on `AUTHENTICATION_METHOD`; see [Authentication](#authentication) above.

**Note**: All endpoints that access a specific client (`/api/clients/:id/*`) now check permissions. Users without access will receive a `403 Forbidden` response.

Expand All @@ -201,9 +207,23 @@ Base URL: `/api`
- `GET /api/clients/:id/agents` - List all agents for a client (supports `limit` and `offset` query parameters)
- `GET /api/clients/:id/agents/:agentId` - Get a single agent by UUID
- `GET /api/clients/:id/agents/:agentId/models` - List models for an agent (proxied; same client access rules as get agent)
- `POST /api/clients/:id/agents` - Create a new agent for a client (returns auto-generated password, saves credentials)
- `POST /api/clients/:id/agents` - Create a new agent for a client (returns auto-generated password, saves credentials, and a one-time **OpenAI API key** for `/api/openai`)
- `POST /api/clients/:id/agents/:agentId` - Update an existing agent
- `DELETE /api/clients/:id/agents/:agentId` - Delete an agent (also deletes stored credentials)
- `POST /api/clients/:id/agents/:agentId/openai-api-key/rotate` - Rotate the per-agent OpenAI key (returns new plaintext once; requires normal platform auth)

### OpenAI-compatible HTTP (`/api/openai`)

For external tools and SDKs that expect an OpenAI-style API. These routes are **`@Public()`** with respect to platform JWT/static API key: you must send **`Authorization: Bearer <per-agent key>`** (or `ApiKey <key>`). The key selects **one agent** (and its client); the request `model` field is the model id for that agent.

- `GET /api/openai/v1/models` - List models (proxied from agent-manager for that agent)
- `POST /api/openai/v1/chat/completions` - Chat completions; `stream: true` returns **SSE** (`text/event-stream`)
- `POST /api/openai/v1/completions` - Legacy completions (string/array prompt)
- `POST /api/openai/v1/responses` - Minimal Responses API subset (text-oriented)

**Encryption:** Per-agent keys are stored encrypted (AES-256-GCM). Set **`ENCRYPTION_KEY`** in the agent-controller environment (required in production); see the app `README` and `main.ts` bootstrap checks.

**Not implemented** on this surface: embeddings, audio, images, realtime WebSocket, files, batches, fine-tuning, and full Responses tool/multimodal parity.

**Note**: Agent creation requests are proxied to the remote agent-manager service. SSH repository configuration (including `GIT_PRIVATE_KEY`) must be configured on the agent-manager instance via environment variables, not through the API request. See the [agent-manager documentation](../feature-agent-manager/README.md) for details on SSH repository setup.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
sequenceDiagram
participant ExtTool as ExternalTool(OpenAI_SDK)
participant AgentCtrl as AgentController(/api/openai)
participant DB as AgentControllerDB
participant ClientAM as ClientAgentManager(SocketIO_/agents)

ExtTool->>AgentCtrl: POST /v1/chat/completions (Bearer agent_openai_key)
AgentCtrl->>DB: Lookup api_key_hash -> (clientId, agentId)
AgentCtrl->>ClientAM: Connect + Authorization (client API key / JWT)
AgentCtrl->>ClientAM: login(agentId, password_from_DB)
AgentCtrl->>ClientAM: chat(message, model, responseMode)
ClientAM-->>AgentCtrl: chatEvent assistantDelta (stream)
AgentCtrl-->>ExtTool: SSE chunks (delta)
ClientAM-->>AgentCtrl: chatEvent assistantMessage (final)
AgentCtrl-->>ExtTool: final SSE + [DONE]
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ info:
All connections require authentication via the `Authorization` header in the handshake
(same as HTTP API: `Bearer <jwt-token>` or `Bearer <api-key>` / `ApiKey <api-key>`).
Unauthenticated connections are rejected with connect_error "Unauthorized".

**OpenAI-compatible HTTP (not this AsyncAPI):** Agent Controller also exposes `/api/openai/v1/*`
for external tools. Those routes use a **per-agent** bearer secret (`agenstra_oai_...`), not platform JWT.
Streaming there is **HTTP Server-Sent Events (SSE)**, not WebSocket. Internally, the controller bridges
to the remote agent-manager `/agents` namespace (`chat` / `chatEvent` assistant deltas and final messages);
see the Agent Controller OpenAPI spec and `docs/openai-sequence.mmd`.
The `setClient` operation enforces per-client authorization: only users with access to
the requested client (global admin, client creator, or client_users entry) can set that
client context. Unauthorized setClient attempts emit an `error` event with message
Expand Down
Loading
Loading