docs(10): create phase plan
This commit is contained in:
262
.planning/phases/10-agent-capabilities/10-02-PLAN.md
Normal file
262
.planning/phases/10-agent-capabilities/10-02-PLAN.md
Normal file
@@ -0,0 +1,262 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user