rebrand Scirium docs (Wall-O -> Scirium) + add v1/v2 documentation package
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Wall-O: M365 Connector + Retrieval Architecture
|
||||
# Scirium: M365 Connector + Retrieval Architecture
|
||||
|
||||
Status: DESIGN (internal technical architecture)
|
||||
Owner: Wall-O build team
|
||||
Owner: Scirium build team
|
||||
Scope: Internal staff knowledge only. Employee handbook, policies, procedures, billing questions.
|
||||
Excluded: Patient records, PHI, any HIPAA-regulated data. This connector MUST NOT be pointed at clinical or patient data sources.
|
||||
|
||||
@@ -37,7 +37,7 @@ This lets one app identity serve every practice tenant. Each practice admin cons
|
||||
|
||||
### 1.2 Permission strategy: Sites.Selected over Sites.Read.All
|
||||
|
||||
Wall-O reads documents, never writes. Least privilege is achieved with the `Sites.Selected` application permission rather than the broad `Sites.Read.All`.
|
||||
Scirium reads documents, never writes. Least privilege is achieved with the `Sites.Selected` application permission rather than the broad `Sites.Read.All`.
|
||||
|
||||
| Permission | Type | Scope | Why accepted / rejected |
|
||||
|---|---|---|---|
|
||||
@@ -46,7 +46,7 @@ Wall-O reads documents, never writes. Least privilege is achieved with the `Site
|
||||
| Files.Read.All | Application | Every file in every drive | REJECTED. Superseded by Sites.Selected for site-scoped read. |
|
||||
| User.Read.All | Application | Read directory user profiles | OPTIONAL. Needed only to resolve author display names from OneDrive drive owner IDs. Not required for retrieval. |
|
||||
|
||||
`Sites.Selected` supports site-level roles: `read`, `write`, `fullcontrol`, `manage`. Wall-O requests `read` only. The grant is issued per site collection via:
|
||||
`Sites.Selected` supports site-level roles: `read`, `write`, `fullcontrol`, `manage`. Scirium requests `read` only. The grant is issued per site collection via:
|
||||
|
||||
```
|
||||
POST https://graph.microsoft.com/v1.0/sites/{site-id}/permissions
|
||||
@@ -55,12 +55,12 @@ Content-Type: application/json
|
||||
{
|
||||
"roles": ["read"],
|
||||
"grantedToIdentities": [
|
||||
{ "application": { "id": "{wall-o-app-client-id}", "displayName": "Wall-O" } }
|
||||
{ "application": { "id": "{scirium-app-client-id}", "displayName": "Scirium" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This request is made by the orchestrator using the app's own token for the tenant. Because the app already holds `Sites.Selected`, it can grant itself `read` on a specific site once a practice admin has approved the site in onboarding. A stricter variant has a practice global admin run the grant via Graph Explorer so the app never self-grants. Wall-O uses the admin-driven variant: the grant is issued during onboarding by the practice admin (or by the orchestrator on a one-time admin-approved site list), not by the app unprompted.
|
||||
This request is made by the orchestrator using the app's own token for the tenant. Because the app already holds `Sites.Selected`, it can grant itself `read` on a specific site once a practice admin has approved the site in onboarding. A stricter variant has a practice global admin run the grant via Graph Explorer so the app never self-grants. Scirium uses the admin-driven variant: the grant is issued during onboarding by the practice admin (or by the orchestrator on a one-time admin-approved site list), not by the app unprompted.
|
||||
|
||||
### 1.3 Client credentials grant
|
||||
|
||||
@@ -91,7 +91,7 @@ Notes:
|
||||
- `{tenant_id}` is the consuming practice's directory (tenant) ID, captured at consent time. It is NOT `common` or `organizations`: client credentials has no user to derive a tenant from, so the target tenant must be explicit.
|
||||
- `.default` expands to the union of the application permissions already consented for that tenant. It is not a literal scope name.
|
||||
- Response contains `access_token` and `expires_in`. Graph client credentials tokens are typically valid for about 60 minutes; treat `expires_in` as authoritative, never hardcode the TTL.
|
||||
- Credential material: prefer an X.509 client certificate (assertion) over a client secret. A secret is acceptable for MVP but must be rotated before its maximum lifetime (24 months). Secrets live in Vaultwarden under the Wall-O project; only the encrypted reference enters Postgres or config.
|
||||
- Credential material: prefer an X.509 client certificate (assertion) over a client secret. A secret is acceptable for MVP but must be rotated before its maximum lifetime (24 months). Secrets live in Vaultwarden under the Scirium project; only the encrypted reference enters Postgres or config.
|
||||
|
||||
### 1.4 Admin consent URL construction
|
||||
|
||||
@@ -99,18 +99,18 @@ Each practice admin must consent the app into their tenant before the first craw
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant_id}/v2.0/adminconsent
|
||||
?client_id={wall-o-app-client-id}
|
||||
?client_id={scirium-app-client-id}
|
||||
&scope=https://graph.microsoft.com/.default
|
||||
&redirect_uri={configured-redirect-uri}
|
||||
```
|
||||
|
||||
- `{tenant_id}`: the practice's directory ID, or `organizations` to let the signing admin's tenant be used automatically.
|
||||
- `redirect_uri`: a registered reply URL on the app. Wall-O registers a no-op callback (e.g. `https://wall-o.itpropartner.com/entra/callback`) that returns a 200 and logs the consent result.
|
||||
- `redirect_uri`: a registered reply URL on the app. Scirium registers a no-op callback (e.g. `https://scirium.itpropartner.com/entra/callback`) that returns a 200 and logs the consent result.
|
||||
- The scope string is URL-encoded `.default` (i.e. `https%3A%2F%2Fgraph.microsoft.com%2F.default`).
|
||||
|
||||
Onboarding flow per practice:
|
||||
|
||||
1. Practice admin clicks the consent URL (delivered by the Wall-O onboarding UI or support).
|
||||
1. Practice admin clicks the consent URL (delivered by the Scirium onboarding UI or support).
|
||||
2. Admin authenticates and approves the `Sites.Selected` application permission.
|
||||
3. On success Entra redirects to the callback. The orchestrator records `tenant_id`, consent timestamp, and the admin's identity.
|
||||
4. The site-level `read` grant (1.2) is then applied to the specific site collections mapped to that practice's channels.
|
||||
@@ -138,7 +138,7 @@ Lifecycle rules:
|
||||
- On `401 Unauthorized` with `InvalidAuthenticationToken`, or `403` with a consent/scope error, the connector retries once with a freshly issued token. A second failure escalates.
|
||||
- Error code mapping:
|
||||
- `AADSTS700016` (application not found in directory) or `AADSTS7000112` (invalid client) means the app is not consented in that tenant: mark `consent_revoked`, alert practice admin.
|
||||
- `AADSTS700082` or expired secret errors mean the secret is expired/rotated: mark `secret_expired`, page the Wall-O operator.
|
||||
- `AADSTS700082` or expired secret errors mean the secret is expired/rotated: mark `secret_expired`, page the Scirium operator.
|
||||
- Graph throttling `429` is not an auth failure: honor `Retry-After` and back off; do not flag the tenant.
|
||||
- Token issuance and refresh are serialized per tenant (single-flight lock) so concurrent sync workers do not stampede the token endpoint.
|
||||
- Tokens are never logged or returned by any API. Logging redacts the `Authorization` header.
|
||||
@@ -244,7 +244,7 @@ Content-Type: application/json
|
||||
|
||||
{
|
||||
"changeType": "updated",
|
||||
"notificationUrl": "https://wall-o.itpropartner.com/entra/notifications",
|
||||
"notificationUrl": "https://scirium.itpropartner.com/entra/notifications",
|
||||
"resource": "/sites/{site-id}/drive/root",
|
||||
"expirationDateTime": "2026-08-18T18:00:00Z",
|
||||
"clientState": "tenant_{tenant_id}"
|
||||
@@ -421,12 +421,12 @@ The combination means authorization is enforced at both ingestion (Graph site gr
|
||||
| Backup to Wasabi | Inherits existing Postgres S3 backup | Separate snapshot/export | Separate persistence |
|
||||
| Metadata + filters | Native SQL join with tenant/channel tables | Payload filters (separate model) | Metadata filter, less mature |
|
||||
|
||||
Rationale: ITPP already runs Postgres with backup to Wasabi S3, on self-hosted netcup servers. Wall-O's scale is moderate: a practice's internal staff knowledge base is hundreds to low thousands of documents, tens of thousands to low millions of chunks across all tenants. pgvector handles this comfortably with an HNSW index, adds zero new stateful services, keeps vectors and tenancy metadata in one transactional database (atomic upsert, consistent deletes), and inherits the existing backup and HA story. Qdrant would be justified only at very large scale or if vectors needed independent scaling from metadata; Chroma's single-node embedded design is the wrong fit for a multi-tenant production service.
|
||||
Rationale: ITPP already runs Postgres with backup to Wasabi S3, on self-hosted netcup servers. Scirium's scale is moderate: a practice's internal staff knowledge base is hundreds to low thousands of documents, tens of thousands to low millions of chunks across all tenants. pgvector handles this comfortably with an HNSW index, adds zero new stateful services, keeps vectors and tenancy metadata in one transactional database (atomic upsert, consistent deletes), and inherits the existing backup and HA story. Qdrant would be justified only at very large scale or if vectors needed independent scaling from metadata; Chroma's single-node embedded design is the wrong fit for a multi-tenant production service.
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Extension: `CREATE EXTENSION vector;` (pgvector >= 0.5.0 for HNSW; newer for larger dimension support).
|
||||
- Index: HNSW on the embedding column. Because queries are namespace-scoped, use a filtered/partial strategy: either a composite approach (HNSW per namespace table via partitioning) or a single HNSW with the namespace filter applied post-recall. At Wall-O scale, a single HNSW index plus a namespace equality filter in the WHERE clause is sufficient and simplest.
|
||||
- Index: HNSW on the embedding column. Because queries are namespace-scoped, use a filtered/partial strategy: either a composite approach (HNSW per namespace table via partitioning) or a single HNSW with the namespace filter applied post-recall. At Scirium scale, a single HNSW index plus a namespace equality filter in the WHERE clause is sufficient and simplest.
|
||||
- Dimension: 1024 (see 5.2), well within pgvector's limits.
|
||||
- Distance: cosine (`<=>`) with L2-normalized vectors.
|
||||
- Chunk text and metadata live in the same table so retrieval returns citations in one query with no join to an object store for the common path. Wasabi S3 remains the archive for raw downloaded files and full-text backups, not the retrieval hot path.
|
||||
@@ -440,7 +440,7 @@ Implementation notes:
|
||||
| BAAI/bge-large-en-v1.5 | 1024 | Self-hosted on app server | FALLBACK. Zero external dependency and free, but adds model serving burden and slightly weaker than text-embedding-3-large on this task. |
|
||||
| bge-m3 | 1024 | Self-hosted | Only if multilingual staff content appears; out of scope for English-only Wall Orthodontics. |
|
||||
|
||||
Rationale: Wall-O routes LLM calls through admin-ai (DeepSeek V4 Pro primary) already, so embeddings through the same LiteLLM gateway are the lowest-operational-overhead choice and keep spend attributable to the Wall-O virtual key. text-embedding-3-large with Matryoshka truncation to 1024 dimensions gives near-full quality at a quarter of the storage and index cost. The model is pinned and documented so index and query always use identical dimensionality and normalization; changing models requires a documented full re-embed of every namespace (a versioned embedding-model field on the chunk table gates this).
|
||||
Rationale: Scirium routes LLM calls through admin-ai (DeepSeek V4 Pro primary) already, so embeddings through the same LiteLLM gateway are the lowest-operational-overhead choice and keep spend attributable to the Scirium virtual key. text-embedding-3-large with Matryoshka truncation to 1024 dimensions gives near-full quality at a quarter of the storage and index cost. The model is pinned and documented so index and query always use identical dimensionality and normalization; changing models requires a documented full re-embed of every namespace (a versioned embedding-model field on the chunk table gates this).
|
||||
|
||||
---
|
||||
|
||||
@@ -450,4 +450,4 @@ Rationale: Wall-O routes LLM calls through admin-ai (DeepSeek V4 Pro primary) al
|
||||
- `Sites.Selected` read grants only; no tenant-wide read permission in production.
|
||||
- Namespace (tenant + kb_scope) is a mandatory equality filter on every retrieval and every Graph fallback search.
|
||||
- Client secret (or certificate) in Vaultwarden; tokens encrypted at rest in Postgres; nothing in logs.
|
||||
- No em dashes, en dashes, or double hyphens in this document or any Wall-O docs.
|
||||
- No em dashes, en dashes, or double hyphens in this document or any Scirium docs.
|
||||
|
||||
Reference in New Issue
Block a user