--- 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" --- 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. @/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md @/home/adelorenzo/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/10-agent-capabilities/10-CONTEXT.md @.planning/phases/10-agent-capabilities/10-RESEARCH.md 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 = "" ``` Task 1: Google Calendar OAuth endpoints and calendar tool replacement 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 - 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 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 cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_calendar_lookup.py tests/unit/test_calendar_auth.py -x -q 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. Task 2: Mount new API routers on gateway and update tool response formatting packages/gateway/gateway/main.py, packages/orchestrator/orchestrator/tools/registry.py, packages/orchestrator/orchestrator/agents/prompt.py 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. 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')" 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. - 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 - 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 After completion, create `.planning/phases/10-agent-capabilities/10-02-SUMMARY.md`