¶ciel.gateway — Superficie de gateway
Bloques de construcción públicos del gateway:
create_control_app— plano de control FastAPI para el runtime del agente.gateway.mcp— cliente MCP (stdio/HTTP) + host MCP + integración con el runtime.WebhookAdapter/SlackAdapter— adapters de mensajería entrante.create_webhook_router,create_slack_webhook_router,create_teams_webhook_router,create_discord_webhook_router,create_webui_router— montan adapters en una app FastAPI existente.mount_mcp_app— app FastAPI que expone el endpoint MCP host (/mcp).make_app— construye la aplicación FastAPI completa del gateway.
¶
ciel.gateway
Ciel gateway surface.
Exposes the public gateway building blocks:
- :func:
gateway.base.create_control_app— FastAPI control plane for the agent runtime (/health,/info,/v1/agent/run,/v1/tools/...,/v1/board/list). - :mod:
gateway.mcp— MCP client (stdio/HTTP) + MCP server host + runtime integration, implemented as a native JSON-RPC endpoint. - :class:
gateway.adapter.WebhookAdapter— generic inbound messaging adapter. - :func:
create_webhook_router— mounts a messaging adapter into an existing FastAPI app so external channels can feed the agent. - :func:
mount_mcp_app— builds a FastAPI app exposing the MCP host endpoint (/mcp) backed by a runtime dispatcher, ready to serve over HTTP.
¶
__all__ = ['create_control_app', 'MCPClient', 'MCPServer', 'DefaultAgentRuntimeMCPHost', 'MCPHostToolProvider', 'Message', 'MessagingAdapter', 'WebhookAdapter', 'SlackAdapter', 'create_webhook_router', 'create_slack_webhook_router', 'create_teams_webhook_router', 'create_discord_webhook_router', 'create_webui_router', 'mount_mcp_app', 'make_app']
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
DefaultAgentRuntimeMCPHost
Host DefaultAgentRuntime behavior through an MCP server boundary.
Source code in src/ciel/gateway/mcp/integration.py
¶
MCPClient
MCP protocol client over an explicit transport tenant-aware runtime.
Source code in src/ciel/gateway/mcp/client.py
¶
MCPHostToolProvider
Bases: ToolProvider
Expose an MCP server as a Ciel ToolProvider.
Source code in src/ciel/gateway/mcp/integration.py
¶
MCPServer
Minimal MCP-style JSON-RPC endpoint with tenant-aware handlers.
Source code in src/ciel/gateway/mcp/server.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
¶
Message
dataclass
Immutable DTO representing a single inbound or outbound message.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique message identifier. Generated via |
channel |
str
|
Logical channel the message arrived on (required). |
sender |
Optional[str]
|
Optional sender identifier (may be |
content |
'str | list[dict[str, Any]]'
|
Message body / content (required). |
metadata |
Dict[str, Any]
|
Free-form metadata dict; defaults to an empty dict. |
Source code in src/ciel/gateway/adapter.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
Mirrors :meth:ciel.runtime.ChatMessage.text: returns str content
verbatim, or concatenates the text of every text part in a list.
Source code in src/ciel/gateway/adapter.py
¶
MessagingAdapter
Abstract base class for messaging adapters.
Subclasses implement :meth:receive (an async generator yielding
inbound :class:Message objects) and :meth:send (delivery of an
outbound :class:Message).
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Yield inbound messages as they arrive.
Implementations should be async generators that block until a
message is available and then yield it.
Source code in src/ciel/gateway/adapter.py
¶
SlackAdapter
Bases: MessagingAdapter
Bidirectional Slack adapter built on slack_sdk.WebClient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
A Slack bot/user token ( |
required |
channel
|
Optional[str]
|
Optional default channel (e.g. |
None
|
bot_user_id
|
Optional[str]
|
Optional bot user id ( |
None
|
The Slack WebClient is created lazily so that merely importing this module
(or instantiating the adapter) is safe even when slack_sdk is not
installed. The client is built on first use of :meth:send / :meth:receive.
Source code in src/ciel/gateway/adapter_slack.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
¶
enqueue(message: Message) -> None
Push an inbound :class:Message onto the internal queue.
Used by the Slack webhook router to feed real message.changed
events into the adapter; consumers can drain them via
:meth:receive (which is overridden below to prefer the queue).
Source code in src/ciel/gateway/adapter_slack.py
¶
receive() -> AsyncIterator[Message]
async
Yield inbound Slack messages via a simple polling loop.
This is a lenient, best-effort implementation: it relies on Slack's
conversations.history to pull recent messages and yields any that
are not authored by the bot user. It polls every few seconds.
The primary, fully-tested contract of this adapter is :meth:send.
The receive loop is provided so the adapter is genuinely bidirectional,
but it is intentionally simple and skips advanced cursor pagination and
deduplication. Inbound event handling is better served by mounting the
Slack webhook router (see :func:create_slack_webhook_router) onto the
FastAPI app, which enqueues real message.changed events.
The loop runs forever; the caller is responsible for cancellation.
Source code in src/ciel/gateway/adapter_slack.py
¶
send(message: Message) -> None
async
Send message.content to Slack via chat.postMessage.
The destination channel is resolved from message.metadata['channel']
falling back to the adapter's default channel. A clear
:class:ValueError is raised when no channel can be resolved.
Network access is required for this call to succeed against the real
Slack API; tests pass a mocked WebClient to verify behaviour
without touching the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
Message
|
The :class: |
required |
Source code in src/ciel/gateway/adapter_slack.py
¶
WebhookAdapter
Bases: MessagingAdapter
Messaging adapter that ingests messages via an HTTP webhook endpoint.
Incoming webhook payloads are validated, converted to :class:Message
instances, and enqueued on an internal :class:asyncio.Queue. The
:meth:receive async generator drains the queue and yields messages
to consumers.
Typical usage alongside an HTTP framework::
adapter = WebhookAdapter()
@app.post("/webhook")
async def webhook(request):
payload = await request.json()
return await adapter.handle_webhook(payload)
async for message in adapter.receive():
...
Source code in src/ciel/gateway/adapter.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
¶
handle_webhook(payload: dict) -> dict
async
Validate a webhook payload, enqueue a :class:Message, and respond.
Expected payload shape::
{"id": "...", "channel": "...", "sender": "...",
"content": "...", "metadata": {...}}
channel and content are required. id is generated
via :func:uuid.uuid4 when absent. sender defaults to
None and metadata defaults to an empty dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict
|
Raw deserialized webhook body. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict
|
|
|
dict
|
or ``{"status": "rejected", "error": "missing required |
|
field |
dict
|
|
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Async generator yielding messages from the internal queue.
Blocks indefinitely on each :meth:asyncio.Queue.get call until
a message becomes available, then yields it. The loop runs
forever — the caller is responsible for cancellation/timeout.
Source code in src/ciel/gateway/adapter.py
¶
receive_internal() -> Message
async
Drain and return a single message from the queue.
Convenience method for consumers that prefer one-at-a-time polling over the async generator. Blocks until a message is available.
Returns:
| Type | Description |
|---|---|
Message
|
The next :class: |
Source code in src/ciel/gateway/adapter.py
¶
send(message: Message) -> None
async
Outbound send — not implemented for the webhook adapter.
The webhook adapter is inbound-only by design. Subclass or compose with an outbound adapter if delivery is needed.
Source code in src/ciel/gateway/adapter.py
¶
create_control_app(*, runtime: DefaultAgentRuntime, registry: Optional[ProviderRegistry] = None, audit_sink: Any = None, tenant_id: Optional[str] = None, api_key: Optional[str] = None, board_db_path: Optional[str] = None) -> FastAPI
Build a FastAPI control plane for the Ciel agent runtime.
¶Parameters
runtime:
A :class:ciel.runtime.DefaultAgentRuntime used to run agent loops and
dispatch tools. Tool dispatch goes through runtime.dispatcher.
El endpoint POST /v1/agent/run/stream usa
runtime.stream_tokens para emitir Server-Sent Events (SSE) con los
fragmentos incrementales del assistant y cierra con data: [DONE].
registry:
Optional :class:ciel.providers.ProviderRegistry surfaced via the
GET /info endpoint. If omitted, an empty provider list is reported.
audit_sink:
Optional audit sink retained for observability hooks. Not directly
invoked here — the runtime already emits audit events.
tenant_id:
Default tenant propagated to the runtime when a request does not
provide its own tenant_id.
api_key:
Optional transport API key. When None (the default), the
CIEL_API_KEY environment variable is consulted. If a key is
configured (explicitly or via env), protected routes require a valid
key via Authorization: Bearer *** orX-API-Key; otherwise
the dependency is a no-op (open mode).
board_db_path:
Optional path to a SQLite database for the kanban board. When set (o
bien vía la variable de entornoCIEL_BOARD_DB), el board se monta
sobre SQLite para que/v1/board/list`` vea las tareas creadas por el
CLI y viceversa. Si no se configura ninguna ruta, el board queda en
memoria (comportamiento legacy, útil para smoke tests/offline).
Source code in src/ciel/gateway/base.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
¶
create_discord_webhook_router(adapter, *, path: str = '/v1/messaging/discord') -> APIRouter
Router que ingiere eventos MESSAGE_CREATE de Discord al adapter.
Discord entrega un JSON con content, author/id y channel_id.
El router lo convierte en Message y lo enqueuea.
Source code in src/ciel/gateway/messaging.py
¶
create_slack_webhook_router(adapter: 'SlackAdapter', *, path: str = '/v1/messaging/slack', signing_secret: str | None = None)
Build an APIRouter that ingests inbound Slack events into adapter.
Slack delivers events (url_verification challenges and
event_callback payloads) as JSON POST requests. This router:
- answers the
url_verificationchallenge so the Slack app can be installed/verified, and - for
messageevents, converts the Slack event into a :class:~ciel.gateway.adapter.Messageand enqueues it for the agent.
The adapter is expected to expose an enqueue method (a subclass of
:class:SlackAdapter that also holds an internal queue — e.g. via
composition with :class:~ciel.gateway.adapter.WebhookAdapter) so events
can be drained by the runtime. To keep coupling low, this router calls
adapter.enqueue(...) when present and otherwise logs.
¶Parameters
adapter:
A :class:SlackAdapter (or compatible) that should receive inbound
events.
path:
Route under which the Slack endpoint is exposed.
signing_secret:
Optional Slack signing secret used for request verification. When
provided, payloads are verified; when omitted, verification is skipped
(acceptable for local/offline smoke testing).
Source code in src/ciel/gateway/__init__.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
¶
create_teams_webhook_router(adapter, *, path: str = '/v1/messaging/teams', signing_secret: Optional[str] = None) -> APIRouter
Router que ingiere eventos de Microsoft Teams al adapter.
Teams entrega cargas JSON con text (y opcionalmente from /
channelData). El router los convierte en Message y los enqueuea.
Si se pasa signing_secret se puede validar la firma HMAC (fuera de
alcance en smoke tests); sin él, se acepta el payload tal cual.
Source code in src/ciel/gateway/messaging.py
¶
create_webhook_router(adapter: WebhookAdapter, *, path: str = '/v1/messaging/webhook', api_key: Optional[str] = None)
Build an APIRouter that pushes inbound HTTP messages into adapter.
The router exposes POST {path} (accept a message) and
GET {path}/health. It is intentionally decoupled from a concrete agent
runtime so it can be composed by any surface (control gateway, ACP, MCP).
Source code in src/ciel/gateway/__init__.py
¶
create_webui_router(adapter, *, path: str = '/v1/messaging/webui') -> APIRouter
Router de Web UI: POST para enviar mensajes al runtime, GET para sondear.
La UI hace POST {path} con un Message y lo enqueuea en el
WebUIAdapter; GET {path}/outbound sondea el siguiente mensaje
saliente del adapter (polling).
Source code in src/ciel/gateway/messaging.py
¶
make_app(*, tenant_id: Optional[str] = None, include_mcp: bool = True, include_webhook: bool = True, state_backend: Optional[object] = None) -> FastAPI
Compose the full Ciel gateway into a single FastAPI app.
¶Parameters
tenant_id:
Default tenant propagated to the runtime when a request omits its own
tenant_id. Falls back to the CIEL_TENANT environment variable.
include_mcp:
Mount the MCP host app at /mcp.
include_webhook:
Mount the webhook messaging router at /v1/messaging/webhook.
state_backend:
Optional pre-built :class:StateBackend (Fase 14 / F15). When omitted,
one is built from CIEL_STATE_BACKEND / CIEL_STATE_DSN (default
SQLite). Injecting a shared backend lets tests and multi-replica
deployments reuse the same state source.
Source code in src/ciel/gateway/server.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
¶
mount_mcp_app(*, dispatcher=None, tenant_id: str | None = None, path: str = '/mcp') -> 'FastAPI'
Build a FastAPI app that serves the Ciel MCP host endpoint.
The endpoint at {path} accepts MCP JSON-RPC payloads and routes them to
a :class:MCPServer wired to the runtime's tool provider. Callers normally
mount this under the control gateway or run it as a standalone service.
¶Parameters
dispatcher:
Optional :class:ciel.runtime.DefaultToolDispatcher. When omitted, a
bare :class:MCPServer with no provider is created (logs initialize /
shutdown only).
tenant_id:
Default tenant used for audit propagation.
path:
Route under which the MCP endpoint is exposed.
Source code in src/ciel/gateway/__init__.py
¶
__all__ = ['create_control_app']
module-attribute
¶
__version__ = _pkg_version('ciel')
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
AgentRunRequest
Bases: BaseModel
Source code in src/ciel/gateway/base.py
¶
AgentRunResponse
¶
AgentRuntimeResult
dataclass
¶
BoardListResponse
¶
BoardTaskPayload
¶
ChatMessage
dataclass
Source code in src/ciel/runtime/tools.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
strcontent is returned verbatim.listcontent concatenates thetextof every part whose type is"text"but drops images/audio/video, so consumers (CLI, compression,AgentResponse.text) see only readable text.
Source code in src/ciel/runtime/tools.py
¶
ChatRequest
dataclass
Source code in src/ciel/runtime/tools.py
¶
ChatResponse
dataclass
¶
DefaultAgentRuntime
Concrete runtime wiring provider + tool execution with tracing.
Source code in src/ciel/runtime/__init__.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
¶
run_agent_loop(*, request: ChatRequest, tenant_id: Optional[str] = None, toolset: Optional[str] = None, limit: int = 32) -> AgentRuntimeResult
async
Run the agentic tool loop (multi-turn ReAct when tools are present).
The loop issues up to limit model completions. After each completion
that requests tool calls, the requested tools are dispatched and their
results are appended as messages; the model is then called again. The
loop stops when (a) the model returns no tool calls (finish_reason
stop), (b) limit turns are exhausted, or (c) there are no tools
registered in the request.
Backward compatibility: when limit <= 1 or request.tools is
empty, exactly one completion is produced and no tool messages are
appended (the historical single-step behaviour). Every tool result from
every turn is preserved in loop_results so the facade can surface
the full tool_results list.
Source code in src/ciel/runtime/__init__.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
¶
stream_tokens(*, request: ChatRequest, tenant_id: Optional[str] = None, toolset: Optional[str] = None) -> AsyncIterator[str]
async
Stream incremental assistant tokens from the provider.
Calls provider.stream (real SSE streaming) and re-emits the
partial content of each incremental :class:ChatResponse as it
arrives, so callers see the answer grow token by token.
Source code in src/ciel/runtime/__init__.py
¶
DefaultToolDispatcher
Dispatch tool requests to a configured ToolProvider.
Source code in src/ciel/runtime/__init__.py
¶
HealthResponse
¶
InfoResponse
¶
KanbanBoard
In-memory kanban board, optionally backed by a durable SQLite store.
When path is None (the default) the board keeps tasks in a process
local dict exactly as before -- this preserves the existing in-memory
API used by the gateway and the CLI. When path is provided the board
transparently reads and writes every task to a SQLite database opened in
WAL mode, so state survives process restarts.
Source code in src/ciel/orchestration/board.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
¶
LivenessResponse
¶
ProviderInfo
¶
ProviderRegistry
Source code in src/ciel/providers/__init__.py
¶
ReadinessResponse
Bases: BaseModel
Readiness: el gateway puede atender tráfico (backend conectado + migrado).
Source code in src/ciel/gateway/base.py
¶
ToolInvokeRequest
¶
ToolInvokeResponse
¶
ToolResult
dataclass
¶
ToolResultPayload
¶
_list_board_tasks(board: KanbanBoard, tenant_id: Optional[str]) -> List[BoardTaskPayload]
Source code in src/ciel/gateway/base.py
¶
_serialize_chat_response(result: AgentRuntimeResult) -> AgentRunResponse
Source code in src/ciel/gateway/base.py
¶
_tool_result_to_payload(result: ToolResult) -> ToolResultPayload
Source code in src/ciel/gateway/base.py
¶
create_control_app(*, runtime: DefaultAgentRuntime, registry: Optional[ProviderRegistry] = None, audit_sink: Any = None, tenant_id: Optional[str] = None, api_key: Optional[str] = None, board_db_path: Optional[str] = None) -> FastAPI
Build a FastAPI control plane for the Ciel agent runtime.
¶Parameters
runtime:
A :class:ciel.runtime.DefaultAgentRuntime used to run agent loops and
dispatch tools. Tool dispatch goes through runtime.dispatcher.
El endpoint POST /v1/agent/run/stream usa
runtime.stream_tokens para emitir Server-Sent Events (SSE) con los
fragmentos incrementales del assistant y cierra con data: [DONE].
registry:
Optional :class:ciel.providers.ProviderRegistry surfaced via the
GET /info endpoint. If omitted, an empty provider list is reported.
audit_sink:
Optional audit sink retained for observability hooks. Not directly
invoked here — the runtime already emits audit events.
tenant_id:
Default tenant propagated to the runtime when a request does not
provide its own tenant_id.
api_key:
Optional transport API key. When None (the default), the
CIEL_API_KEY environment variable is consulted. If a key is
configured (explicitly or via env), protected routes require a valid
key via Authorization: Bearer *** orX-API-Key; otherwise
the dependency is a no-op (open mode).
board_db_path:
Optional path to a SQLite database for the kanban board. When set (o
bien vía la variable de entornoCIEL_BOARD_DB), el board se monta
sobre SQLite para que/v1/board/list`` vea las tareas creadas por el
CLI y viceversa. Si no se configura ninguna ruta, el board queda en
memoria (comportamiento legacy, útil para smoke tests/offline).
Source code in src/ciel/gateway/base.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
Composed Ciel gateway application (control + MCP host + webhook).
:func:make_app builds a single FastAPI application that exposes the three
verified gateway surfaces on one port:
- control plane -> :func:
ciel.gateway.base.create_control_app(/health,/info,/v1/agent/run,/v1/tools/...,/v1/board/list) - MCP host -> :func:
ciel.gateway.mount_mcp_appmounted at/mcp(JSON-RPCPOST /mcp/) - webhook -> :func:
ciel.gateway.create_webhook_routerat/v1/messaging/webhook(POST+/health)
The runtime is built from environment configuration so ciel serve can
start with zero external dependencies: when no CIEL_PROVIDER_URL is set a
local echo provider is used, which keeps the gateway bootable for smoke tests
and offline deployments. Multi-tenancy is never relaxed — the control plane
still requires tenant_id on runtime/tool requests.
¶
__all__ = ['make_app', '_EchoProvider']
module-attribute
¶
__version__ = _pkg_version('ciel')
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
ChatChoice
dataclass
¶
ChatMessage
dataclass
Source code in src/ciel/runtime/tools.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
strcontent is returned verbatim.listcontent concatenates thetextof every part whose type is"text"but drops images/audio/video, so consumers (CLI, compression,AgentResponse.text) see only readable text.
Source code in src/ciel/runtime/tools.py
¶
ChatResponse
dataclass
¶
DefaultAgentRuntime
Concrete runtime wiring provider + tool execution with tracing.
Source code in src/ciel/runtime/__init__.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
¶
run_agent_loop(*, request: ChatRequest, tenant_id: Optional[str] = None, toolset: Optional[str] = None, limit: int = 32) -> AgentRuntimeResult
async
Run the agentic tool loop (multi-turn ReAct when tools are present).
The loop issues up to limit model completions. After each completion
that requests tool calls, the requested tools are dispatched and their
results are appended as messages; the model is then called again. The
loop stops when (a) the model returns no tool calls (finish_reason
stop), (b) limit turns are exhausted, or (c) there are no tools
registered in the request.
Backward compatibility: when limit <= 1 or request.tools is
empty, exactly one completion is produced and no tool messages are
appended (the historical single-step behaviour). Every tool result from
every turn is preserved in loop_results so the facade can surface
the full tool_results list.
Source code in src/ciel/runtime/__init__.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
¶
stream_tokens(*, request: ChatRequest, tenant_id: Optional[str] = None, toolset: Optional[str] = None) -> AsyncIterator[str]
async
Stream incremental assistant tokens from the provider.
Calls provider.stream (real SSE streaming) and re-emits the
partial content of each incremental :class:ChatResponse as it
arrives, so callers see the answer grow token by token.
Source code in src/ciel/runtime/__init__.py
¶
DefaultToolDispatcher
Dispatch tool requests to a configured ToolProvider.
Source code in src/ciel/runtime/__init__.py
¶
ToolProvider
dataclass
Concrete tool provider used by the runtime's tool dispatcher.
Source code in src/ciel/runtime/__init__.py
¶
ToolRegistry
Source code in src/ciel/runtime/tools.py
¶
WebhookAdapter
Bases: MessagingAdapter
Messaging adapter that ingests messages via an HTTP webhook endpoint.
Incoming webhook payloads are validated, converted to :class:Message
instances, and enqueued on an internal :class:asyncio.Queue. The
:meth:receive async generator drains the queue and yields messages
to consumers.
Typical usage alongside an HTTP framework::
adapter = WebhookAdapter()
@app.post("/webhook")
async def webhook(request):
payload = await request.json()
return await adapter.handle_webhook(payload)
async for message in adapter.receive():
...
Source code in src/ciel/gateway/adapter.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
¶
handle_webhook(payload: dict) -> dict
async
Validate a webhook payload, enqueue a :class:Message, and respond.
Expected payload shape::
{"id": "...", "channel": "...", "sender": "...",
"content": "...", "metadata": {...}}
channel and content are required. id is generated
via :func:uuid.uuid4 when absent. sender defaults to
None and metadata defaults to an empty dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict
|
Raw deserialized webhook body. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict
|
|
|
dict
|
or ``{"status": "rejected", "error": "missing required |
|
field |
dict
|
|
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Async generator yielding messages from the internal queue.
Blocks indefinitely on each :meth:asyncio.Queue.get call until
a message becomes available, then yields it. The loop runs
forever — the caller is responsible for cancellation/timeout.
Source code in src/ciel/gateway/adapter.py
¶
receive_internal() -> Message
async
Drain and return a single message from the queue.
Convenience method for consumers that prefer one-at-a-time polling over the async generator. Blocks until a message is available.
Returns:
| Type | Description |
|---|---|
Message
|
The next :class: |
Source code in src/ciel/gateway/adapter.py
¶
send(message: Message) -> None
async
Outbound send — not implemented for the webhook adapter.
The webhook adapter is inbound-only by design. Subclass or compose with an outbound adapter if delivery is needed.
Source code in src/ciel/gateway/adapter.py
¶
_EchoProvider
Offline provider used when no remote LLM endpoint is configured.
It returns an echo:<prompt> completion so the gateway can boot and
serve health/tool endpoints without network access. It is intentionally
deterministic and never makes outbound calls.
Source code in src/ciel/gateway/server.py
¶
_build_chat_provider() -> object
Source code in src/ciel/gateway/server.py
¶
create_control_app(*, runtime: DefaultAgentRuntime, registry: Optional[ProviderRegistry] = None, audit_sink: Any = None, tenant_id: Optional[str] = None, api_key: Optional[str] = None, board_db_path: Optional[str] = None) -> FastAPI
Build a FastAPI control plane for the Ciel agent runtime.
¶Parameters
runtime:
A :class:ciel.runtime.DefaultAgentRuntime used to run agent loops and
dispatch tools. Tool dispatch goes through runtime.dispatcher.
El endpoint POST /v1/agent/run/stream usa
runtime.stream_tokens para emitir Server-Sent Events (SSE) con los
fragmentos incrementales del assistant y cierra con data: [DONE].
registry:
Optional :class:ciel.providers.ProviderRegistry surfaced via the
GET /info endpoint. If omitted, an empty provider list is reported.
audit_sink:
Optional audit sink retained for observability hooks. Not directly
invoked here — the runtime already emits audit events.
tenant_id:
Default tenant propagated to the runtime when a request does not
provide its own tenant_id.
api_key:
Optional transport API key. When None (the default), the
CIEL_API_KEY environment variable is consulted. If a key is
configured (explicitly or via env), protected routes require a valid
key via Authorization: Bearer *** orX-API-Key; otherwise
the dependency is a no-op (open mode).
board_db_path:
Optional path to a SQLite database for the kanban board. When set (o
bien vía la variable de entornoCIEL_BOARD_DB), el board se monta
sobre SQLite para que/v1/board/list`` vea las tareas creadas por el
CLI y viceversa. Si no se configura ninguna ruta, el board queda en
memoria (comportamiento legacy, útil para smoke tests/offline).
Source code in src/ciel/gateway/base.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
¶
make_app(*, tenant_id: Optional[str] = None, include_mcp: bool = True, include_webhook: bool = True, state_backend: Optional[object] = None) -> FastAPI
Compose the full Ciel gateway into a single FastAPI app.
¶Parameters
tenant_id:
Default tenant propagated to the runtime when a request omits its own
tenant_id. Falls back to the CIEL_TENANT environment variable.
include_mcp:
Mount the MCP host app at /mcp.
include_webhook:
Mount the webhook messaging router at /v1/messaging/webhook.
state_backend:
Optional pre-built :class:StateBackend (Fase 14 / F15). When omitted,
one is built from CIEL_STATE_BACKEND / CIEL_STATE_DSN (default
SQLite). Injecting a shared backend lets tests and multi-replica
deployments reuse the same state source.
Source code in src/ciel/gateway/server.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
Generic messaging adapter for webhook-based HTTP message intake.
This module is pure Python + asyncio. It does not depend on FastAPI,
ciel.runtime, or ciel.providers — it is runtime-agnostic so any
HTTP server (FastAPI, Starlette, aiohttp, etc.) can plug into it.
¶
__all__ = ['Message', 'MessagingAdapter', 'WebhookAdapter']
module-attribute
¶
Message
dataclass
Immutable DTO representing a single inbound or outbound message.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique message identifier. Generated via |
channel |
str
|
Logical channel the message arrived on (required). |
sender |
Optional[str]
|
Optional sender identifier (may be |
content |
'str | list[dict[str, Any]]'
|
Message body / content (required). |
metadata |
Dict[str, Any]
|
Free-form metadata dict; defaults to an empty dict. |
Source code in src/ciel/gateway/adapter.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
Mirrors :meth:ciel.runtime.ChatMessage.text: returns str content
verbatim, or concatenates the text of every text part in a list.
Source code in src/ciel/gateway/adapter.py
¶
MessagingAdapter
Abstract base class for messaging adapters.
Subclasses implement :meth:receive (an async generator yielding
inbound :class:Message objects) and :meth:send (delivery of an
outbound :class:Message).
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Yield inbound messages as they arrive.
Implementations should be async generators that block until a
message is available and then yield it.
Source code in src/ciel/gateway/adapter.py
¶
WebhookAdapter
Bases: MessagingAdapter
Messaging adapter that ingests messages via an HTTP webhook endpoint.
Incoming webhook payloads are validated, converted to :class:Message
instances, and enqueued on an internal :class:asyncio.Queue. The
:meth:receive async generator drains the queue and yields messages
to consumers.
Typical usage alongside an HTTP framework::
adapter = WebhookAdapter()
@app.post("/webhook")
async def webhook(request):
payload = await request.json()
return await adapter.handle_webhook(payload)
async for message in adapter.receive():
...
Source code in src/ciel/gateway/adapter.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
¶
handle_webhook(payload: dict) -> dict
async
Validate a webhook payload, enqueue a :class:Message, and respond.
Expected payload shape::
{"id": "...", "channel": "...", "sender": "...",
"content": "...", "metadata": {...}}
channel and content are required. id is generated
via :func:uuid.uuid4 when absent. sender defaults to
None and metadata defaults to an empty dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict
|
Raw deserialized webhook body. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict
|
|
|
dict
|
or ``{"status": "rejected", "error": "missing required |
|
field |
dict
|
|
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Async generator yielding messages from the internal queue.
Blocks indefinitely on each :meth:asyncio.Queue.get call until
a message becomes available, then yields it. The loop runs
forever — the caller is responsible for cancellation/timeout.
Source code in src/ciel/gateway/adapter.py
¶
receive_internal() -> Message
async
Drain and return a single message from the queue.
Convenience method for consumers that prefer one-at-a-time polling over the async generator. Blocks until a message is available.
Returns:
| Type | Description |
|---|---|
Message
|
The next :class: |
Source code in src/ciel/gateway/adapter.py
¶
send(message: Message) -> None
async
Outbound send — not implemented for the webhook adapter.
The webhook adapter is inbound-only by design. Subclass or compose with an outbound adapter if delivery is needed.
Source code in src/ciel/gateway/adapter.py
Bidirectional Slack messaging adapter.
This module provides :class:SlackAdapter, a concrete
:class:~ciel.gateway.adapter.MessagingAdapter that can send messages to
Slack over the real Web API (chat.postMessage) and receive inbound
events via a simple polling loop.
The slack_sdk dependency is lenient: importing this module never
crashes the package when the SDK is absent. Only the act of constructing a
:class:SlackAdapter lazily imports slack_sdk (it is pulled in by the
messaging extra). If send is invoked without the SDK installed, a
clear :class:RuntimeError explaining how to install it is raised — but the
rest of the package keeps working.
Requires Python >= 3.10 (uses X | None syntax).
¶
_SLACK_SDK_AVAILABLE = True
module-attribute
¶
__all__ = ['SlackAdapter']
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
Message
dataclass
Immutable DTO representing a single inbound or outbound message.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique message identifier. Generated via |
channel |
str
|
Logical channel the message arrived on (required). |
sender |
Optional[str]
|
Optional sender identifier (may be |
content |
'str | list[dict[str, Any]]'
|
Message body / content (required). |
metadata |
Dict[str, Any]
|
Free-form metadata dict; defaults to an empty dict. |
Source code in src/ciel/gateway/adapter.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
Mirrors :meth:ciel.runtime.ChatMessage.text: returns str content
verbatim, or concatenates the text of every text part in a list.
Source code in src/ciel/gateway/adapter.py
¶
MessagingAdapter
Abstract base class for messaging adapters.
Subclasses implement :meth:receive (an async generator yielding
inbound :class:Message objects) and :meth:send (delivery of an
outbound :class:Message).
Source code in src/ciel/gateway/adapter.py
¶
receive() -> AsyncIterator[Message]
async
Yield inbound messages as they arrive.
Implementations should be async generators that block until a
message is available and then yield it.
Source code in src/ciel/gateway/adapter.py
¶
SlackAdapter
Bases: MessagingAdapter
Bidirectional Slack adapter built on slack_sdk.WebClient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
A Slack bot/user token ( |
required |
channel
|
Optional[str]
|
Optional default channel (e.g. |
None
|
bot_user_id
|
Optional[str]
|
Optional bot user id ( |
None
|
The Slack WebClient is created lazily so that merely importing this module
(or instantiating the adapter) is safe even when slack_sdk is not
installed. The client is built on first use of :meth:send / :meth:receive.
Source code in src/ciel/gateway/adapter_slack.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
¶
enqueue(message: Message) -> None
Push an inbound :class:Message onto the internal queue.
Used by the Slack webhook router to feed real message.changed
events into the adapter; consumers can drain them via
:meth:receive (which is overridden below to prefer the queue).
Source code in src/ciel/gateway/adapter_slack.py
¶
receive() -> AsyncIterator[Message]
async
Yield inbound Slack messages via a simple polling loop.
This is a lenient, best-effort implementation: it relies on Slack's
conversations.history to pull recent messages and yields any that
are not authored by the bot user. It polls every few seconds.
The primary, fully-tested contract of this adapter is :meth:send.
The receive loop is provided so the adapter is genuinely bidirectional,
but it is intentionally simple and skips advanced cursor pagination and
deduplication. Inbound event handling is better served by mounting the
Slack webhook router (see :func:create_slack_webhook_router) onto the
FastAPI app, which enqueues real message.changed events.
The loop runs forever; the caller is responsible for cancellation.
Source code in src/ciel/gateway/adapter_slack.py
¶
send(message: Message) -> None
async
Send message.content to Slack via chat.postMessage.
The destination channel is resolved from message.metadata['channel']
falling back to the adapter's default channel. A clear
:class:ValueError is raised when no channel can be resolved.
Network access is required for this call to succeed against the real
Slack API; tests pass a mocked WebClient to verify behaviour
without touching the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
Message
|
The :class: |
required |
Source code in src/ciel/gateway/adapter_slack.py
Webhook routers para adapters de canal (Fase 8 — Teams/Discord/Web UI).
Estos routers montan endpoints HTTP que convierten eventos entrantes de cada
canal en ciel.gateway.adapter.Message y los empujan (enqueue) al
adapter correspondiente, de forma que el runtime puede consumirlos vía
adapter.receive(). Siguen el mismo patrón que
ciel.gateway.create_slack_webhook_router.
OFFLINE-SAFE: los routers no requieren red para importarse ni para arrancar;
solo la entrega real de send (en los adapters) toca la red.
¶
logger = logging.getLogger(__name__)
module-attribute
¶
Message
dataclass
Immutable DTO representing a single inbound or outbound message.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique message identifier. Generated via |
channel |
str
|
Logical channel the message arrived on (required). |
sender |
Optional[str]
|
Optional sender identifier (may be |
content |
'str | list[dict[str, Any]]'
|
Message body / content (required). |
metadata |
Dict[str, Any]
|
Free-form metadata dict; defaults to an empty dict. |
Source code in src/ciel/gateway/adapter.py
¶
text() -> str
Extract plain text from content, tolerant to multimodal parts.
Mirrors :meth:ciel.runtime.ChatMessage.text: returns str content
verbatim, or concatenates the text of every text part in a list.
Source code in src/ciel/gateway/adapter.py
¶
create_discord_webhook_router(adapter, *, path: str = '/v1/messaging/discord') -> APIRouter
Router que ingiere eventos MESSAGE_CREATE de Discord al adapter.
Discord entrega un JSON con content, author/id y channel_id.
El router lo convierte en Message y lo enqueuea.
Source code in src/ciel/gateway/messaging.py
¶
create_teams_webhook_router(adapter, *, path: str = '/v1/messaging/teams', signing_secret: Optional[str] = None) -> APIRouter
Router que ingiere eventos de Microsoft Teams al adapter.
Teams entrega cargas JSON con text (y opcionalmente from /
channelData). El router los convierte en Message y los enqueuea.
Si se pasa signing_secret se puede validar la firma HMAC (fuera de
alcance en smoke tests); sin él, se acepta el payload tal cual.
Source code in src/ciel/gateway/messaging.py
¶
create_webui_router(adapter, *, path: str = '/v1/messaging/webui') -> APIRouter
Router de Web UI: POST para enviar mensajes al runtime, GET para sondear.
La UI hace POST {path} con un Message y lo enqueuea en el
WebUIAdapter; GET {path}/outbound sondea el siguiente mensaje
saliente del adapter (polling).
Source code in src/ciel/gateway/messaging.py
¶
__all__ = ['MCPClient', 'MCPHTTPTransport', 'MCPStdioTransport', 'MCPTransport', 'MCPServer', 'DefaultAgentRuntimeMCPHost', 'MCPHostToolProvider']
module-attribute
¶
DefaultAgentRuntimeMCPHost
Host DefaultAgentRuntime behavior through an MCP server boundary.
Source code in src/ciel/gateway/mcp/integration.py
¶
MCPClient
MCP protocol client over an explicit transport tenant-aware runtime.
Source code in src/ciel/gateway/mcp/client.py
¶
MCPHTTPTransport
Bases: MCPTransport
Source code in src/ciel/gateway/mcp/transports.py
¶
MCPHostToolProvider
Bases: ToolProvider
Expose an MCP server as a Ciel ToolProvider.
Source code in src/ciel/gateway/mcp/integration.py
¶
MCPServer
Minimal MCP-style JSON-RPC endpoint with tenant-aware handlers.
Source code in src/ciel/gateway/mcp/server.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
¶
MCPStdioTransport
Bases: MCPTransport
Source code in src/ciel/gateway/mcp/transports.py
¶
MCPTransport
Bases: ABC
Source code in src/ciel/gateway/mcp/transports.py
Optional transport-level API key authentication for gateway surfaces.
This module provides a FastAPI dependency that enforces an optional API key
on protected routes. The behaviour is driven by the CIEL_API_KEY
environment variable (or an explicit key passed by the caller):
- When a key is configured, every protected route requires a valid key
supplied via the
Authorization: Bearer <key>header or theX-API-Keyheader. A missing or mismatched key yields401 Unauthorized. - When no key is configured (the default for offline/dev/smoke-test deployments) the dependency is a no-op and the request proceeds. This keeps the gateway bootable with zero configuration.
The comparison uses :func:hmac.compare_digest to avoid timing side-channels.
¶
API_KEY_ENV = 'CIEL_API_KEY'
module-attribute
¶
__all__ = ['API_KEY_ENV', 'read_expected_key', 'make_auth_dependency', 'require_api_key', 'depends', 'AuthContext', 'make_oidc_dependency']
module-attribute
¶
require_api_key = make_auth_dependency(expected_key=None)
module-attribute
¶
AuthContext
dataclass
Contexto de autenticación resuelto por la dependency.
subject y role provienen de los claims OIDC cuando el token se validó
contra un IdP real; en modo api_key/open ambos son None.
Source code in src/ciel/gateway/auth.py
¶
_extract_provided_key(authorization: Optional[str], x_api_key: Optional[str]) -> Optional[str]
Pull the key out of the supported headers.
Source code in src/ciel/gateway/auth.py
¶
_reject() -> HTTPException
¶
depends(api_key: Optional[str] = None)
¶
make_auth_dependency(expected_key: Optional[str] = None)
Build a FastAPI dependency enforcing an optional API key.
The returned callable reads expected_key (explicit) or falls back to
the CIEL_API_KEY environment variable at request time. This lets the
same closure be reused across requests while still honoring runtime env
changes (e.g. monkeypatch.setenv in tests).
Source code in src/ciel/gateway/auth.py
¶
make_oidc_dependency(*, verifier=None, enabled: Optional[bool] = None, api_key: Optional[str] = None)
Build a FastAPI dependency that enforces OIDC when enabled.
Behaviour:
- When OIDC is disabled (
enabledis False, or None andCIEL_OIDC_ENABLEDis unset), it fully delegates to the api_key guard (retrocompatibilidad total, open-mode por defecto). - When OIDC is enabled, every protected route requires a valid Bearer
JWT. The token is verified (local key or JWKS) and its claims are mapped to
an RBAC role. Missing/invalid token =>
401.
The dependency returns an :class:AuthContext.
Source code in src/ciel/gateway/auth.py
¶
read_expected_key(*, explicit: Optional[str] = None) -> Optional[str]
Return the configured API key, preferring an explicit value.
¶Parameters
explicit: A key passed directly by the caller. If provided (even when empty string), it takes precedence over the environment.