# Seahaven Slack Bot — Alex ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![AWS CDK](https://img.shields.io/badge/AWS-CDK-FF9900?logo=amazonaws&logoColor=white) ![Slack](https://img.shields.io/badge/Slack-integration-4A154B?logo=slack&logoColor=white) ![CI](https://github.com/Sea-Haven-Industries/seahaven-slack-bot/actions/workflows/ci.yaml/badge.svg) Internal Slack assistant for Sea Haven Industries, powered by AWS Bedrock. Employees can DM Alex or @mention Alex in any channel to ask about vendors, payments, work orders, purchase orders, Amazon sites, company policies, and more. ## Architecture ``` Slack (WebSocket) │ ▼ ECS Fargate (Socket Mode) ← persistent WebSocket connection to Slack │ ├── DM (message.im) ──────────────────┐ ├── @mention (app_mention) ────────────┤ │ async invoke │ ▼ │ slack-processor Lambda ← calls Bedrock Agent, logs to DynamoDB, posts reply │ │ │ ▼ │ Bedrock Agent — Alex (Claude Sonnet 4.5) │ ├── Knowledge Base (AOSS + S3) │ ├── QBO_Lookup action group ← QuickBooks vendor search (VPC, static IP) │ ├── Google_Maps_Lookup action group ← fallback vendor search │ └── WO_PO_Lookup action group ← WO, PO, site, and payment lookups (DynamoDB direct) │ └── App Home opened ──────────────────→ app-home Lambda ← publishes Block Kit capabilities view API Gateway (bot.seahaven.com) │ └── GET /qbo/* ▼ qbo-oauth Lambda (VPC, static IP) ← OAuth 2.0 connect/callback/disconnect/launch Outbound IP: 52.202.83.13 (NAT Gateway in seahaven-vpc) EventBridge (daily 02:00 UTC) │ ├── notion-sync Lambda ← exports Office Operations Notion pages → S3 → KB ingestion ├── po-sync Lambda ← exports purchase-orders DynamoDB → S3 → KB ingestion └── workorder-sync Lambda ← exports WorkOrders + Comments DynamoDB → S3 → KB ingestion ``` ### What Alex Can Do - **Vendor Lookup** — search QBO for existing vendors, KB for approved lists, Google Maps for new options - **Payment & Invoice Status** — check if a vendor has been paid, if an invoice is scheduled, look up by check number - **Work Order Lookups** — status, assignments, comments, and full history by WO number - **Purchase Order Lookups** — status, line items, supplier, and ship-to details by PO number - **Site Lookups** — Amazon facility address and details by site code, or list all sites in a state - **Company Policies & SOPs** — employee handbook, SA8000 compliance, and operational procedures ### Vendor Query Priority 1. **QBO** — existing vetted vendors in QuickBooks Online 2. **Knowledge Base** — approved vendor lists in internal docs 3. **Google Maps** — fallback for net-new vendor discovery ## Stack | Resource | Details | |---|---| | AWS Account | 328440206208, us-east-1 | | CDK | TypeScript, v2 | | LLM | Claude Sonnet 4.5 (`anthropic.claude-sonnet-4-5-20250929-v1:0`) | | Embeddings | Amazon Titan Embed Text V2 (1024 dimensions) | | Vector Store | OpenSearch Serverless (VECTORSEARCH) | | Socket Mode | ECS Fargate (`seahaven-socket-mode`) — persistent WebSocket to Slack | | Conversation Log | DynamoDB `seahaven-conversations` (90-day TTL) | | Unanswered Questions | DynamoDB `seahaven-unanswered-questions` (180-day TTL) | | Payment Data | DynamoDB `PaymentsDashboard` (via payments-dashboard) | | PO Data Source | DynamoDB `purchase-orders` (read-only; owned by procurement-ingest / po-ingest) | | Work Order Data Source | DynamoDB `WorkOrders` + `WorkOrderComments` (via workorder-ingest) | | Site Assignments | DynamoDB `verified-sites` (auto-populated via po-ingest Streams pipeline) | | VPC | `seahaven-vpc` (`vpc-0d3d4b67bd0cf8a68`) — QBO Lambdas + Socket Mode | | Static Outbound IP | `52.202.83.13` (NAT Gateway for Intuit IP allowlist) | | QBO OAuth URLs | `bot.seahaven.com/qbo/connect`, `/qbo/callback`, `/qbo/disconnect`, `/qbo/launch` | ### Shared Resources This bot reads the `purchase-orders` DynamoDB table **read-only** (via the `po-sync` and `wo-po-lookup` Lambdas, both granted `grantReadData`). The table is owned by the `procurement-ingest` repo (`po-ingest` stack), which is the sole authoritative writer. Any change to the `purchase-orders` schema must be coordinated with `procurement-ingest` (owner) and `payments-dashboard` (the other read-only consumer). ## Cross-stack data contracts This bot does **not own any DynamoDB tables.** It imports five tables owned by other stacks via `Table.fromTableName(...)` (`lib/constructs/bedrock-agent.ts`, `workorder-sync.ts`, `po-sync.ts`) and reads them read-only. Because the tables are imported by name, this stack has no compile-time link to the owner: an owner-side change to a table's name, key schema, attribute names, GSIs, encryption key, or lifecycle policy will **silently break the Bedrock agent (Alex)** at runtime, not at deploy. The CMK grant-gap incident (INFRA-95 / M-3) is precedent: because `grantReadData` on an imported table does not carry KMS permissions, every read failed with `kms:Decrypt AccessDenied` until an explicit grant was added. Owner-side changes to any of these tables must be coordinated with this repo before they ship. Treat them as cross-repo migrations, not local edits. | Table | Owner repo / stack | Keys the bot depends on | Attributes the bot reads | Encryption | |---|---|---|---|---| | `WorkOrders` | `procurement-ingest` / `WorkorderIngestStack` (`cdk/wo_stack.py`) | PK `work_order_id` (S). `GetItem` by id; full-table `Scan` in `workorder-sync`. | `work_order_id`, `description`, `wo_status`, `customer`, `site_code`, `building`, `address`, `severity`, `priority`, `assigned_to`, `date_reported`, `scheduled_start`, `due_date`, `updated_at` | Shared CMK (`/seahaven/dynamodb/cmk-arn`) | | `WorkOrderComments` | `procurement-ingest` / `WorkorderIngestStack` (`cdk/wo_stack.py`) | PK `work_order_id` (S), SK `comment_id` (S). `Query` by `work_order_id`. | `work_order_id`, `created_at` (used for sort), `commenter`, `text` | Shared CMK (`/seahaven/dynamodb/cmk-arn`) | | `purchase-orders` | `procurement-ingest` / `po-ingest` (`cdk/po_stack.py`) | PK `po_number` (S). `GetItem` by number; full-table `Scan` in `po-sync`. | `po_number`, `po_status`, `email_type`, `cancelled_at`, `total_amount`, `currency`, `supplier.name`, `source_system`, `order_date`, `revision_date`, `payment_terms`, `requisition_number`, `department`, `submitted_by`, `on_behalf_of`, `ship_to.{name,address,location_code,attn}`, `line_items[].{description,amount,currency,need_by,category}` | Shared CMK (`/seahaven/dynamodb/cmk-arn`) | | `verified-sites` | `procurement-ingest` / `po-ingest` (`cdk/po_stack.py`) | PK `siteCode` (S). `GetItem` by code, **and `Query` on GSI `by-state`** for state listings. | `siteCode`, `address`, `fullAddress`, `city`, `state`, `zip`, `latitude`, `longitude`, `notes` | AWS-managed (NOT CMK) | | `PaymentsDashboard` | `payments-dashboard` / `payments-dashboard` (`template.yaml`) | PK `pk` (S), format `payment#`. `GetItem` by pk; full-table `Scan` filtered `begins_with(pk, "payment#")`. | `pk`, `check_number`, `payee`, `amount_usd`, `method`, `status`, `send_payment_on`, `clear_status`, `cleared_date`, `invoice_numbers`, `company_subsidiary`, `bank_reference` | CMK (shared) | **Known broken contract (verified-sites `by-state` GSI):** `lambda/wo-po-lookup/index.ts` still queries `IndexName: 'by-state'` for state-based site listings, but the owner removed that GSI on 2026-06-03 (procurement-ingest audit M-20, "0 reads in 30d"). State listings (`lookup_site` with a `state` arg) therefore fail at runtime today. Point-lookups by `siteCode` are unaffected. This needs either the GSI restored owner-side or the state-listing path removed from the bot. ## Documentation The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's `seahaven-slack-bot` stack is represented there as a Mermaid subgraph. - **[AWS Architecture Map](https://seahaven.atlassian.net/wiki/spaces/IT/pages/1540098)** (Confluence, IT space, page 1540098) ## Prerequisites - Node.js 22+ - AWS CDK (`npm install -g aws-cdk`) - Docker Desktop (required for Lambda bundling and Socket Mode container build) - AWS credentials configured locally ## Secrets Manager These secrets must exist before deploying. The Slack, QBO, Maps, and app-level token secrets must be created manually before first deploy. The Notion secret is created automatically by CDK with a placeholder. | Secret Name | Created | Structure | |---|---|---| | `seahaven/slack/credentials` | Manual (pre-deploy) | `{ "botToken": "xoxb-...", "signingSecret": "..." }` | | `seahaven/slack/app-level-token` | Manual (pre-deploy) | Plaintext `xapp-...` token with `connections:write` scope | | `seahaven/qbo/oauth` | Manual (pre-deploy) | `{ "clientId": "", "clientSecret": "", "refreshToken": "", "realmId": "" }` | | `seahaven/google/maps-api-key` | Manual (pre-deploy) | `{ "apiKey": "" }` | | `seahaven/notion/api-key` | Auto (CDK) | `{ "apiKey": "secret_..." }` | ## Deployment ```bash npm install npx cdk bootstrap aws://328440206208/us-east-1 # first time only npx cdk deploy ``` The deploy takes ~15 minutes on first run. The AOSS collection, vector index creation, and Docker image build are the slow steps. Stack outputs after deploy: - `KBDocsBucketName` — S3 bucket to upload knowledge base documents - `AgentId` — Bedrock Agent ID (Alex) ## Post-Deployment Setup ### 1. Upload knowledge base documents Upload SA8000 compliance docs, SOPs, and the employee handbook to the S3 bucket printed in stack outputs. ```bash aws s3 cp ./your-docs/ s3:/// --recursive ``` ### 2. Trigger KB sync ```bash aws bedrock-agent start-ingestion-job \ --knowledge-base-id \ --data-source-id \ --region us-east-1 ``` ### 3. Configure Notion sync 1. Create a Notion internal integration at **Settings → Connections → Develop or manage integrations** 2. Name it `seahaven-office-ops` and associate it with the **Office Operations** teamspace 3. Copy the integration token and update: **Secrets Manager → `seahaven/notion/api-key` → Edit** ### 4. Configure Slack app In the [Slack API dashboard](https://api.slack.com/apps): 1. **Basic Information** → update Display Name to "Alex", upload avatar 2. **Socket Mode** → enable, generate app-level token with `connections:write` scope 3. Store the app-level token in Secrets Manager as `seahaven/slack/app-level-token` 4. **Event Subscriptions** → enable (no Request URL needed with Socket Mode) 5. Subscribe to bot events: `message.im`, `app_mention`, `app_home_opened` 6. **OAuth & Permissions** → Bot Token Scopes: `chat:write`, `im:history`, `app_mentions:read`, `channels:history`, `groups:history` 7. **App Home** → enable Home Tab and Messages Tab 8. Reinstall app to workspace 9. Invite Alex to channels: `/invite @Alex` ## Project Structure ``` bin/ seahaven-slack-bot.ts CDK app entry point lib/ seahaven-slack-bot-stack.ts Main stack constructs/ knowledge-base.ts Bedrock KB + AOSS + S3 (via @cdklabs L2 construct) bedrock-agent.ts Alex — Bedrock Agent + QBO/Maps/WO-PO-Site-Payment action groups slack-handler.ts Processor Lambda + App Home Lambda + QBO OAuth + API Gateway socket-mode.ts ECS Fargate service running Slack Socket Mode client conversation-log.ts DynamoDB conversation + unanswered questions tables notion-sync.ts EventBridge daily cron + notion-sync Lambda po-sync.ts EventBridge daily cron + po-sync Lambda workorder-sync.ts EventBridge daily cron + workorder-sync Lambda services/ socket-mode/ Socket Mode ECS service (Dockerfile, TypeScript, @slack/socket-mode) lambda/ slack-processor/ Calls agent, writes DynamoDB, posts Slack reply app-home/ Publishes Block Kit capabilities view to App Home tab qbo-oauth/ OAuth 2.0 connect/callback/disconnect/launch for QuickBooks qbo-lookup/ Bedrock action group — QuickBooks vendor search maps-lookup/ Bedrock action group — Google Maps Places search wo-po-lookup/ Bedrock action group — WO, PO, site, and payment lookups (DynamoDB direct) notion-sync/ Daily Notion → S3 export, triggers KB ingestion po-sync/ Daily purchase-orders DynamoDB → S3 export, triggers KB ingestion workorder-sync/ Daily WorkOrders DynamoDB → S3 export, triggers KB ingestion scripts/ create-aoss-index.ts Manual fallback for AOSS index creation (not needed in normal deploy) ``` ## Known Maintenance Items - **QBO OAuth** — the refresh token auto-rotates on every API call (persisted back to Secrets Manager). If the token ever expires (100 days of inactivity), reconnect via `https://bot.seahaven.com/qbo/connect`. - **Notion sync** runs daily at 02:00 UTC automatically. Manual trigger: invoke `seahaven-notion-sync`. - **PO sync** runs daily at 02:00 UTC. Scans `purchase-orders` DynamoDB table (from [po-ingest](https://github.com/Sea-Haven-Industries/po-ingest)). Manual trigger: invoke `seahaven-po-sync`. - **Work order sync** runs daily at 02:00 UTC. Scans `WorkOrders` and `WorkOrderComments` (from [workorder-ingest](https://github.com/Sea-Haven-Industries/workorder-ingest)). Manual trigger: invoke `seahaven-workorder-sync`. - **Site assignments** are auto-populated from the `verified-sites` DynamoDB table, maintained by the po-ingest DynamoDB Streams pipeline. No manual seeding required. - **Unanswered questions** are logged to `seahaven-unanswered-questions` when Alex detects it couldn't answer a question. Review via DynamoDB console — records have `status: "pending"` and 180-day TTL. - **Socket Mode** — the ECS Fargate task maintains a persistent WebSocket connection to Slack. Monitor via CloudWatch Logs (`socket-mode` log stream). The service auto-restarts on failure.