Handbook
Playwright-assisted extension architecture
Step: 01_architecture_docs (PDCA Do phase) Repo: forge-lcdl (src/forge_lcdl/) Status: Architecture and boundaries only — implementation lands in later PDCA steps.
Updated
Purpose
Extend forge-lcdl with Playwright-assisted, read-only modules. An already-authorized user browser/session is attached through playwright. LCDL exposes deterministic Python APIs that read and normalize enterprise UI state into a source-neutral item model — without private Graph/Workday/SharePoint REST calls and without harvesting session secrets.
This extension is not a replacement for governed LLM tasks (run_task, TASK_REGISTRY_V1). It is a parallel capability for structured reads from attached sessions.
Design principles
| Principle | Meaning |
|---|---|
| Session-first | All enterprise reads go through an attached PlaywrightSession facade; no alternate credential channel in v1. |
| Read-only v1 | Navigate and observe only; no writes, submits, or DOM mutation (see READONLY_POLICY.md). |
| Deterministic core | Same fixture + fake session → same UnifiedItem shapes; LLM optional later, not required for v1 reads. |
| Core stays light | Disk-backed session bridges and heavy runtime wiring live in forge-lcdl-runtime or the host app (see runtime boundary). |
| Offline-first tests | Default pytest uses FakePlaywrightSession and fixtures; live playwright tests are opt-in. |
Separate from pw_* |
Playwright LLM tasks (tasks/pw_*_v1.py) ingest page mechanics for QA/education — not M365/Workday mailboxes. |
Placement in forge-lcdl
Playwright extension placement
How an attached authorized browser session becomes governed, source-neutral UnifiedItem reads inside forge-lcdl.
- Consumer / host appHost obtains and attaches an already-authorized playwright session before LCDL reads.
- forge_lcdl.playwright ← new package (subsequent steps)New package exposes PlaywrightSession, read-only policy, and per-source normalizers.
- UnifiedItem[] ← source-neutral modelNormalized reads return a consistent, source-neutral item list for downstream consumers.
- forge_lcdl.tasks.* ← governed LLM wrappers if neededOptional governed LLM tasks may wrap reads when schema-aware task envelopes are needed.
- forge_lcdl.mcp_client ← optional MCP/Playwright backend hook onlyOptional MCP bridge can delegate browser mechanics to PlaywrightAdapter tools.
Consumer / host app
│ attach authorized playwright session
▼
forge_lcdl.playwright ← new package (subsequent steps)
│ PlaywrightSession Protocol
│ policy + normalize
│ sources/* (outlook, teams, workday, …)
▼
UnifiedItem[] ← source-neutral model
Optional later:
├── forge_lcdl.tasks.* ← governed LLM wrappers if needed
└── forge_lcdl.mcp_client ← optional MCP/Playwright backend hook onlyUnchanged in v1 architecture (do not refactor):
src/forge_lcdl/runner.py—run_task/TaskRunnersrc/forge_lcdl/tasks/catalog_v1.py—TASK_REGISTRY_V1src/forge_lcdl/generic/— OpenAI-compat transportsrc/forge_lcdl/mcp_client/— optional MCP client (related, not identical to playwright)
Module boundaries (planned)
Target tree under src/forge_lcdl/playwright/ (created in later steps):
| Module | Responsibility | Depends on |
|---|---|---|
types.py |
UnifiedItem, attachments, provenance, schema_version |
stdlib / typing only |
errors.py |
PlaywrightError + kinds (not_attached, policy_denied, …) |
result.py patterns |
session.py |
PlaywrightSession Protocol: attach, navigate, snapshot, readonly download |
nothing playwright-runtime-specific |
policy.py |
Read-only guards, domain allowlists, tool-name blocks | may mirror mcp_client/policy.py |
normalize.py |
Shared mapping → UnifiedItem |
types.py |
sources/outlook.py |
Message list/detail normalization | session + normalize |
sources/teams.py |
Chats, channel posts, activity | session + normalize |
sources/workday.py |
Inbox, notifications, announcements | session + normalize |
sources/sharepoint.py |
News, lists, library rows | session + normalize |
sources/onedrive.py |
Recent files, file activity | session + normalize |
sources/files.py |
PPTX, XLSX, DOCX, PDF from local bytes | session + optional file extras |
adapters/fake.py |
FakePlaywrightSession + fixture replay |
session Protocol |
adapters/mcp_bridge.py |
Optional delegate to PlaywrightAdapter |
feature-flagged; mcp_client |
Public API (later): export minimal types and attach_session from forge_lcdl.playwright. Do not add to top-level forge_lcdl.__init__ __all__ until the surface stabilizes.
Unified item model
Every source maps into one shape (implementation in playwright/types.py):
| Field | Role |
|---|---|
schema_version |
Wire version (e.g. "1") |
item_id |
Stable id within source + session |
source_system |
outlook | teams | workday | sharepoint | onedrive | file |
item_type, source_kind |
Source-specific subtype |
title, body_preview, body_text |
Present only when policy allows |
actor, participants |
People / identities visible in UI |
created_at, updated_at |
ISO-8601 strings when parseable |
url, thread_id, container, state |
Navigation / threading context |
attachments |
File metadata list (not secret blobs) |
provenance, raw_ref |
Non-secret locator (DOM ref, list row key) — never tokens |
Optional governed tasks may add JSON Schema sidecars under contracts/playwright_*/v1/ in a later step if reads are wrapped by run_task.
Deterministic call flows
1. Attach (consumer or runtime)
Host obtains authorized playwright session (outside LCDL)
→ PlaywrightSession.attach(session_handle)
→ Result[None, PlaywrightError]
LCDL does not perform login, MFA, or credential prompts.
2. Read a source (core pattern)
Caller: read_outlook_inbox(session, options)
→ policy.check_navigation(url) # domain allowlist
→ session.navigate(approved_url)
→ session.snapshot() # DOM / accessibility tree / text
→ sources.outlook.normalize(snapshot) # pure function on snapshot bytes
→ Result[list[UnifiedItem], PlaywrightError]
Normalization is pure given snapshot input so unit tests do not need a browser.
3. Read-only file extraction
Caller: read_file_bytes(session, file_ref)
→ policy permits readonly download
→ session.download_readonly(file_ref) # local bytes only; no upload path
→ sources.files.extract_text(bytes, mime)
→ Result[UnifiedItem, PlaywrightError]
Office/PDF parsers may be optional extras (playwright-files); core documents the hook even if parsers ship later.
4. Error envelope
All facade entrypoints return Result[T, PlaywrightError], consistent with McpError usage in the MCP client.
Source systems (v1 scope)
source_system |
Typical UI surfaces | Normalizer module |
|---|---|---|
outlook |
Mail list, reading pane | sources/outlook.py |
teams |
Chats, channels, activity feed | sources/teams.py |
workday |
Inbox tasks, notifications, announcements | sources/workday.py |
sharepoint |
News, lists, libraries; modern reads via canonical sharepoint_* tasks (list_library_files_viewport, scroll, columns, rest) |
connectors/sharepoint.py (+ sharepoint_urls, sharepoint_modern, sharepoint_scroll, sharepoint_rest, scripts/sharepoint_readonly) — see SHAREPOINT-LIBRARY-READ-STRATEGIES.md |
onedrive |
Recent, activity | sources/onedrive.py |
file |
PPTX, XLSX, DOCX, PDF | sources/files.py |
Tenant-specific DOM variance is handled in normalizers with tolerant parsing and fixture-backed tests — not brittle single-selector assumptions in the session layer.
Relationship to MCP and runtime
| Component | Relationship |
|---|---|
mcp_client |
Optional backend when playwright exposes Playwright-like tools; McpPolicy.readmostly_playwright() is the ceiling for tool posture (see READONLY_POLICY.md). |
forge-lcdl-runtime |
Session persistence, bridge stubs, host wiring — out of core imports. |
| Governed LLM tasks | Optional later via tasks/registry.py + packs; not required for v1 reads. |
Testing architecture
| Layer | Approach |
|---|---|
| Unit | FakePlaywrightSession records (method, args); fixtures under tests/playwright/fixtures/ |
| Policy | Assert denied tools/URLs and allowed read paths |
| Normalizers | Static HTML/JSON snapshots → expected UnifiedItem fields |
| Live | Marker playwright_live (planned), skipped unless FORGE_LCDL_PLAYWRIGHT_LIVE=1 |
Default pytest in pyproject.toml continues to exclude granite and must not require network or attached playwright.
SharePoint atoms vs consumer orchestration
LCDL exposes product-agnostic SharePoint read atoms (normalize, probe, REST list, scroll/viewport/columns, list-item fields, binary download). The host attaches an authorized browser session (PlaywrightPageSession.from_page(page)), registers routes, and invokes connectors or governed tasks. LCDL never imports consumer repos and must not know deck IDs, corpus paths, job phases, or M365 Copilot menus.
See SHAREPOINT-CONSUMER-BOUNDARY.md for the atomic catalog and consumer rules.
Outlook / Teams messaging atoms vs consumer orchestration
LCDL exposes product-agnostic Outlook and Teams read atoms (health probe, chunked list harvest, conversation/thread read). The host attaches Edge/CDP, registers session_ref, and composes tasks into ingest flows (caps, checkpoints, unified_message). On attach, LCDL applies CDP focus emulation by default so Microsoft virtualized lists render without stealing OS focus (FORGE_LCDL_CDP_BRING_TO_FRONT=0). LCDL must not import consumer repos or know Cockpit sync UI steps.
See MESSAGING-CONSUMER-BOUNDARY.md, OUTLOOK-EML-EXPORT-DUAL-ARCHITECTURE.md, OUTLOOK-READ-STRATEGIES.md, and TEAMS-READ-STRATEGIES.md.
Outlook People / CRM atoms vs consumer orchestration
LCDL exposes product-agnostic Outlook People and Dynamics CRM read atoms. (The former Org Explorer Delve-iframe crawl surface has been removed; the Outlook People Organization tab is now the sole org-structure source.) The host attaches Edge/CDP, registers session_ref, and maps wire output to SQLCipher / on-disk folders. Selective CDP attach enables the same focus emulation policy as messaging ingest so background tabs stay harvestable. LCDL must not import consumer repos or know crawl depth policies, enrich tier planners, or portfolio board swimlanes.
| Surface | Dual architecture | Read strategies |
|---|---|---|
| Outlook People | OUTLOOK-PEOPLE-DUAL-ARCHITECTURE.md | OUTLOOK-PEOPLE-READ-STRATEGIES.md |
| Org directory | — | ORG-DIRECTORY-CONSUMER-BOUNDARY.md |
| CRM portfolio | CRM-PORTFOLIO-DUAL-ARCHITECTURE.md | CRM-READ-STRATEGIES.md |
Security and compliance posture
- No cookie, storage-state, header, or token export — enforced in policy and types (see READONLY_POLICY.md).
- No Microsoft Graph, SharePoint REST, or Workday REST in v1.
- Provenance and logs must remain redactable; tests will assert banned fields stay absent.
Enterprise data-handling expectations align with Data handling: LCDL core does not introduce durable mailbox storage; consumers own retention.
PDCA handoff
| Topic | Document |
|---|---|
| Discovery and file touch list | PDCA_IMPLEMENTATION_MAP.md |
| Read-only rules | READONLY_POLICY.md |
| Protocols, adapters, optional tasks | EXTENSION_POINTS.md |
Deferred to later steps: playwright/ package implementation, pytest markers, source modules, optional MCP bridge, handbook nav in docs/index.md, live testing guide.
References
- Product boundary
- Runtime boundary
- MCP client
mcp_client/policy.py—readmostly_playwright().cursor/rules/lcdl-playwright-pdca.mdc— implementation guardrails