Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6667d13
feat(autonomous): simply team access and separate admin control to a tab
suwhang-cisco Aug 18, 2026
162a66f
Merge branch 'main' into prebuild/feat/autonomous-team-access
suwhang-cisco Aug 18, 2026
d8327f1
chore: bump version to 0.5.69-dev.1
github-actions[bot] Aug 18, 2026
bfee5da
fix(ui): autonomous page scroll issue
suwhang-cisco Aug 19, 2026
b2071fe
fix(autonomous): secret should be secretly handled
suwhang-cisco Aug 19, 2026
cba96a2
fix(autonomous): better admin tab and remove task oversight
suwhang-cisco Aug 19, 2026
6308faa
feat(ui): autonomous and schedule chats in separate sections to history
suwhang-cisco Aug 19, 2026
a2faa23
feat(autonomous): add min run interval (configurable) and wehook shou…
suwhang-cisco Aug 19, 2026
c599974
fix(autonomous): add webhook limits and address security concerns
suwhang-cisco Aug 19, 2026
4a80eca
Merge branch 'main' into prebuild/feat/autonomous-team-access
suwhang-cisco Aug 19, 2026
ba685a3
chore: bump version to 0.5.69-dev.2
github-actions[bot] Aug 19, 2026
7eba33c
feat(docs): up to date autonomous agent docs
suwhang-cisco Aug 19, 2026
62111f2
fix(autonomous): webhook secret ffs
suwhang-cisco Aug 20, 2026
c705597
Merge remote-tracking branch 'origin/main' into prebuild/feat/autonom…
suwhang-cisco Aug 24, 2026
89929af
chore: merge latest main
suwhang-cisco Aug 26, 2026
c0b8fb0
chore: bump version to 1.0.0-dev.7
github-actions[bot] Aug 26, 2026
532214a
feat(autonomous): webhook to be in a single chat with each run having…
suwhang-cisco Aug 26, 2026
fc09618
fix(ui): test
suwhang-cisco Aug 26, 2026
301e61a
feat(autonomous): remove redundent max retires and timeout
suwhang-cisco Aug 26, 2026
fe42ef8
Merge branch 'main' into prebuild/feat/autonomous-team-access
suwhang-cisco Aug 26, 2026
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
265 changes: 160 additions & 105 deletions ai_platform_engineering/autonomous_agents/README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions ai_platform_engineering/autonomous_agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ dependencies = [
"fastapi==0.135.3",
"uvicorn[standard]==0.34.2",
"apscheduler==3.11.0",
"boto3==1.43.16",
"cnoe-agent-utils==0.4.1",
"cryptography==50.0.0",
"httpx==0.28.1",
"motor==3.7.1",
"pydantic==2.13.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class Settings(BaseSettings):
# dynamic agent's own model config governs execution).
llm_provider: str = "anthropic-claude"

# Cron and interval tasks may not fire more frequently than this. Webhook
# tasks are event-driven and deliberately exempt.
minimum_schedule_interval_seconds: int = Field(default=1800, ge=1)

# Dynamic-agents runtime — the single execution backend for autonomous
# tasks. Every task targets a dynamic_agent_id and runs through this
# service (its tools / system prompt / model / middleware).
Expand Down Expand Up @@ -85,6 +89,14 @@ def _reject_nonfinite(cls, v: float) -> float:
# Per-task secrets always win when both are configured.
webhook_secret: str | None = None

# Per-task webhook secrets use the same envelope-encryption scheme as the
# UI credential store (including UI-managed Webex OAuth secrets): a fresh
# AES-256-GCM data key per write, wrapped by this AWS KMS CMK. When the CMK
# is unset, tasks without per-task secrets still work, but persisting or
# reading a per-task secret fails closed instead of writing plaintext.
credential_kms_cmk_id: str | None = None
credential_kms_region: str | None = None

# IMP-07 — webhook replay protection.
#
# When > 0, signed webhooks must additionally carry an
Expand All @@ -100,6 +112,20 @@ def _reject_nonfinite(cls, v: float) -> float:
# timestamp header. See README.md for the signing contract.
webhook_replay_window_seconds: int = Field(default=0, ge=0)

# Application-level webhook overload protection. Each webhook task is a
# FIFO with exactly one active run. Queue item/byte ceilings bound memory;
# separate owner/global execution limits allow safe parallelism across
# different webhooks. Edge/WAF limiting remains the first DDoS boundary.
webhook_max_payload_bytes: int = Field(default=1_048_576, ge=1)
webhook_max_pending_per_task: int = Field(default=100, ge=1)
webhook_max_pending_per_owner: int = Field(default=500, ge=1)
webhook_max_pending_global: int = Field(default=5_000, ge=1)
webhook_max_pending_payload_bytes_global: int = Field(
default=67_108_864, ge=1
)
webhook_max_concurrent_per_owner: int = Field(default=20, ge=1)
webhook_max_concurrent_global: int = Field(default=100, ge=1)

# Path to the YAML file describing webhook provider adapters
# (signature header, scheme, algorithm, payload template, etc.).
# ``None`` (the default) means use the bundled
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Application-level exception handlers."""

import logging

from fastapi import Request
from fastapi.responses import JSONResponse

from autonomous_agents.services.secret_encryption import WebhookSecretEncryptionError

logger = logging.getLogger("autonomous_agents")


async def webhook_secret_encryption_error_handler(
request: Request,
exc: WebhookSecretEncryptionError,
) -> JSONResponse:
"""Return a stable JSON error without exposing encryption internals."""
logger.error(
"Webhook secret encryption failed for %s %s: %s",
request.method,
request.url.path,
exc,
exc_info=(type(exc), exc, exc.__traceback__),
)
return JSONResponse(
status_code=503,
content={
"detail": (
"Webhook secret encryption is unavailable. Check "
"CREDENTIAL_KMS_CMK_ID and the Autonomous Agents service's "
"AWS KMS permissions."
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from fastapi.middleware.cors import CORSMiddleware

from autonomous_agents.config import get_settings
from autonomous_agents.error_handlers import webhook_secret_encryption_error_handler
from autonomous_agents.routes import health, tasks, webex, webhooks
from autonomous_agents.routes.webex import set_bot_person_id, set_webex_client
from autonomous_agents.services.chat_history import NoopChatHistoryPublisher
Expand All @@ -28,6 +29,7 @@
get_scheduler,
register_scheduler_tasks,
)
from autonomous_agents.services.secret_encryption import WebhookSecretEncryptionError
from autonomous_agents.services.task_lifecycle import set_task_store
from autonomous_agents.services.task_runner import (
set_chat_history_publisher,
Expand Down Expand Up @@ -275,6 +277,11 @@ def create_app() -> FastAPI:
lifespan=lifespan,
)

app.add_exception_handler(
WebhookSecretEncryptionError,
webhook_secret_encryption_error_handler,
)

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from datetime import datetime, timezone
from enum import Enum
from math import isfinite
from typing import Any, Literal, Optional

from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import BaseModel, Field, model_validator


class TriggerType(str, Enum):
Expand Down Expand Up @@ -53,7 +52,14 @@ def require_positive_interval(self) -> "IntervalTrigger":
class WebhookTrigger(BaseModel):
"""Trigger for webhook-scheduled tasks"""
type: Literal[TriggerType.WEBHOOK] = TriggerType.WEBHOOK
secret: str | None = Field(None, description="Optional HMAC secret for payload validation")
secret: str | None = Field(
None,
description=(
"HMAC secret for payload validation. The create route always generates "
"this value; None is accepted only so redacted update payloads can "
"preserve the already-stored secret."
),
)
provider: str = Field(
default="generic_hmac",
description=(
Expand Down Expand Up @@ -194,14 +200,6 @@ class TaskDefinition(BaseModel):
)
enabled: bool = True
metadata: dict[str, Any] = Field(default_factory=dict)
timeout_seconds: float | None = Field(
default=None,
gt=0,
description=(
"Override the dynamic-agents call timeout for this task "
"(seconds, > 0). Defaults to DYNAMIC_AGENTS_TIMEOUT_SECONDS."
),
)
owner_id: str | None = Field(
default=None,
description=(
Expand All @@ -225,16 +223,6 @@ class TaskDefinition(BaseModel):
),
)

@field_validator("timeout_seconds")
@classmethod
def _timeout_must_be_finite(cls, v: float | None) -> float | None:
"""Reject non-finite values that would break httpx timeouts at runtime."""
if v is None:
return v
if not isfinite(v):
raise ValueError("timeout_seconds must be a finite number")
return v

@model_validator(mode="after")
def _drop_deprecated_agent_hint(self) -> "TaskDefinition":
"""Clear the deprecated ``agent`` hint when a dynamic agent is set.
Expand Down Expand Up @@ -303,6 +291,17 @@ class FollowUpContext(BaseModel):
)


class TaskRunFollowUpCreate(BaseModel):
"""Authenticated UI request to continue one webhook run."""

user_text: str = Field(
...,
min_length=1,
max_length=10_000,
description="Operator message to send in the selected run's context.",
)



class TaskCreate(TaskDefinition):
"""Request body for ``POST /tasks``.
Expand Down Expand Up @@ -333,6 +332,18 @@ class TaskRun(BaseModel):
# threaded timeline instead of unrelated rows. ``None`` for the
# original webhook fire and for cron / interval / manual runs.
parent_run_id: str | None = None
# Root delivery for a webhook conversation branch. Initial deliveries set
# this to their own run_id; follow-ups inherit it from the selected parent.
# None for cron/interval runs and legacy webhook records.
root_run_id: str | None = None
# Dynamic Agents conversation/checkpointer id used for execution. This is
# intentionally distinct from ``conversation_id`` below: the latter is a
# UI chat-history link, while this field protects execution isolation.
execution_context_id: str | None = None
# Exact operator message for a follow-up run. Stored separately from the
# augmented request_prompt so timeline clients can render a clean user turn.
follow_up_text: str | None = None
follow_up_transport: str | None = None
# Prompt materialised for this specific run. For normal scheduled
# runs this is the task prompt; for inbound follow-ups it includes
# the operator reply appended by task_runner. The UI's autonomous
Expand Down
Loading
Loading