Files
konstruct/.planning/phases/10-agent-capabilities/10-02-PLAN.md
Adolfo Delorenzo eae4b0324d
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Portal E2E (push) Has been cancelled
docs(10): create phase plan
2026-03-25 23:33:27 -06:00

263 lines
15 KiB
Markdown

---
phase: 10-agent-capabilities
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- packages/shared/shared/api/calendar_auth.py
- packages/orchestrator/orchestrator/tools/builtins/calendar_lookup.py
- packages/orchestrator/orchestrator/tools/registry.py
- tests/unit/test_calendar_lookup.py
- tests/unit/test_calendar_auth.py
autonomous: true
requirements:
- CAP-05
- CAP-06
user_setup:
- service: google-cloud
why: "Google Calendar OAuth for per-tenant calendar access"
env_vars:
- name: GOOGLE_CLIENT_ID
source: "Google Cloud Console -> APIs & Services -> Credentials -> OAuth 2.0 Client ID (Web application)"
- name: GOOGLE_CLIENT_SECRET
source: "Google Cloud Console -> APIs & Services -> Credentials -> OAuth 2.0 Client ID secret"
dashboard_config:
- task: "Create OAuth 2.0 Client ID (Web application type)"
location: "Google Cloud Console -> APIs & Services -> Credentials"
- task: "Add authorized redirect URI: {PORTAL_URL}/api/portal/calendar/callback"
location: "Google Cloud Console -> Credentials -> OAuth client -> Authorized redirect URIs"
- task: "Enable Google Calendar API"
location: "Google Cloud Console -> APIs & Services -> Library -> Google Calendar API"
must_haves:
truths:
- "Tenant admin can initiate Google Calendar OAuth from the portal and authorize calendar access"
- "Calendar OAuth callback exchanges code for tokens and stores them encrypted per tenant"
- "Calendar tool reads per-tenant OAuth tokens from channel_connections and calls Google Calendar API"
- "Calendar tool supports list events, check availability, and create event actions"
- "Token auto-refresh works — expired access tokens are refreshed via stored refresh_token and written back to DB"
- "Tool results are formatted as natural language (no raw JSON)"
artifacts:
- path: "packages/shared/shared/api/calendar_auth.py"
provides: "Google Calendar OAuth install + callback endpoints"
exports: ["calendar_auth_router"]
- path: "packages/orchestrator/orchestrator/tools/builtins/calendar_lookup.py"
provides: "Per-tenant OAuth calendar tool with list/create/check_availability"
exports: ["calendar_lookup"]
- path: "tests/unit/test_calendar_lookup.py"
provides: "Unit tests for calendar tool with mocked Google API"
key_links:
- from: "packages/shared/shared/api/calendar_auth.py"
to: "channel_connections table"
via: "Upsert ChannelConnection(channel_type='google_calendar') with encrypted token"
pattern: "google_calendar.*encrypt"
- from: "packages/orchestrator/orchestrator/tools/builtins/calendar_lookup.py"
to: "channel_connections table"
via: "Load encrypted token, decrypt, build Credentials, call Google API"
pattern: "Credentials.*refresh_token"
---
<objective>
Build Google Calendar OAuth per-tenant integration and replace the service-account stub with full CRUD calendar tool.
Purpose: Enables CAP-05 (calendar availability checking + event creation) by replacing the service account stub in calendar_lookup.py with per-tenant OAuth token lookup. Also addresses CAP-06 (natural language tool results) by ensuring calendar and all tool outputs are formatted as readable text.
Output: Google Calendar OAuth install/callback endpoints, fully functional calendar_lookup tool with list/create/check_availability actions, encrypted per-tenant token storage, token auto-refresh with write-back.
</objective>
<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/10-agent-capabilities/10-CONTEXT.md
@.planning/phases/10-agent-capabilities/10-RESEARCH.md
<interfaces>
<!-- Existing OAuth pattern from Slack to reuse -->
From packages/shared/shared/api/channels.py:
```python
channels_router = APIRouter(prefix="/api/portal/channels", tags=["channels"])
def _generate_oauth_state(tenant_id: uuid.UUID) -> str:
"""HMAC-SHA256 signed state with embedded tenant_id + nonce."""
...
def _verify_oauth_state(state: str) -> uuid.UUID:
"""Verify HMAC signature, return tenant_id. Raises HTTPException on failure."""
...
```
From packages/shared/shared/crypto.py:
```python
class KeyEncryptionService:
def encrypt(self, plaintext: str) -> str: ...
def decrypt(self, ciphertext: str) -> str: ...
```
From packages/shared/shared/models/tenant.py:
```python
class ChannelConnection(Base):
__tablename__ = "channel_connections"
id: Mapped[uuid.UUID]
tenant_id: Mapped[uuid.UUID]
channel_type: Mapped[ChannelTypeEnum] # TEXT + CHECK in DB
workspace_id: Mapped[str]
config: Mapped[dict] # JSON — stores encrypted token
created_at: Mapped[datetime]
```
From packages/shared/shared/config.py (after Plan 01):
```python
class Settings(BaseSettings):
google_client_id: str = ""
google_client_secret: str = ""
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Google Calendar OAuth endpoints and calendar tool replacement</name>
<files>
packages/shared/shared/api/calendar_auth.py,
packages/orchestrator/orchestrator/tools/builtins/calendar_lookup.py,
tests/unit/test_calendar_lookup.py,
tests/unit/test_calendar_auth.py
</files>
<behavior>
- OAuth install endpoint returns redirect URL with HMAC-signed state containing tenant_id
- OAuth callback verifies HMAC state, exchanges code for tokens, encrypts and stores in channel_connections as google_calendar type
- OAuth callback redirects to portal settings page with connected=true param
- calendar_lookup(date, action="list", tenant_id=...) loads encrypted token from DB, decrypts, calls Google Calendar API, returns formatted event list
- calendar_lookup(date, action="create", event_summary=..., event_start=..., event_end=..., tenant_id=...) creates a Google Calendar event and returns confirmation
- calendar_lookup(date, action="check_availability", tenant_id=...) returns free/busy summary
- calendar_lookup returns informative message when no Google Calendar is connected for tenant
- Token refresh: if access_token expired, google-auth auto-refreshes, updated token written back to DB
- All results are natural language strings, not raw JSON
</behavior>
<action>
1. **Calendar OAuth router** (`packages/shared/shared/api/calendar_auth.py`):
- calendar_auth_router = APIRouter(prefix="/api/portal/calendar", tags=["calendar"])
- Import and reuse _generate_oauth_state / _verify_oauth_state from channels.py (or extract to shared utility if private)
- If they are private (_prefix), create equivalent functions in this module using the same HMAC pattern
- GET /install?tenant_id={id}:
- Guard with require_tenant_admin
- Generate HMAC-signed state with tenant_id
- Build Google OAuth URL: https://accounts.google.com/o/oauth2/v2/auth with:
- client_id from settings
- redirect_uri = settings.portal_url + "/api/portal/calendar/callback"
- scope = "https://www.googleapis.com/auth/calendar" (full read+write per locked decision)
- state = hmac_state
- access_type = "offline" (to get refresh_token)
- prompt = "consent" (force consent to always get refresh_token)
- Return {"url": oauth_url}
- GET /callback?code={code}&state={state}:
- NO auth guard (external redirect from Google — no session cookie)
- Verify HMAC state to recover tenant_id
- Exchange code for tokens using google_auth_oauthlib or httpx POST to https://oauth2.googleapis.com/token
- Encrypt token JSON with KeyEncryptionService (Fernet)
- Upsert ChannelConnection(tenant_id=tenant_id, channel_type="google_calendar", workspace_id=str(tenant_id), config={"token": encrypted_token})
- Redirect to portal /settings?calendar=connected
- GET /{tenant_id}/status:
- Guard with require_tenant_member
- Check if ChannelConnection with channel_type='google_calendar' exists for tenant
- Return {"connected": true/false}
2. **Replace calendar_lookup.py** entirely:
- Remove all service account code
- New signature: async def calendar_lookup(date: str, action: str = "list", event_summary: str | None = None, event_start: str | None = None, event_end: str | None = None, calendar_id: str = "primary", tenant_id: str | None = None, **kwargs) -> str
- If no tenant_id: return "Calendar not available: missing tenant context."
- Load ChannelConnection(channel_type='google_calendar', tenant_id=tenant_uuid) from DB
- If not found: return "Google Calendar is not connected for this tenant. Ask an admin to connect it in Settings."
- Decrypt token JSON, build google.oauth2.credentials.Credentials
- Build Calendar service: build("calendar", "v3", credentials=creds, cache_discovery=False)
- Run API call in thread executor (same pattern as original — avoid blocking event loop)
- action="list": list events for date, format as "Calendar events for {date}:\n- {time}: {summary}\n..."
- action="check_availability": list events, format as "Busy slots on {date}:\n..." or "No events — the entire day is free."
- action="create": insert event with summary, start, end, return "Event created: {summary} from {start} to {end}"
- After API call: check if credentials.token changed (refresh occurred) — if so, encrypt and UPDATE channel_connections.config with new token
- All errors return human-readable messages, never raw exceptions
3. **Update tool registry** if needed — ensure calendar_lookup parameters schema includes action, event_summary, event_start, event_end fields so LLM knows about CRUD capabilities. Check packages/orchestrator/orchestrator/tools/registry.py for the calendar_lookup entry and update its parameters JSON schema.
4. **Tests** (write BEFORE implementation):
- test_calendar_lookup.py: mock Google Calendar API (googleapiclient.discovery.build), mock DB session to return encrypted token, test list/create/check_availability actions, test "not connected" path, test token refresh write-back
- test_calendar_auth.py: mock httpx for token exchange, test HMAC state generation/verification, test callback stores encrypted token
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_calendar_lookup.py tests/unit/test_calendar_auth.py -x -q</automated>
</verify>
<done>Google Calendar OAuth install/callback endpoints work. Calendar tool loads per-tenant tokens, supports list/create/check_availability, formats results as natural language. Token refresh writes back to DB. Service account stub completely removed. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Mount new API routers on gateway and update tool response formatting</name>
<files>
packages/gateway/gateway/main.py,
packages/orchestrator/orchestrator/tools/registry.py,
packages/orchestrator/orchestrator/agents/prompt.py
</files>
<action>
1. **Mount routers on gateway** (`packages/gateway/gateway/main.py`):
- Import kb_router from shared.api.kb and include it on the FastAPI app (same pattern as channels_router, billing_router, etc.)
- Import calendar_auth_router from shared.api.calendar_auth and include it on the app
- Verify both are accessible via curl or import
2. **Update tool registry** (`packages/orchestrator/orchestrator/tools/registry.py`):
- Update calendar_lookup tool definition's parameters schema to include:
- action: enum ["list", "check_availability", "create"] (required)
- event_summary: string (optional, for create)
- event_start: string (optional, ISO 8601 with timezone, for create)
- event_end: string (optional, ISO 8601 with timezone, for create)
- date: string (required, YYYY-MM-DD format)
- Update description to mention CRUD capabilities: "Look up, check availability, or create calendar events"
3. **Tool result formatting check** (CAP-06):
- Review agent runner prompt — the LLM already receives tool results as 'tool' role messages and formulates a response. Verify the system prompt does NOT contain instructions to dump raw JSON.
- If the system prompt builder (`packages/orchestrator/orchestrator/agents/prompt.py` or similar) has tool-related instructions, ensure it says: "When using tool results, incorporate the information naturally into your response. Never show raw data or JSON to the user."
- If no such instruction exists, add it as a tool usage instruction appended to the system prompt when tools are assigned.
4. **Verify CAP-04 (HTTP request tool)**: Confirm http_request.py needs no changes — it already works. Just verify it's in the tool registry and functions correctly.
5. **Verify CAP-07 (audit logging)**: Confirm executor.py already calls audit_logger.log_tool_call() on every invocation (it does — verified in code review). No changes needed.
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct && python -c "from shared.api.kb import kb_router; from shared.api.calendar_auth import calendar_auth_router; print('Routers import OK')" && python -c "from orchestrator.tools.registry import TOOL_REGISTRY; print(f'Registry has {len(TOOL_REGISTRY)} tools')"</automated>
</verify>
<done>KB and Calendar Auth routers mounted on gateway. Calendar tool registry updated with CRUD parameters. System prompt includes tool result formatting instruction. CAP-04 (HTTP) confirmed working. CAP-07 (audit) confirmed working. All routers importable.</done>
</task>
</tasks>
<verification>
- Calendar OAuth endpoints accessible: GET /api/portal/calendar/install, GET /api/portal/calendar/callback
- KB API endpoints accessible: POST/GET/DELETE /api/portal/kb/{tenant_id}/documents
- Calendar tool supports list, create, check_availability actions
- All unit tests pass: `pytest tests/unit/test_calendar_lookup.py tests/unit/test_calendar_auth.py -x -q`
- Tool registry has updated calendar_lookup schema with CRUD params
</verification>
<success_criteria>
- Google Calendar OAuth flow: install -> Google consent -> callback -> encrypted token stored in channel_connections
- Calendar tool reads per-tenant tokens and calls Google Calendar API for list, create, and availability check
- Token auto-refresh works with write-back to DB
- Natural language formatting on all tool results (no raw JSON)
- All new routers mounted on gateway
- CAP-04 and CAP-07 confirmed already working
- All unit tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/10-agent-capabilities/10-02-SUMMARY.md`
</output>