---
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>
