{"id":"@lssm-tech/app.communicationos-api","slug":"app.communicationos-api","name":"@lssm-tech/app.communicationos-api","version":"0.9.5","path":"packages/apps/communicationos-api","manifestPath":"packages/apps/communicationos-api/package.json","manifestBlobSha":"09733aeee1c7ef74b21e3a532bf030ce0395242e","layer":"apps","physicalArea":null,"primaryArea":"communication-and-notifications","classification":{"privateDeclared":true,"catalogFamily":null,"catalogKind":null,"catalogStatus":null,"adoptionMode":null,"stability":null},"description":null,"descriptionOrigin":"package.json","catalog":{},"canonicalImports":[],"scripts":{"build":"bun run build:elysia","canary:meeting-host":"bun run scripts/meeting-host-canary.ts","build:elysia":"bun build --target node --packages bundle --outfile dist/index.js src/index.ts && mkdir -p api/dist api/db/migrations && cp dist/index.js api/dist/index.js && cp db/migrations/*.sql api/db/migrations/","dev":"bun run --watch src/server.ts","start":"bun run src/server.ts","test":"bun test src","test:e2e:identity":"bunx playwright test --config=playwright.identity.config.ts","typecheck":"bun --bun x tsgo --project tsconfig.json --noEmit"},"scriptStatus":"complete pinned manifest","dependencies":{"@better-auth/drizzle-adapter":"1.7.5","@elysiajs/cors":"^1.4.2","@lssm-tech/integration.authos-better-auth":"workspace:*","@lssm-tech/integration.crdt-loro":"workspace:*","@lssm-tech/integration.communication-channel-core":"workspace:*","@lssm-tech/integration.communication-channel-discord":"workspace:*","@lssm-tech/integration.communication-channel-email":"workspace:*","@lssm-tech/integration.communication-channel-slack":"workspace:*","@lssm-tech/integration.communication-channel-sms":"workspace:*","@lssm-tech/integration.communication-channel-telegram":"workspace:*","@lssm-tech/integration.communication-os-agent":"workspace:*","@lssm-tech/integration.communication-os-host-auth":"workspace:*","@lssm-tech/integration.communication-os-realtime-ticket":"workspace:*","@lssm-tech/integration.governed-product-data-postgres":"workspace:*","@lssm-tech/integration.communication-os-persistence":"workspace:*","@lssm-tech/integration.provider-email":"workspace:*","@lssm-tech/integration.provider-database":"workspace:*","@lssm-tech/integration.provider-messaging":"workspace:*","@lssm-tech/integration.provider-sms":"workspace:*","@lssm-tech/integration.runtime":"workspace:*","@lssm-tech/lib.ai-providers":"workspace:^","@lssm-tech/lib.communication-spec":"workspace:*","@lssm-tech/lib.communication-runtime":"workspace:*","@lssm-tech/lib.contracts-spec":"workspace:*","@lssm-tech/lib.contracts-runtime-server-rest":"workspace:*","@lssm-tech/lib.data-exchange-server":"workspace:*","@lssm-tech/lib.organization-planning-runtime":"workspace:*","@lssm-tech/lib.organization-planning-spec":"workspace:*","@lssm-tech/module.communication-os":"workspace:*","@lssm-tech/module.forms":"workspace:*","@lssm-tech/lib.authos-spec":"workspace:*","drizzle-orm":"catalog:data","elysia":"catalog:server","jose":"^6.2.12","postgres":"^3.4.9","@lssm-tech/lib.customer-support-spec":"workspace:*","@lssm-tech/lib.customer-support-runtime":"workspace:*","@lssm-tech/integration.customer-support-postgres":"workspace:*","@lssm-tech/integration.provider-customer-support":"workspace:*","@lssm-tech/lib.contractspec-connect":"workspace:^","@lssm-tech/integration.lssm-os-state-postgres":"workspace:^"},"devDependencies":{"@electric-sql/pglite":"catalog:data","@axe-core/playwright":"^4.13.0","@lssm-tech/tool.typescript":"workspace:*","@playwright/test":"^1.63.0","@types/bun":"catalog:types","typescript":"catalog:"},"peerDependencies":{},"optionalDependencies":{},"peerDependenciesMeta":{},"bundledDependencies":null,"dependencyStatus":"complete pinned manifest","exports":{".":"./src/index.ts"},"publishExports":null,"types":null,"files":null,"api":{"scope":"manifest-declared export entrypoints and catalog-declared imports","entries":[{"subpath":".","importPath":"@lssm-tech/app.communicationos-api","target":"./src/index.ts","targets":[{"conditions":[],"target":"./src/index.ts","sourcePath":"packages/apps/communicationos-api/src/index.ts","sourceUrl":"https://github.com/lssm-tech/contractspec/blob/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/communicationos-api/src/index.ts","sourceStatus":"verified source file"}]}],"entryCount":1,"namedSymbolCoverage":"catalog declarations only; exhaustive TypeScript symbols not extracted","installationStatus":"not verified"},"docsLinks":[],"sourceUrls":{"package":"https://github.com/lssm-tech/contractspec/tree/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/communicationos-api","manifest":"https://github.com/lssm-tech/contractspec/blob/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/communicationos-api/package.json","readme":"https://github.com/lssm-tech/contractspec/blob/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/communicationos-api/README.md","guide":"https://github.com/lssm-tech/contractspec/blob/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/web-landing/public/llms-packages/app.communicationos-api.txt"},"readme":{"status":"available","body":"# CommunicationOS API\n\n`/v2/accounts` and `/v2/contacts` expose public-safe exact-scope projections.\nTheir projections rebuild from the durable journal after API restart and remain\nempty for foreign tenant, workspace, or space scopes.\n`/v2/identity-reviews/:reviewId/decision` is manager-only and permits one\naudited merge or rejection transition.\n\nStateless Elysia entrypoint for `api.hcircle.app`. The public health endpoint is\n`/v2/health`; protected routes require an injected AuthOS verifier and explicit\ntenant, workspace, and space scope. The default composition fails closed until\nproduction authentication and ticket issuance ports are wired.\n\nAuthenticated `GET /v2/spaces` and `GET /v2/rooms` return deterministic\nprojections for exactly the supplied scope. Cross-space discovery belongs to an\nauthorized scope catalog and is not inferred from journal access.\n\nThe canonical ContractSpec transport is available under `/communication/*`.\nVersion 3 space, room, account, and contact reads derive their complete scope\nfrom the verified bearer session. Version 3 room creation accepts only a name,\nroom kind, and idempotency key; client-supplied tenant or principal fields never\nbecome authority. Invalid intent returns problem details without appending to\nthe journal. Canonical governed-work v2 list, create, and transition operations\nuse the same boundary, enforce manager roles for managed kinds, and translate\nmissing or invalid lifecycle state into explicit problem responses.\n\n## Meetings: one concept, two owners\n\nA meeting has exactly one identity, owned by OPA. CommunicationOS owns the\nconversation around it.\n\n| Concern | Owner | Contracts |\n| --- | --- | --- |\n| Scheduling, approval, booking, conference link | OPA (CompanyOS API) | `opa.smart_meeting.*` |\n| Private preparation, notes, transcripts, reviewed outcomes, shares | OPA meeting workspace (CompanyOS API for CompanyOS, this API for hCircle) | `opa.meeting_workspace.*` |\n| Conversation room, shared agenda, conversation participants, shared notes | CommunicationOS meeting thread | `communication.meeting_thread.{list,link,revise}@1.0.0` |\n| Follow-ups | CommunicationOS governed work (`kind: action`, bound to the meeting room) | `communication.governed_work.*` |\n\n`GET|POST /v2/meeting-threads` and `POST /v2/meeting-threads/:threadId/revise`\npersist meeting threads in the scope journal. Linking is idempotent per\n`(meetingSource, meetingRef)` and creates the meeting room on first link;\nrevisions require the current `expectedRevision` (stale writes answer `409`\nwith the latest thread). Scope and principal always come from the verified\nsession; client authority fields are rejected. A meeting thread never grants\nOPA authority: link-outs open the OPA workspace, which re-authorizes. Shared\nnotes are room-visible conversation notes, distinct from private OPA notes.\nIn CompanyOS the web BFF gates `list` by `communication-os.rooms.read` and\n`link`/`revise` by `communication-os.reply.send`.\n\n## OPA Meeting Intelligence host (hCircle)\n\nThe shared `opa.meeting_workspace.*` contract routes are mounted under\n`/api/v2/opa/*` for hCircle personal scopes. CompanyOS workspaces use the OPA\nmeeting workspace of the CompanyOS API instead; workspace (community) scopes\nstay denied here until a live community membership resolver is bound.\n\n`HCIRCLE_MEETING_RUNTIME_ENABLED` stays off by default. When it is `true`, the\nstartup verdict validates, before any connection opens:\n\n| Variable | Requirement |\n| --- | --- |\n| `HCIRCLE_GOVERNED_DATA_DATABASE_URL` | `postgres://` URL connecting as `opa_app` (RLS enforced) |\n| `HCIRCLE_GOVERNED_RECOVERY_TOKEN` | at least 32 characters; required by governed-data recovery whenever `HCIRCLE_GOVERNED_DATA_DATABASE_URL` is set (startup refuses otherwise) |\n| `HCIRCLE_GOVERNED_DATA_MIGRATION_URL` | `postgres://` URL connecting as `opa_migrator`; must differ from the runtime URL |\n| `DATABASE_URL` | existing direct-auth database (product membership authority) |\n| `HCIRCLE_MEETING_BYOK_MASTER_KEY` | at least 32 characters, dedicated |\n| `OPA_MEETING_ACCESS_GRANT_PEPPER` | at least 32 characters, different from the BYOK key |\n| `HCIRCLE_GOVERNED_DATA_SSL_MODE` | optional: `require` (default), `prefer`, or `allow` |\n| `COMMUNICATION_OS_API_ORIGIN` | optional exact origin; `https` (plain `http` only on loopback) |\n\n`/v2/health` reports `meetingHost`: `enabled`, `disabled_flag`, or\n`misconfigured`, and `release.sha` (`COMMUNICATION_OS_BUILD_SHA`, else\n`VERCEL_GIT_COMMIT_SHA`). A misconfigured host no longer aborts API startup:\nthe rest of CommunicationOS stays up and every meeting route answers `503\nmeeting_host_misconfigured`. A disabled host leaves the routes unmounted.\n\nThe schema ledger `hcircle_meeting_schema_ledger` applies, under one advisory\nlock and digest check, `0039` (meeting workspace + access grants, the deployed\nbatch), `0040` (meeting intake occurrences and immutable review storage), and\n`0041` (recurring intake definitions and fenced execution leases). Before any\nDDL it requires the `opa_app` and `opa_migrator` roles; afterwards it proves\nthat all 21 runtime tables exist with forced row-level security. Missing roles,\na tampered digest, or an incomplete schema report `misconfigured`. PGlite tests\ncover first apply, replay, digest tampering, lost forced RLS, and owner-scope\nisolation for the application role.\n\nEach request requires the existing short-lived CommunicationOS bearer plus its\nexact tenant, workspace, and space headers. The host hashes that verified scope\ninto an hCircle owner scope, re-verifies the bearer before governed mutations,\nreloads current direct-auth product membership from `DATABASE_URL`, and never\naccepts owner authority from a request body. Preparation reads the\nauthenticated scope's CommunicationOS history through the same tenant-scoped\njournal runtime. PersonalOS context remains fail-closed until an active,\nnon-expired consent projection is supplied through its projection port;\nprivate projection fields are never included in shared briefs.\n\nProduction qualification: after deploying the exact commit with the variables\nabove, run\n`bun run --cwd packages/apps/communicationos-api canary:meeting-host -- --origin https://api.hcircle.app --sha <40-hex commit> --out <evidence.json>`.\nIt passes only when health reports the exact SHA and `meetingHost: enabled`,\nmeeting routes are mounted and fail closed without a bearer (`403`), and the\npublic share route fails closed with `no-store`. Attach the evidence file to\nthe release capsule.\n\nAuthorized owners, administrators, auditors, and operators can inspect the\nsame append-only scope through `GET /v2/audit`. Its response follows the stable\n`audit.scoped.query@1.0.0` record shape while replacing event payloads with an\nempty object. `GET /v2/replay?traceId=...` follows `audit.trace.get@1.0.0` and\nreturns a deterministic, effect-free trace. Neither route exposes message\nbodies, provider payloads, credential references, or another scope.\n\n`GET|POST /v2/governed-work` and\n`POST /v2/governed-work/:workId/transition` persist the governed lifecycle used\nby identity review, drafts, actions, handoffs, escalations, triage, summaries,\nagents, consent, retention, and policy. Scope and principal authority always\ncome from the verified session; managed identity/privacy/policy decisions are\nrestricted to manager roles and every mutation requires an idempotency key.\nThe public contract keys are `communication.governed_work.list@1.0.0`,\n`communication.governed_work.create@1.0.0`, and\n`communication.governed_work.transition@1.0.0`. Approved agent, consent,\nretention, and policy authority can be revoked; approved operational work can be\nresolved; blocked work can recover only through an explicit resolution.\n\nProvider callbacks use opaque HMAC-bound endpoint tokens under\n`/v2/webhooks/:network/:endpointToken`. Production composition additionally\nrequires `DATABASE_URL`, a dedicated `COMMUNICATION_OS_WEBHOOK_TOKEN_SECRET`\n(at least 32 bytes), an exact `COMMUNICATION_OS_API_ORIGIN`, and explicit\n`COMMUNICATION_OS_CREDENTIAL_REF_PREFIXES`. Slack, Telegram, and official Meta\nWhatsApp and Microsoft Teams bot callbacks are authenticated before canonical\njournal ingestion;\nunsupported or incompletely configured providers return an unavailable result\ninstead of ingesting unverified content.\n\nTeams bot callbacks require both the opaque connection endpoint token and a\nfully verified Microsoft Bot Connector JWT. Verified activities durably update\nan evidence-bound conversation reference; signed installation removals revoke\nit. Delegated Graph OAuth and installed-bot credentials remain non-confusable\nprofiles.\n\nEmail channel endpoints accept Google-first Gmail OAuth, explicitly selected\nMicrosoft Mail OAuth, and MailerSend server-managed connections. OAuth state\npins `providerKey: \"google\" | \"microsoft\"` so both email providers can coexist.\nA MailerSend credential resolves server-side to\n`{ network: \"email\", provider: \"mailersend\", apiKey, signingSecret, fromEmail,\nfromName? }`. Authenticated `inbound.message` callbacks enter the canonical\nemail adapter and journal; delivery and test callbacks are authenticated and\nrecorded without creating duplicate room messages. Raw API keys, signing\nsecrets, and provider message identifiers are never returned to clients.\n\nMicrosoft Mail uses delegated `Mail.Read` and `Mail.Send`. Graph validation\ntokens are echoed only after the opaque connection endpoint resolves, callback\nbatches require the exact subscription/client-state binding, and their content\nis reduced to a durable sync invalidation. Missed lifecycle callbacks request a\ndelta reconciliation pass; they never enter agent context as instructions.\n\nProvider onboarding uses authenticated `POST /v2/oauth/:network/start` and the\npublic provider callback `GET /v2/oauth/:network/callback`. Scope, principal,\nplacement, provider proof material, expiry, an issuer-derived return origin,\nand a relative return path are AES-GCM\nencrypted into opaque state; Postgres stores only a SHA-256 replay fingerprint\nand consumes it atomically. Exchanged tokens go directly to either GCP Secret\nManager or the portable AES-GCM Postgres credential vault, and only an opaque\ncredential reference reaches the canonical connection.\n\n## Message attachments\n\nAuthenticated clients upload supported PDF, image, CSV, or text files through\n`POST /v2/attachments`, then reference at most five opaque attachment IDs when\nsending a message. The API verifies the declared size and SHA-256 digest,\npersists content inside the selected scope, and serves downloads only through\nthe authenticated `/v2/attachments/:attachmentId` route. Public message\nprojections never expose storage locations or provider credentials.\n\nManaged OAuth configuration requires `DATABASE_URL`,\n`COMMUNICATION_OS_OAUTH_STATE_SECRET` (at least 32 bytes),\nexact `COMMUNICATION_OS_API_ORIGIN`/`COMMUNICATION_OS_WEB_ORIGIN`, one credential\nvault (`COMMUNICATION_OS_SECRET_GCP_PROJECT_ID` or the at-least-32-byte\n`COMMUNICATION_OS_CREDENTIAL_MASTER_SECRET`), and at least one\nprovider pair: `COMMUNICATION_OS_GMAIL_CLIENT_ID` plus\n`COMMUNICATION_OS_GMAIL_CLIENT_SECRET`, `COMMUNICATION_OS_SLACK_CLIENT_ID` plus\n`COMMUNICATION_OS_SLACK_CLIENT_SECRET` and, for authenticated Slack webhooks,\n`COMMUNICATION_OS_SLACK_SIGNING_SECRET`, or `COMMUNICATION_OS_META_CLIENT_ID`\nplus `COMMUNICATION_OS_META_CLIENT_SECRET` and the Meta Login for Business\n`COMMUNICATION_OS_META_CONFIGURATION_ID`, or\n`COMMUNICATION_OS_MICROSOFT_CLIENT_ID` plus\n`COMMUNICATION_OS_MICROSOFT_CLIENT_SECRET`. Microsoft Teams uses delegated\nauthorization-code PKCE with `User.Read` and `ChannelMessage.Send`; app-only\nGraph tokens are not ordinary channel-send authority. Slack PKCE must be enabled\nin its app settings; Meta uses its confidential-client Business configuration\nand state.\nEmbedded hosts are configured with `COMMUNICATION_OS_OAUTH_HOST_ORIGINS`, a JSON\nobject mapping each trusted host-exchange issuer (the\n`COMMUNICATION_OS_COMPANYOS_ISSUER` value, carried as the signed `hostIssuer`\nclaim of the exchanged session) to its exact HTTPS UI origin, for example\n`{\"https://companyos.hcircle.app\":\"https://www.companyos.run\"}`. The shared\naccess-token issuer `https://api.hcircle.app` is never a key. Direct sessions,\nsessions minted before the claim existed, and unknown, malformed, wildcard, or\npath-bearing origins fall back to `COMMUNICATION_OS_WEB_ORIGIN`.\n\nDirect-auth email uses `RESEND_API_KEY` as the primary provider. Any Resend\ntransport error or non-success response falls back to MailerSend when\n`MAILERSEND_API_KEY` and `MAILERSEND_FROM_EMAIL` are present; optional sender\nlabels use `MAILERSEND_FROM_NAME`. Both providers share\n`COMMUNICATION_OS_AUTH_EMAIL_FROM` as the requested sender address. If neither\nprovider is configured, or both fail, authentication fails closed and no login\nticket is issued.\n\nDirect hCircle identity is invite-only. `COMMUNICATION_OS_AUTH_SECRET`,\n`COMMUNICATION_OS_ACCESS_TOKEN_SECRET`, `COMMUNICATION_OS_API_ORIGIN`, and\n`COMMUNICATION_OS_CHAT_ORIGIN` bind the local application session. Magic-link\nand email-code plugins cannot create identities. The central LSSM relying party\nremains disabled unless `HCIRCLE_LSSM_RP_ENABLED=1`,\n`HCIRCLE_LSSM_RP_SECURITY_REVIEW_REF`, and an at-least-32-character\n`HCIRCLE_LSSM_RP_CLIENT_SECRET` are all present. It uses the fixed\n`hcircle-chat` client registration, PKCE, issuer validation, and the exact\n`https://chat.hcircle.app/auth/callback/lssm` redirect. The central provider\nwill not mint the code without an active `app_hcircle` entitlement; only then\nmay the hCircle Better Auth host create its local account projection. Local\nmagic-link signup stays disabled. When the reviewed central RP is active, the\nAPI does not register the local magic-link plugin or delivery callback at all;\ndisabling the RP restores the existing-email local path only as a rollback\nauthority. The resulting personal scope remains product-local. These settings\ndo not replace room, role, consent, or data authority checks.\n\n`HCIRCLE_MEMBERSHIP_PROVISION_TOKEN` is the 32+ character, server-only token\nused by central AuthOS to create an idempotent local `app_hcircle` membership.\nThe direct session exchanger issues no CommunicationOS token until that local\nmembership is active; the local role, not a central claim, remains the product\nrole authority.\n\n`HCIRCLE_BOOTSTRAP_OWNER_EMAILS` is an optional comma-separated recovery seed\nfor canonical owners whose central entitlement predates local membership\nprovisioning. Reconciliation runs only after a valid local Better Auth session,\na verified exact email match, and a linked LSSM pairwise account. It is\nidempotent, server-derived, and does not bypass any later room or data scope.\n\nGoverned cross-product copies use a separate product-local PostgreSQL boundary:\n`HCIRCLE_GOVERNED_DATA_DATABASE_URL`, a distinct 32+ character\n`HCIRCLE_GOVERNED_RECOVERY_TOKEN`, optional\n`HCIRCLE_GOVERNED_DATA_AUTO_MIGRATE=true`, and\n`HCIRCLE_GOVERNED_DATA_SSL_MODE=require|allow|prefer`. The internal\n`/internal/governed-data/revocations` and `/internal/governed-data/rights`\ningresses fail closed when this boundary is absent. Do not configure the\ncentral recovery origin until every hCircle governed projection copy uses this\ncanonical encrypted store; exports remain unavailable until an authoritative\nhCircle exporter is bound.\n\nEncrypted-copy admission is independent and defaults off. Configure both\n`HCIRCLE_GOVERNED_DATA_REGION` and the secret-manager value\n`HCIRCLE_GOVERNED_MATERIALIZATION_BINDINGS` to enable\n`POST /internal/governed-data/materializations/:sourceApplicationId`. The\nbindings value is a JSON array of exact\n`{\"sourceApplicationId\":\"app_…\",\"bearerToken\":\"<32+ chars>\"}` records with\none distinct bearer per source-to-hCircle pair. Partial topology, unknown\nsources, reused source ids, stale purpose/field/region/expiry/retention/key\nauthority, and plaintext fail closed before persistence. Recovery-only\nconfiguration remains supported; do not bind production materialization until\nthe receiver key service and retention cleanup are qualified.\n\n## AI thread assistance (Vercel AI Gateway)\n\n`POST /v2/threads/:roomId/summary` (`comms.thread.summarize`) and\n`POST /v2/threads/:roomId/draft` (`comms.reply.draft`) call models only through\nthe Vercel AI Gateway. They use the shared adapter\n`createVercelGatewayModel` (`@lssm-tech/lib.ai-providers/vercel-gateway`). No\ndirect provider key is read: `MISTRAL_API_KEY`, `ANTHROPIC_API_KEY`,\n`COMMOS_LLM_PROVIDER`, `COMMOS_LLM_MODEL` and `COMMOS_LLM_BASE_URL` are ignored.\n\nEnvironment:\n\n| Variable | Required | Meaning |\n| --- | --- | --- |\n| `COMMUNICATION_OS_AI_ASSIST_ENABLED` | yes | Must be `true`. Thread content never reaches a model without it. |\n| Gateway credential | one of them | `AI_GATEWAY_API_KEY` is used first. Otherwise, on Vercel (`VERCEL=1`), the per-invocation OIDC token from the `x-vercel-oidc-token` request header is used and labelled `oidc`. If `VERCEL_PROJECT_ID` is exposed, the token must belong to this project. A `VERCEL_OIDC_TOKEN` env value is the last fallback. |\n| `COMMUNICATION_OS_AI_DEFAULT_MODEL` | no | Default Gateway model id. Ignored unless it is in the catalog. |\n\nThe flag and the credential are read on every request, so nothing is cached at startup. If either is missing, authorized calls answer `503 ai_unavailable`. Public `GET /v2/health` reports `data.aiAssist` as `enabled`, `disabled_flag` or `no_gateway_auth`, with no secrets, so you can check production without user credentials. Tokens are passed per call and never logged.\n\n**Switching models.** The allowlist is `COMMUNICATION_OS_AI_MODEL_CATALOG`\n(`@lssm-tech/lib.communication-spec/contracts/ai-model-catalog`). It lists\nAnthropic Claude Haiku/Sonnet/Opus 5.5, OpenAI GPT-6 Luna and GPT-6.1 Sol, and\nGoogle Gemini 3.8 Flash and Gemini 4 Argon. Each request uses the first match\nin this order:\n\n1. The request body `model`. It must be in the allowlist, and only `ai.use`\n   roles may send it. Otherwise the call fails with `422 ai_model_not_allowed`\n   or `403 ai_model_override_denied`.\n2. The workspace setting.\n3. `COMMUNICATION_OS_AI_DEFAULT_MODEL`.\n4. The catalog default, `anthropic/claude-haiku-5.5`.\n\nThe workspace setting lives at `GET|POST /v2/ai/model-settings`\n(`communication.ai.model-settings.get|set`). Owner, admin, enterprise_admin\nand founder_approver roles can change it. Each change is appended to the\njournal as `communication.ai.model-setting.changed`, with the actor and the\nprevious model. To add a model, add a catalog entry (contract first) and\nrelease. No environment change is needed.\n\nSummaries return `modelName` (the Gateway id) and `modelSource`. AI drafts\nrecord `modelName`, `modelSource`, `modelGateway` and `inferenceRegion` in the\ngoverned-work details. The existing governance still applies: drafts are never\nsent automatically, authors are pseudonymized, at most 40 messages are sent,\nthere are at most five pending drafts per principal and thread, and provider\nfailures answer `503 ai_provider_unavailable`.\n\n**Inbox triage.** `POST /v2/threads/:roomId/triage` (`comms.thread.triage`) and\n`POST /v2/threads/triage` (`comms.thread.triage.batch`, body\n`{ \"threadIds\": [...] }`, 1 to 25 unique ids) return advisory labels. Each\nlabel has a category (request, question, complaint, sales, support,\nscheduling, fyi, spam_suspect), an urgency (low, normal, high), `needsReply`\nand a one-line `reason`.\n\n- Only `ai.use` roles may call them. They use the same flag, Gateway\n  credential and model order as summaries.\n- The model must return the strict structured label. Anything else is a\n  provider failure, and no label is stored.\n- A fresh label is appended to the journal as\n  `communication.ai.thread-triaged`, with its model, prompt version and\n  revision (the latest covered message id). The next call reuses it until a\n  new message arrives.\n- Fresh classifications are limited to 100 per principal and workspace every\n  10 minutes, counted per process. Cache hits do not count.\n- Reasons are stripped of emails and phone numbers.\n- In a batch, each thread reports `fresh`, `cached`, `skipped` (with\n  `thread_not_found`, `insufficient_messages` or `rate_limited`) or `failed`.\n  One thread failing never fails the whole batch.\n- Labels never send, assign or change a thread.\n\n**Data residency.** Every Gateway call asks for EU-zone inference\n(`inferenceRegion: { scope: 'zone', geoRegion: 'eu' }`), zero data retention\nand no prompt training. Calls are limited (`only`) to the catalog's inference\nproviders: Bedrock for Anthropic, OpenAI for OpenAI, Vertex for Google. On\n2026-10-15 each of these advertised an EU region for its catalog models.\n\nThis constrains provider inference only. The Gateway itself routes and\nprocesses requests globally, so EU-only transit is not guaranteed and needs\nits own approval. Requested routing is also not proof of where a request was\nserved: use `readVercelGatewayRoutingMetadata` for per-call evidence.\n\nMistral models are not offered. The Gateway lists no EU inference region for\nthem, and the shared adapter refuses Mistral inference.\n\n## Forms\n\nForms uses the same bearer principal and tenant/workspace scope as messaging.\nConfigure `COMMUNICATION_OS_FORMS_DATA_PROTECTION_SECRET` and\n`COMMUNICATION_OS_FORMS_MILESTONES`; optional verification, AI, and distributed\ndefense adapters use the matching `COMMUNICATION_OS_FORMS_*` endpoint, bearer,\nchannel/provider, and identifier-secret variables. The API is `/api/forms/v1`;\npublic hosts expose `/f/{opaqueToken}`. Fresh databases receive the hardened\nschema directly; existing Forms databases are upgraded additively by\n`0002_forms_scope_hardening.sql` semantics during readiness initialization.\n\n## Support private notes\n\nService-only `POST /api/communications/v1/support/cases/{caseRef}/internal-notes`\ncreates encrypted private-note content. The matching evidence URL verifies the\nexact case, note, and evidence reference without returning content. Both routes\nderive tenant, workspace, actor, audience, and capability from a short-lived\nAuthOS grant; caller scope headers are ignored.\n\nConfigure `COMMUNICATION_OS_SUPPORT_NOTE_PROTECTION_SECRET`, the separately\nrotated `COMMUNICATION_OS_SUPPORT_NOTE_IDENTIFIER_SECRET`,\n`COMMUNICATION_OS_SUPPORT_NOTE_KEY_VERSION`,\n`COMMUNICATION_OS_AUTHOS_GRANT_VERIFY_URL`, and\n`COMMUNICATION_OS_AUTHOS_WORKLOAD_TOKEN`. The routes remain unavailable if any\nrequired authority or protection adapter is absent.\nThe database migrator records immutable, digest-bound\n`0001_communication_os_baseline` and\n`0002_communication_support_internal_notes`, followed by the pinned\n`0003_communication_os_rls` receipt before enabling these routes; changed\nhistorical source or receipt digests fail closed.\n\n### Authorized send retries\n\nMessage retries recheck current membership and connection authority before returning accepted content. Reuse of a request key with a different principal, room, content, subject, reply target, metadata, encryption envelope or attachment list is denied. Object metadata key order does not change semantic identity. Missing or partial attachment resolution fails before acceptance.\n\nThe journal/outbox transaction validates a replayed message before enqueueing delivery, and insert-returned journal events preserve their tenant scope. Queued acceptance remains distinct from provider delivery. Source-bound OPA follow-up delivery, full concurrent authority fencing and production qualification remain open.\n\n### Provider queue acceptance\n\nAn external connection send requires `appendForDelivery`; journal-only persistence cannot claim provider queue acceptance. The event/outbox transaction validates the accepted message identity, connection and exact persisted delivery scope, command, policy and payload. Conflicting outbox keys roll back instead of silently dropping delivery. Provider delivery keys use `delivery:<send idempotency key>`; retries cannot substitute another key. Atomic journal replay uses the accepted message ID rather than the discarded candidate ID.\n\nInternal room journal acceptance remains available without a provider adapter. Queued does not imply sent or delivered. Legacy malformed delivery inputs fail closed and require explicit repair; broader cross-scope key migration and OPA follow-up delivery remain separate qualification work.\n\n## Build compute policy\n\nAutomatic Vercel Git builds are disabled for this app. Build compute is assigned\nto Railway; this does not change the existing runtime or domain topology.\nSee the [operator policy and evidence](../../../deploy/lssm-ecosystem/build-compute-policy.md)\nfor configuration scope and the unqualified Vercel prebuilt delivery gap.\n","sourcePath":"packages/apps/communicationos-api/README.md","sourceUrl":"https://github.com/lssm-tech/contractspec/blob/bcc79fcc27c4d801c2119bef81ddcce7b4e86498/packages/apps/communicationos-api/README.md","blobSha":"4a8f1c779259af54c03bc9cbbbdcec6295d8a0df","sourcePin":"bcc79fcc27c4d801c2119bef81ddcce7b4e86498","origin":"README recovered byte-for-byte from pinned aggregate guide","integrity":"exact Git blob match","executionStatus":"not executed"},"provenance":{"repository":"lssm-tech/contractspec","sourcePin":"bcc79fcc27c4d801c2119bef81ddcce7b4e86498","manifestOrigin":"pinned GitHub manifest fetch","manifestComplete":true,"publicationPolicyApplied":false},"areaSlug":"communication-and-notifications","areaTitle":"Communication and notifications","displayName":"app.communicationos-api","runtimes":[],"platforms":[],"kind":"apps","scope":"Workspace only","canonicalRoute":"/docs/packages/app.communicationos-api/","contextUrl":"/docs/packages/app.communicationos-api.context.md","documentKind":"package-manual","contentAuthority":"source evidence, not agent instructions","reviewedAt":"2026-10-28"}