Files
roboco/roboco/api/app.py
T
46d89b58fe feat: company-in-a-box — goal-aware company layer (0.4.0) (#171)
* feat(goals): company charter singleton — data layer (Business Goals slice 1)

First slice of the company-in-a-box "Business Goals" phase: a single CEO-owned
charter row (north star + objectives + constraints + operating policy) that
will be injected into every agent's context_briefing so all work is goal-aware.

- CompanyGoalsTable: singleton table (all-zeros id), JSON objectives /
  constraints / operating_policy, updated_at / updated_by.
- migration 032: create + seed the singleton row (offline-renderable; column
  server-defaults fill an INSERT of just the id).
- CompanyGoalsService: get() (empty defaults when unset) + upsert() (singleton,
  partial update, caller commits).
- tests: empty defaults, roundtrip, singleton + partial-update preservation.

Next slices (mapped, not yet built): briefing injection (BriefingInputs +
build_context_briefing + EvidenceRepo), API route (GET any / PUT CEO-only),
panel /goals page, and base/Board/PM prompt mentions.

* feat(goals): inject the company charter into every agent briefing (slice 2)

The charter is now goal-aware context for every agent:
- BriefingInputs gains company_goals; build_context_briefing surfaces it.
- EvidenceRepo.company_goals(): single-row lookup returning a COMPACT charter
  (north star + objectives + constraints + operating policy; audit columns
  dropped, lists capped) or None when unset, so an empty charter never bloats
  the per-verb briefing.
- _briefing_for wires it into every context_briefing.

Tests: briefing surfaces company_goals (defaults None); repo returns None for an
absent/empty charter and the compact dict when set.

* feat(goals): company charter API — GET any agent, PUT CEO-only (slice 3)

- routes/company_goals.py: GET returns the charter (any authenticated agent —
  it drives every briefing); PUT is CEO-only (403 otherwise), partial update via
  model_dump(exclude_unset=True), explicit commit.
- schemas/company_goals.py: response + partial-update models.
- registered at /api/company-goals.
- tests: GET open to any role, CEO update persists + is readable, non-CEO 403.

* feat(goals): make the company charter actionable in agent prompts (slice 5)

Agents already receive company_goals in the briefing (slice 2); now tell them to
act on it:
- base.md: universal "Align with the company charter" section — favour work and
  trade-offs that advance the objectives, honour the constraints, flag conflicts;
  never a license to leave your role.
- board / main_pm / cell_pm: role-specific lines tying triage / cell-routing /
  subtask decomposition to the charter.

Prompts are composed at spawn from base.md + roles/*.md directly (compose_prompt),
so no _generated regeneration is needed.

* feat(goals): company charter panel page (slice 4)

CEO-facing editor for the charter at /company-goals:
- lib/api/company-goals.ts: get / update (PUT) client.
- company-goals-card.tsx: edit north star + constraints (one per line) +
  objectives / operating_policy (JSON, parsed + validated with toast errors);
  display derives from server state (no set-state-in-effect).
- (dashboard)/company-goals/page.tsx + a "Company Goals" sidebar nav link.

tsc --noEmit + eslint clean. Completes Phase 1 (Business Goals): data, briefing
injection, API, prompts, panel.

* fix(test): make test_app route assertions robust to FastAPI 0.137 _IncludedRouter

FastAPI 0.137 stopped flattening include_router into app.routes — each include is
now an _IncludedRouter (a BaseRoute with no .path), so `{r.path for r in
app.routes}` raised AttributeError and the two router-registration tests failed
(the bump arrived via the claude-agent-sdk update in uv.lock). Add
_registered_paths(): OpenAPI schema paths (the stable public contract) plus each
included router's prefix, which also covers the websocket /ws mount (never in the
schema). Drops the now-incorrect type: ignore[attr-defined].

* feat(research): pluggable web search/fetch for Board + PM agents

Add a provider-agnostic web-research capability so the Board and PMs can
ground decisions in current external evidence the knowledge base can't
answer.

- ResearchService selects a provider adapter from config: Tavily, Brave,
  and Exa adapters plus a NullProvider that degrades gracefully when no
  key is set. Result count and fetched-content size are clamped to caps.
- /api/research/search and /api/research/fetch: role-gated to Board + PMs
  (and the CEO), with a per-agent/day Redis quota that fails open.
- roboco-search MCP server (web_search / web_fetch) calls those routes;
  the provider key stays server-side and agent containers never egress.
  Mounted per role by the orchestrator, behind a master switch.
- Charter-aware prompt guidance for Board, Main PM, and Cell PM.

Additive: with no key configured it is a no-op and the existing delivery
lifecycle is unchanged.

* feat(pitch): Board pitch -> CEO approve -> auto-provision repos

Add an additive origination path so a product can be proposed, approved,
and stood up without manual repo/Project setup.

- Pitch entity + migration (pitches table); PitchService create/list/
  reject/approve.
- GitHubProvisioningService: the one place that creates repos (POST
  /orgs/{org}/repos). Server-side token/org; when unconfigured the whole
  approve path is inert and nothing is created.
- On approval: provision one repo per target cell, register a Project per
  repo, create a Product when multi-cell, and seed one Main-PM delivery
  task — all reusing the existing Product / coordination-task machinery.
- /api/pitches: Board authors (PO/HoM), CEO approves/rejects, Board+PM+CEO
  view. Errors mapped via a single translator.

Additive: the delivery lifecycle is untouched; with no provisioning token
the capability is a no-op. Agent-facing pitch tool + panel are follow-ups.

* feat(strategy): dormant autonomous strategy engine (engine 2)

Add a second, optional engine that watches the company against its
standing goals and surfaces what needs the CEO — without touching the
delivery lifecycle (engine 1).

- StrategyEngine.assess() reports observations: the company is idle while
  goals stand, and tasks stranded in 'blocked' past a threshold.
- run_cycle() notifies the CEO (notify-only; it never spends, builds, or
  auto-approves — originating work stays a CEO decision).
- Orchestrator runs it on its own interval, started/stopped with the other
  background loops; the loop returns immediately unless enabled.

DORMANT by default (strategy_engine_enabled=False): the loop never runs and
a standard deployment is unchanged. Auto-origination is a further opt-in.

* docs(changelog): record Business Goals, Web Research, Pitch->Provision, and the dormant strategy engine under Unreleased

* feat(secretary): wire the Secretary role end-to-end (foundation)

Add SECRETARY as a distinct role — the CEO's conversational chief-of-staff,
governed separately from the Prompter (which stays read-only/human-only).
This is the role foundation only; authority, the live agent, and the panel
land in following commits.

- foundation/identity: Role.SECRETARY (board level), seeded secretary-1 agent,
  role-level mapping.
- journaling read tier (ALL — it advises the CEO), role_config entry,
  per-role model (opus), prompt-layer mapping + roles/secretary.md.
- i_am_idle gains SECRETARY so the role has a verb surface.
- migration 034: add 'secretary' to the agentrole enum (mirrors 025).
- Role-registry tests updated for the new role.

Inert by itself (nothing spawns it yet); additive — existing roles unchanged.

* feat(secretary): directives + gate-list authority (backend)

The Secretary acts only under CEO command. Low-risk directives (relay a
dictated message) execute immediately; high-impact ones — charter edits,
task start/cancel/override, pitch approval, announcements — are recorded
pending and run only after the CEO confirms (the gate list).

- secretary_directives table (migration 035) as the command audit + queue.
- SecretaryService: read company state; submit (direct->run, gated->queue +
  notify CEO); confirm/reject; execution runs with the CEO as actor through
  the existing services (the Secretary never holds CEO authority itself).
- /api/secretary: submit + state/task reads (Secretary or CEO); list/confirm/
  reject (CEO only). Writes commit explicitly.

* feat(secretary): live conversational agent (container + bridge)

Stand up the Secretary as a persistent Claude-SDK container the CEO chats
with, mirroring the Intake agent and reusing its driver/session machinery.

- secretary_driver: build_secretary_options exposes read_company_state /
  read_task / submit_directive as SDK tools that call /api/secretary/* with
  the agent's HMAC token; backend-call logic is module-level + tested.
- secretary_main: container entrypoint (receiver + relay) reusing IntakeDriver.
- orchestrator: start/spawn/reap secretary session + run-cmd builder; no
  workspace clone (reads state via API), mints a role=secretary token.
- secretary_live routes: panel <-> container bridge over the live registry.
- agent-secretary image (Dockerfile + compose build service).

Inert until a session is started; additive — intake and all agents unchanged.

* feat(secretary): panel chat + directive confirmation queue

The CEO's Secretary surface: a live chat (SSE) to talk to the Secretary, and
a 'Needs your confirmation' queue listing gated directives the Secretary
proposed — each with Confirm / Reject. Adds the sidebar nav entry.

- lib/api/secretary.ts: live (start/stream/status/send/stop) + directive
  (list/confirm/reject) + state clients (all as the CEO).
- hooks/use-secretary.ts: drives one chat, accumulating SSE token deltas.
- secretary page: chat pane + pending-directive cards.

Completes the Secretary end-to-end (role + authority + live agent + panel).

* feat(pitch): agent-facing pitch tool + pitches panel

Complete the pitch path: the Board can now author pitches through the gateway,
and the CEO reviews/approves them in the panel.

- content_actions.pitch (Board-only) -> PitchService.create, returning an
  Envelope; wired as a do-tool (do_server + /api/v1/do/pitch + schema) and
  added to the Board's do-tools.
- Panel /pitches page: lists pitches with CEO Approve & provision / Reject;
  sidebar nav entry.

Pitch (Phase 4) is now end-to-end: author -> CEO approve -> auto-provision.

* feat(cockpit): read-only 'is the business winning?' summary

A pure aggregation for the CEO over existing data — no new state, no writes.

- CockpitService.summary(): charter north-star/objectives, delivery counts
  (in-flight/blocked/awaiting-CEO), 30-day spend vs the charter's budget cap,
  pending pitches, and the strategy engine's signals (what needs you). Stamped
  basis='proxy' — performance is a proxy until real launches.
- GET /api/cockpit/summary (CEO / Board / Main PM / Secretary).
- Panel /cockpit page + sidebar nav.

Reuses goals + usage + StrategyEngine.assess(); reads only.

* docs(changelog): add the Secretary and Cockpit to Unreleased

* fix(test): isolate the company-goals empty-defaults test from committed state

The shared test DB persists committed writes across tests; a route test
commits a charter, so the unit test's 'unset' assertion must establish its
own clean precondition rather than assume global emptiness.

* fix(gateway): lower evidence_repo complexity to rank A (xenon gate)

company_goals()'s 4-way `or` emptiness check tipped the module average to
rank B; `any(...)` is equivalent and keeps the module under the gate's A bar.

* chore(compose): mirror agent-secretary-image build into docker-compose.yaml

Both compose files are byte-identical and tracked; .yaml carries the same
agent-secretary-image build service already present in docker-compose.yml.

* chore(lifecycle): regenerate artifacts for secretary i_am_idle

The secretary role gained i_am_idle in the lifecycle spec; regenerate the
generated prompt/doc/json artifacts so foundation-check stays green.

* docs(changelog): cut the company-in-a-box phases to 0.4.0

Label the six additive phases (business goals, web research, pitch-provision,
strategy engine, secretary, cockpit) as 0.4.0; tag v0.4.0 is held until the
branch merges to master so it points at the release commit.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-15 20:47:41 +02:00

439 lines
14 KiB
Python

"""
FastAPI Application Factory
Creates and configures the FastAPI application with all routes,
middleware, and event handlers.
"""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from roboco.api.deps import _auth_required
from roboco.api.middleware import setup_middleware
from roboco.api.routes.a2a import router as a2a_router
from roboco.api.routes.a2a import wellknown_router as a2a_wellknown_router
from roboco.api.routes.agents import router as agents_router
from roboco.api.routes.channels import router as channels_router
from roboco.api.routes.cockpit import router as cockpit_router
from roboco.api.routes.company_goals import router as company_goals_router
from roboco.api.routes.dashboard import router as dashboard_router
from roboco.api.routes.docs import router as docs_router
from roboco.api.routes.git import router as git_router
from roboco.api.routes.groups import router as groups_router
from roboco.api.routes.health import router as health_router
from roboco.api.routes.journals import router as journals_router
from roboco.api.routes.kanban import router as kanban_router
from roboco.api.routes.messages import router as messages_router
from roboco.api.routes.notifications import router as notifications_router
from roboco.api.routes.optimal import router as optimal_router
from roboco.api.routes.orchestrator import router as orchestrator_router
from roboco.api.routes.pitch import router as pitch_router
from roboco.api.routes.product import router as product_router
from roboco.api.routes.project import router as project_router
from roboco.api.routes.prompter_live import router as prompter_live_router
from roboco.api.routes.provider import router as provider_router
from roboco.api.routes.research import router as research_router
from roboco.api.routes.secretary import router as secretary_router
from roboco.api.routes.secretary_live import router as secretary_live_router
from roboco.api.routes.sessions import router as sessions_router
from roboco.api.routes.settings import router as settings_router
from roboco.api.routes.stream import router as stream_router
from roboco.api.routes.system import router as system_router
from roboco.api.routes.tasks import router as tasks_router
from roboco.api.routes.usage import router as usage_router
from roboco.api.routes.v1 import do as do_module
from roboco.api.routes.v1 import flow_auditor as flow_auditor_module
from roboco.api.routes.v1 import flow_board as flow_board_module
from roboco.api.routes.v1 import flow_cell_pm as flow_cell_pm_module
from roboco.api.routes.v1 import flow_dev as flow_dev_module
from roboco.api.routes.v1 import flow_doc as flow_doc_module
from roboco.api.routes.v1 import flow_main_pm as flow_main_pm_module
from roboco.api.routes.v1 import flow_qa as flow_qa_module
from roboco.api.routes.work_session import router as work_session_router
from roboco.api.websocket import router as ws_router
from roboco.config import settings
from roboco.db.base import close_db, init_db
from roboco.logging import get_logger, setup_logging
from roboco.services.extraction import ExtractionPipeline, ExtractionService
from roboco.services.learning import get_learning_service
from roboco.services.optimal import close_optimal_service, get_optimal_service
from roboco.services.transcription import TranscriptionService
# Setup logging before anything else
setup_logging()
logger = get_logger(__name__)
class _AppServices:
"""Holder for application service instances (initialized in lifespan)."""
transcription: TranscriptionService | None = None
extraction: ExtractionPipeline | None = None
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
"""
Application lifespan manager.
Handles startup and shutdown events.
"""
logger.info(
"Starting RoboCo API",
version=settings.app_version,
environment=settings.environment,
)
if not _auth_required():
logger.warning(
"Agent auth is in HEADER-TRUST mode (ROBOCO_AGENT_AUTH_REQUIRED is "
"not set to true): the API accepts X-Agent-Id / X-Agent-Role without "
"verifying a signed token, so any client that can reach it may act as "
"any role, including 'ceo'. Acceptable only on a trusted private "
"network. Set ROBOCO_AGENT_AUTH_REQUIRED=true and do NOT expose this "
"API to untrusted networks.",
)
# Startup: apply Alembic migrations (+ create_all fallback for fresh DBs).
# init_db runs on every environment now — migrations are idempotent via
# alembic_version, and this is the only way new schema (e.g. enum value
# additions like NotificationType.APPROVAL) reaches the running DB.
await init_db()
logger.info("Database initialized")
# Initialize Phase 2 services
_AppServices.transcription = TranscriptionService()
await _AppServices.transcription.start()
extraction_service = ExtractionService()
_AppServices.extraction = ExtractionPipeline(extraction_service)
# Store in app state for access in routes
app.state.transcription = _AppServices.transcription
app.state.extraction = _AppServices.extraction
# Initialize OptimalService (RAG) - BLOCKS until fully ready
# This ensures /health only returns 200 when RAG is operational
# Typical initialization time: 30-90 seconds (embedding + indexing)
try:
logger.info("Initializing OptimalService (RAG)...")
optimal_service = await get_optimal_service()
app.state.optimal = optimal_service
logger.info("OptimalService (RAG) initialized successfully")
except Exception as e:
logger.warning(
"OptimalService (RAG) initialization failed - RAG features disabled",
error=str(e),
)
app.state.optimal = None
# Wire the learning-propagation singleton to OptimalService. Without this,
# record_learning() raises "not initialized" and every task completion logs
# "Failed to extract learnings". Skipped when RAG is disabled (no optimal).
if app.state.optimal is not None:
try:
learning_service = await get_learning_service()
await learning_service.initialize(app.state.optimal)
logger.info("LearningPropagationService initialized")
except Exception as e:
logger.warning("LearningPropagationService init failed", error=str(e))
logger.info("All services initialized, API ready")
yield
# Shutdown
logger.info("Shutting down RoboCo API")
if _AppServices.transcription:
await _AppServices.transcription.stop()
# Close Phase 3 services
await close_optimal_service()
await close_db()
logger.info("Shutdown complete")
def create_app() -> FastAPI:
"""
Create and configure the FastAPI application.
Returns:
Configured FastAPI application instance.
"""
app = FastAPI(
title="RoboCo API",
description="AI Agents Company - Messaging and Task Management API",
version=settings.app_version,
docs_url="/docs", # if settings.debug else None,
redoc_url="/redoc", # if settings.debug else None,
lifespan=lifespan,
)
# ==========================================================================
# Middleware
# ==========================================================================
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=settings.cors_allow_credentials,
allow_methods=["*"],
allow_headers=["*"],
)
# Setup custom middleware (error handling, logging, correlation IDs)
setup_middleware(app)
# ==========================================================================
# Routes
# ==========================================================================
# Health check
app.include_router(health_router, tags=["Health"])
# A2A Protocol: Well-known endpoints at root level
# (/.well-known/agent.json, /agents/{id}/.well-known/agent.json)
app.include_router(a2a_wellknown_router, tags=["A2A Protocol"])
# API v1
api_prefix = "/api"
app.include_router(
agents_router,
prefix=f"{api_prefix}/agents",
tags=["Agents"],
)
app.include_router(
channels_router,
prefix=f"{api_prefix}/channels",
tags=["Channels"],
)
app.include_router(
groups_router,
prefix=f"{api_prefix}/groups",
tags=["Groups"],
)
app.include_router(
sessions_router,
prefix=f"{api_prefix}/sessions",
tags=["Sessions"],
)
app.include_router(
settings_router,
prefix=f"{api_prefix}/settings",
tags=["Settings"],
)
app.include_router(
company_goals_router,
prefix=f"{api_prefix}/company-goals",
tags=["Company"],
)
app.include_router(
messages_router,
prefix=f"{api_prefix}/messages",
tags=["Messages"],
)
app.include_router(
notifications_router,
prefix=f"{api_prefix}/notifications",
tags=["Notifications"],
)
# Phase 2: Stream processing and permissions
app.include_router(
stream_router,
prefix=f"{api_prefix}/stream",
tags=["Stream Processing"],
)
# Phase 3: Intelligence - Optimal API and Journal API
app.include_router(
optimal_router,
prefix=f"{api_prefix}/optimal",
tags=["Optimal API"],
)
app.include_router(
journals_router,
prefix=f"{api_prefix}/journals",
tags=["Journals"],
)
# Web research — pluggable external search/fetch for Board + PM agents.
app.include_router(
research_router,
prefix=f"{api_prefix}/research",
tags=["Research"],
)
# Cockpit — the CEO's read-only "is the business winning?" summary.
app.include_router(
cockpit_router,
prefix=f"{api_prefix}/cockpit",
tags=["Cockpit"],
)
# Pitches — Board proposals + CEO approve -> auto-provision origination path.
app.include_router(
pitch_router,
prefix=f"{api_prefix}/pitches",
tags=["Pitches"],
)
# Secretary — the CEO's chief-of-staff: company-state reads + gated directives.
app.include_router(
secretary_router,
prefix=f"{api_prefix}/secretary",
tags=["Secretary"],
)
# Secretary live chat — panel <-> Secretary container bridge.
app.include_router(
secretary_live_router,
prefix=f"{api_prefix}/secretary",
tags=["Secretary"],
)
# Phase 5: Management - Tasks, Kanban, Dashboards
app.include_router(
tasks_router,
prefix=f"{api_prefix}/tasks",
tags=["Tasks"],
)
app.include_router(
kanban_router,
prefix=f"{api_prefix}/kanban",
tags=["Kanban"],
)
app.include_router(
dashboard_router,
prefix=f"{api_prefix}/dashboard",
tags=["Dashboard"],
)
# Phase 7: Agent Runtime
app.include_router(
orchestrator_router,
prefix=f"{api_prefix}/orchestrator",
tags=["Orchestrator"],
)
# A2A Protocol: API endpoints
app.include_router(
a2a_router,
prefix=f"{api_prefix}/a2a",
tags=["A2A Protocol"],
)
# Git Integration
app.include_router(
git_router,
prefix=f"{api_prefix}/git",
tags=["Git Operations"],
)
# Project Management
app.include_router(
project_router,
prefix=f"{api_prefix}/projects",
tags=["Projects"],
)
# Product Management
app.include_router(
product_router,
prefix=f"{api_prefix}/products",
tags=["Products"],
)
# AI Providers (model routing + Ollama-cloud fallback)
app.include_router(
provider_router,
prefix=f"{api_prefix}/providers",
tags=["Providers"],
)
# Prompter live chat — panel <-> spawned intake agent (SSE + relay)
app.include_router(
prompter_live_router,
prefix=f"{api_prefix}/prompter",
tags=["Prompter"],
)
# Work Sessions
app.include_router(
work_session_router,
prefix=f"{api_prefix}/work-sessions",
tags=["Work Sessions"],
)
# Documentation
app.include_router(
docs_router,
prefix=f"{api_prefix}/docs",
tags=["Documentation"],
)
# Token Usage Analytics
app.include_router(
usage_router,
prefix=f"{api_prefix}/usage",
tags=["Usage Analytics"],
)
# System monitoring (rate-limits, etc.)
app.include_router(
system_router,
prefix=f"{api_prefix}/system",
tags=["System"],
)
# API v1 — intent-verb flow endpoints
app.include_router(flow_dev_module.router)
# API v1 — intent-verb QA flow endpoints
app.include_router(flow_qa_module.router)
# API v1 — intent-verb documenter flow endpoints
app.include_router(flow_doc_module.router)
# API v1 — intent-verb cell PM flow endpoints
app.include_router(flow_cell_pm_module.router)
# API v1 — intent-verb main PM flow endpoints
app.include_router(flow_main_pm_module.router)
# API v1 — intent-verb board flow endpoints
app.include_router(flow_board_module.router)
# API v1 — intent-verb auditor flow endpoints
app.include_router(flow_auditor_module.router)
# API v1 — content-tool endpoints
app.include_router(do_module.router)
# ==========================================================================
# WebSocket
# ==========================================================================
app.include_router(ws_router, prefix="/ws", tags=["WebSocket"])
return app
# Create the default application instance
app = create_app()