sh-mcp/README.md
Adam Moussa b5e604dabe
Some checks failed
deploy / deploy (push) Has been cancelled
Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas (#4)
* Phase 2a: Cognito auth substrate (CDK) + pre-token & group-sync Lambdas

Stands up the real AWS auth broker the servers already validate against
(SH_MCP_ENV=aws), surface-agnostic. Nothing deployed yet (gated on Google
secrets); CI synthesizes the stack.

infra/ — root CDK app, stack sh-mcp-auth:
  - Cognito user pool, ESSENTIALS feature plan (required for the V2 pre-token
    trigger), Google external OIDC IdP (client_id/secret resolved from Secrets
    Manager at deploy via CFN dynamic reference, never inlined).
  - Resource servers + per-tier app clients whose AllowedOAuthScopes ARE the
    trust-tier boundary: ops=(read,tasks), exec=(ops+gmail/calendar, NO finance),
    finance=(finance:read ONLY, 15-min access TTL). offline refresh 30d.
  - Cognito groups sh-mcp-ops/-assistant/-finance/-admin.
  - sync-state + deny-list DynamoDB tables (overrideLogicalId pinned so a future
    refactor cannot replace+drop them; deny-list TTL attr 'expiresAt').
  - Least-priv IAM (no wildcard action/resource; Google SA secret grant scoped to
    the one secret), arm64 Lambdas, explicit 60-day log groups, alarms on the
    seahaven-alarm-topics CMK (ALARM-state actions only, two-alarm group-sync).

auth/pre-token-gen — SUPPRESS-ONLY V2 Lambda. Maps Cognito group entitlement to
  scopesToSuppress; NEVER scopesToAdd a tier scope (AllowedOAuthScopes stays the
  ceiling). Reads last_successful_sync; fail-closed to base ops:read when stale.

auth/group-sync — mirrors Google Group membership into Cognito groups every 5 min
  (jose-signed SA JWT -> Directory API, no googleapis dep); writes the freshness
  marker ONLY on full success so a partial failure keeps the pre-token Lambda
  failing closed.

37 new tests (suppress-only policy, fail-closed, reconcile diff, 16 CDK
assertions incl. Essentials/V2/per-client-scope/no-wildcard-IAM). 448 total pass;
tsc -b + infra typecheck + cdk synth + prettier clean; CI run-cdk-synth re-enabled.

App-client callback URLs are a context placeholder pending the surface decision.
Confluence map (1540098) + project memory updates owed once this deploys.

* Phase 2a: harden auth substrate per security-review + IAM cross-review

Both mandatory gates run on the 2a diff. GPT-4.1 IAM/Lambda cross-review: the
suppress-only invariant is now an executable fail-closed guard (a future edit
that sets scopesToAdd throws → no token minted). /sh-security-review fan-out +
proof-or-kill verifier: PASS (0 confirmed critical/high). The verifier refuted
the two "high" candidates (the email-case revocation "bypass" is symmetric — the
add path uses the same lowercasing filter, so an un-removable user could never
have been added; the empty-directory purge is a non-200 throw → stale marker →
fail closed). Three confirmed findings remediated:

- C2 (deny-list was inert): the sh-mcp-deny-list table was provisioned and
  documented as "hard revocation" but no code read it. The pre-token Lambda now
  reads it on every mint (DENY_LIST_TABLE env + grantReadData) and strips a
  deny-listed sub to NO tier scopes, ahead of the next group sync. Fail-OPEN on
  a DDB read error (logs deny_list_read_failed) so a blip can't lock everyone
  out — group membership + its fail-closed 30-min window stay authoritative.
- C5 (finance 30-day refresh nullified the 15-min access TTL): refresh window is
  now per-tier; finance caps at 8h, ops/exec keep 30d.
- C7 (nested Google-group members silently dropped): listGroupMembers now sets
  includeDerivedMembership and skips non-USER rows, honoring the documented
  "nested resolved" contract instead of pushing a phantom group address.

Also corrects the sync.ts comment that overstated fail-closed as instantaneous
(it is bounded by MAX_SYNC_AGE_MS). +8 tests (deny-list unit, hard-revocation
handler path, finance refresh window, deny-list env wiring); 456 pass. tsc -b,
cdk synth, prettier, eslint all clean.
2026-06-26 14:33:46 -04:00

4 KiB

sh-mcp

Sea Haven MCP platform. A TypeScript monorepo of trust-tiered MCP servers that expose Sea Haven's proprietary integrations as tools, plus the Cognito/Google auth broker and the rebuilt scheduled jobs. This service replaces seahaven-slack-bot and exec-aide, which are deprecated completely; the conversational surface becomes a configurable Slack task agent.

Status: BUILDING. Platform + auth substrate built; nothing deployed to AWS yet. Phase 0b (monorepo + @sh-mcp/shared core), Phase 1 (runnable sh-mcp-ops / sh-mcp-finance over MCP + OpenAPI), and Phase 2a (the Cognito auth substrate CDK stack + pre-token/group-sync Lambdas) are on main. The servers run locally (SH_MCP_ENV=local); SH_MCP_ENV=aws is wired but not yet deployed. The authoritative spec is docs/design.md.

Auth substrate (Phase 2a — infra/ + auth/)

infra/ is the root CDK app; cdk synth builds the sh-mcp-auth stack: a Google-federated Cognito user pool (ESSENTIALS feature plan), per-tier app clients whose AllowedOAuthScopes are the trust-tier boundary (sh-agentforce-ops / -finance / -exec), a suppress-only V2 pre-token Lambda (auth/pre-token-gen), a 5-minute group-sync Lambda (auth/group-sync), and the sync-state + deny-list tables.

Revocation has two layers. Group membership (Google → Cognito, 5-min cadence) is the primary entitlement control, with a fail-closed 30-minute freshness window: if group-sync stalls, the pre-token Lambda drops every caller to base ops:read. The sh-mcp-deny-list table is a hard-kill overlay the pre-token Lambda consults on every mint — a sub listed there is stripped to no tier scopes immediately (TTL-expiring), ahead of the next sync. The deny-list read is fail-open (a DynamoDB blip logs deny_list_read_failed and does not lock everyone out, since group membership + its fail-closed window remain authoritative). The finance app client additionally caps its refresh token at 8h (vs 30d for ops/exec) so a stolen finance refresh token cannot ride past the short access-token TTL for a month.

Deploy is gated on two manual prerequisites (owner: Adam):

  1. Google Cloud OAuth 2.0 web client (Cognito federation) → Secrets Manager sh-mcp/google-oidc ({client_id, client_secret}).
  2. Google Workspace service account w/ domain-wide delegation (Directory groups.readonly) → Secrets Manager sh-mcp/google-directory-sa.

App-client OAuth callback URLs are a placeholder (-c callbackUrls=...) until the conversational surface (Agentforce vs Bolt) is chosen — the substrate is surface-agnostic. The optional alias/seahaven-logs CMK for the Lambda log groups is supplied at deploy via -c logsKmsArn=....

Shape (planned)

  • MCP servers (trust-tiered, remote HTTP, per-server IAM):
    • sh-mcp-ops — read-mostly, agent-facing (WO/PO/site lookups, KB search, Google Maps, Gmail/Calendar, tasks, reminders).
    • sh-mcp-finance — sensitive, read-only, audited (QBO vendor search, payment lookups).
    • sh-mcp-physical — DEFERRED, admin/out-of-band only (Lenel/Yealink/3CX control).
  • Auth — Google Workspace is the single IdP; an Amazon Cognito user pool federated to Google issues scoped, audience-bound JWTs; group → scope mapping via a pre-token Lambda. See design §2.
  • Jobs — rebuilt proactive Lambdas (email classify/digest, KB syncs).
  • Language — TypeScript everywhere (servers, packages, CDK, jobs).

Open decisions

  • Task-agent surface: Agentforce (recommended) vs marketplace Claude app vs custom Bolt assistant (design §12). Drives the model + guardrail story.
  • Endpoint exposure specifics (Slack egress ranges / WAF) — design §9 / §11.

Layout (target)

packages/   shared + one package per integration
servers/    sh-mcp-ops, sh-mcp-finance  (CDK stacks)
auth/       cognito, pre-token-lambda, group-sync-lambda
jobs/       rebuilt scheduled Lambdas
docs/       design.md  (the canonical plan)

See docs/design.md for the authoritative spec.