# Gateco — Full LLM Context > Permission-aware retrieval layer for AI systems — the security and permission middleware between AI agents and organizational knowledge. ## 1. Overview Gateco is a permission-aware retrieval layer that gates access to digital resources with policy enforcement, identity-based access control, and auditability. It is **not** a vector database, RAG framework, or IAM replacement — it is the missing middleware between AI agents and organizational knowledge. **Business model:** 4-tier SaaS (Free / Team / Growth / Enterprise) with usage-based billing on "secured retrievals." - **API base URL:** `https://api.gateco.ai` - **Domain:** `gateco.ai` - **OpenAPI spec:** `https://www.gateco.ai/openapi.json` - **Docs:** `https://www.gateco.ai/docs` - **RAG authorization guide:** `https://www.gateco.ai/rag-authorization` (the category page: exact failure, all architectural options including Cerbos/OPA/Purview/database-native, when Gateco fits and when it does not) - **Permission-aware retrieval:** `https://www.gateco.ai/permission-aware-retrieval` (term definition + late-binding architecture) How Gateco does it: Gateco evaluates each retrieved candidate against policy at request time and refills; it does not rewrite your vector query. - **RAG access control:** `https://www.gateco.ai/rag-access-control` (practical implementation path) - **HIPAA-compliant RAG:** `https://www.gateco.ai/hipaa-compliant-rag` (requirement-to-control mapping; Gateco BAA is on the roadmap, not yet available) - **GDPR-compliant RAG:** `https://www.gateco.ai/gdpr-compliant-rag` (requirement-to-control mapping; DPA available) All API endpoints are prefixed with `/api/`. Requests and responses use JSON. List endpoints return a paginated envelope: `{ "data": [...], "meta": { "pagination": { "page": 1, "per_page": 20, "total": 100, "total_pages": 5 } } }`. --- ## 2. Authentication ### JWT Login ```bash curl -X POST https://api.gateco.ai/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@company.com","password":"secret"}' ``` Returns `{ "access_token": "...", "refresh_token": "..." }`. Use the access token as `Authorization: Bearer `. ### API Key Pass `api_key` to the SDK constructor instead of calling login. Mutually exclusive with JWT flow. ### OAuth Exchange For Google/GitHub OAuth, the frontend receives an auth `code` and exchanges it: ``` POST /api/auth/exchange { "code": "...", "provider": "google" } ``` ### Token Refresh ``` POST /api/auth/refresh { "refresh_token": "..." } ``` Both SDKs handle token refresh automatically — proactive refresh before expiry, fallback refresh on 401. Access tokens live 15 minutes and refresh tokens 7 days, and **refresh rotates**: every `POST /api/auth/refresh` revokes the session it was called with and issues a new pair. One refresh token therefore belongs to one client. Copying a stored refresh token (for example from `~/.gateco/credentials.json`, which `gateco login` writes) into a second client makes the two revoke each other on their next refresh, which surfaces as `Invalid or expired refresh token` in both. To share credentials between the CLI, the MCP server and your own code, use an API key (`GATECO_API_KEY`) or log each client in separately. --- ## 3. Python SDK ```bash pip install gateco # With MCP server support pip install gateco[mcp] ``` ### Client Initialization ```python from gateco_sdk import GatecoClient, AsyncGatecoClient # Synchronous with GatecoClient("https://api.gateco.ai") as client: client.login("user@company.com", "password") # Async async with AsyncGatecoClient("https://api.gateco.ai") as client: await client.login("user@company.com", "password") # With an API key. Keys carry scopes: a key with the `retrieve` scope can # execute retrievals, grounded answers and principal lookup. Create it under # Settings > API Keys (every plan). See section 19 for the scope table. client = GatecoClient("https://api.gateco.ai", api_key="gck_...") ``` Constructor options: `base_url`, `api_key`, `access_token` / `refresh_token`, `timeout` (seconds, default 30), `max_retries` (default 2), `retry_backoff_factor` (default 0.5). Credential resolution (SDK 1.13.0+): the `api_key` argument, else `GATECO_API_KEY`, else the `access_token` argument, else the session `gateco login` stored in `~/.gateco/credentials.json` (whose `base_url` also applies when none is given). Tokens refreshed from a stored session are written back to the file, because refresh rotates. `GATECO_CREDENTIALS_FILE` overrides the path. TypeScript has no session file and reads only the argument and `GATECO_API_KEY`. ### Resource Namespaces #### client.auth - `login(email, password) -> TokenResponse` - `signup(name, email, password, organization_name) -> TokenResponse` - `refresh() -> TokenResponse` - `logout() -> None` #### client.connectors - `list(page=1, per_page=20) -> Page` - `get(connector_id) -> Connector` - `create(name, type, config=None, **kwargs) -> Connector` - `update(connector_id, **kwargs) -> Connector` - `delete(connector_id) -> None` - `test(connector_id) -> TestResult` - `bind(connector_id, bindings) -> BindResult` - `get_search_config(connector_id) -> dict` - `update_search_config(connector_id, search_config) -> dict` - `get_ingestion_config(connector_id) -> dict` - `update_ingestion_config(connector_id, ingestion_config) -> dict` - `get_coverage(connector_id) -> Coverage` - `suggest_classifications(connector_id, **kwargs) -> Suggestions` - `apply_suggestions(connector_id, suggestions) -> ApplyResult` #### client.retrievals - `execute(query_vector=None, *, search_mode="vector", alpha=None, pattern_type=None, case_sensitive=None, **kwargs) -> RetrievalResult` - `filter(principal_id, connector_id, candidates, include_trace=False) -> FilterResult` — Apply policy filtering to externally-sourced candidates - `list(page=1, per_page=20, **filters) -> Page` - `get(retrieval_id) -> Retrieval` #### client.policies - `list(page=1, per_page=20) -> Page` - `get(policy_id) -> Policy` - `create(name, type, effect, **kwargs) -> Policy` - `update(policy_id, **kwargs) -> Policy` - `delete(policy_id) -> None` - `activate(policy_id) -> Policy` - `archive(policy_id) -> Policy` - `get_templates() -> list` — List 7 built-in policy templates (all plans) - `create_from_template(template_id, placeholder_values) -> Policy` — Create draft from template (Team+) #### client.ingest - `document(connector_id, external_resource_id, text, *, chunking=None, embedding=None, **kwargs) -> IngestResult` - `batch(connector_id, records, *, chunking=None, embedding=None, **kwargs) -> BatchResult` — Team+ (batch_ingestion) - `document_byoe(connector_id, external_resource_id, pre_embedded_chunks, **kwargs) -> IngestResult` — bring your own embeddings - `file(connector_id, path, **kwargs) -> IngestFileResult` — upload + self-hosted extraction - `delete_resource(connector_id, external_resource_id) -> dict` — tombstone: vectors + registry + soft delete - `jobs.enqueue(connector_id, job_type, payload, max_attempts=3) -> dict` — async job, 202 (Team+, async_ingestion) - `jobs.get(job_id)` / `jobs.list(status=)` / `jobs.cancel(job_id)` / `jobs.wait_for(job_id)` — job lifecycle #### client.sources (Growth+, source_connectors) - `create(name, source_type, config) -> dict` — source_type: gdrive | sharepoint | confluence | notion - `list()` / `get(id)` / `delete(id)` — secrets never returned - `test(id) -> {ok, message}` — adapter connectivity check - `acl_coverage(id) -> dict` — matched/unmatched principals in imported ACLs #### client.identity_providers - `list(page=1, per_page=20) -> Page` - `get(idp_id) -> IdentityProvider` - `create(name, type, **kwargs) -> IdentityProvider` - `update(idp_id, **kwargs) -> IdentityProvider` - `delete(idp_id) -> None` - `sync(idp_id) -> SyncResult` #### client.principals - `list(page=1, per_page=20) -> Page` - `get(principal_id) -> Principal` - `resolve(email=None, provider_subject=None, identity_provider_id=None) -> Principal` — Find active principal by email or IDP subject (never creates) #### client.data_catalog - `list(page=1, per_page=20, **kwargs) -> Page` - `get(resource_id) -> Resource` - `update(resource_id, **kwargs) -> Resource` #### client.pipelines - `list(page=1, per_page=20) -> Page` - `get(pipeline_id) -> Pipeline` - `create(name, source_connector_id, **kwargs) -> Pipeline` - `update(pipeline_id, **kwargs) -> Pipeline` - `get_runs(pipeline_id) -> list` - `run(pipeline_id) -> RunResult` #### client.billing - `get_plans() -> list` - `get_usage() -> Usage` - `get_invoices(page=1, per_page=20) -> Page` - `get_subscription() -> Subscription` - `start_checkout(plan_id, billing_period="monthly") -> Checkout` - `create_portal(return_url=None) -> Portal` #### client.audit - `list(page=1, per_page=20, **kwargs) -> Page` - `export_csv(**kwargs) -> Export` #### client.simulator - `run(principal_id, **kwargs) -> SimulationResult` #### client.dashboard - `get_stats() -> Stats` #### client.answers - `execute(query, principal_id, connector_id, top_k=None, filters=None, search_mode=None, alpha=None) -> Answer` #### client.onboarding - `get_status() -> OnboardingStatus` — Computed 6-step setup checklist - `dismiss() -> None` — Dismiss onboarding wizard #### client.retroactive - `register(connector_id, **kwargs) -> RegisterResult` #### client.users - `get_me() -> User` — Get the authenticated user's profile - `update_me(name) -> User` — Update the authenticated user's display name --- ## 4. TypeScript SDK ```bash npm install @gateco/sdk ``` ### Client Initialization ```typescript import { GatecoClient } from "@gateco/sdk"; const client = new GatecoClient({ baseUrl: "https://api.gateco.ai" }); await client.login("user@company.com", "password"); // With API key const client = new GatecoClient({ baseUrl: "https://api.gateco.ai", apiKey: "gck_...", }); ``` Constructor options: `baseUrl`, `apiKey`, `timeout` (ms), `maxRetries` (default 2), `retryBackoffFactor` (default 0.5). ### Resource Namespaces All namespaces mirror the Python SDK with camelCase naming: - `client.auth` — `login()`, `signup()`, `refresh()`, `logout()` - `client.users` — `getMe()`, `updateMe(name)` - `client.connectors` — `list()`, `get()`, `create()`, `update()`, `delete()`, `test()`, `bind()`, `getSearchConfig()`, `updateSearchConfig()`, `getIngestionConfig()`, `updateIngestionConfig()`, `getCoverage()`, `suggestClassifications()`, `applySuggestions()` - `client.retrievals` — `execute()`, `filter()`, `list()`, `get()` - `client.policies` — `list()`, `get()`, `create()`, `update()`, `delete()`, `activate()`, `archive()`, `getTemplates()`, `createFromTemplate()` - `client.ingest` — `document()`, `batch()`, `file()`, `files()` (multipart, 1.10.0+), `deleteResource()`, plus `client.ingest.jobs` (`enqueue()`, `get()`, `list()`, `cancel()`, `waitFor()`) - `client.sources` — `create()`, `list()`, `get()`, `delete()`, `test()`, `aclCoverage()` (Growth+) - `client.identityProviders` — `list()`, `get()`, `create()`, `update()`, `delete()`, `sync()`, `generateScimToken()`, `revokeScimToken()`, `suggestPolicies()`, `applyPolicySuggestions()` - `client.principals` — `list(status=, search=, group=)`, `get()`, `resolve()` - `client.groups` — `list(search=)` (IdP-synced groups, live member counts) - `client.dataCatalog` — `list()`, `get()`, `update()`, `updateMetadata()` (1.10.0+) - `client.pipelines` — `list()`, `get()`, `create()`, `update()`, `getRuns()`, `run()` - `client.billing` — `getPlans()`, `getUsage()`, `getInvoices()`, `getSubscription()`, `startCheckout()`, `createPortal()` - `client.audit` — `list()`, `exportCsv()` - `client.apiKeys` — `create()`, `list()`, `delete()`, `rotate()` - `client.onboarding` — `status()`, `dismiss()` - `client.relationships` — `create()`, `list()`, `delete()` - `client.simulator` — `run()`, `runBatchPreview()` - `client.dashboard` — `getStats({ sparklines? })` - `client.answers` — `execute()` - `client.retroactive` — `register()` Call `client.close()` when done. --- ## 5. CLI The CLI is installed with the Python SDK (`pip install gateco`). ```bash # Authentication (you are prompted for the password, or pipe it on stdin; # --password works but lands in shell history and `ps` output) gateco login --email user@company.com gateco whoami # Connectors gateco connectors list gateco connectors create --name "My DB" --type pgvector --config '{"host":"..."}' gateco connectors test gateco connectors get gateco connectors get-search-config gateco connectors set-search-config --json '{"table_name":"docs","vector_column":"embedding","content_column":"text"}' gateco connectors get-ingestion-config gateco connectors set-ingestion-config --file ingestion.json # Retrievals (vector search, default) gateco retrieve --connector-id --principal-id --vector-file embedding.json --top-k 10 # Keyword search gateco retrieve --connector-id --principal-id --query "revenue report" --search-mode keyword # Hybrid search gateco retrieve --connector-id --principal-id --query "revenue report" --search-mode hybrid --alpha 0.5 # Grep search (exact match) gateco retrieve --connector-id --principal-id --query "ERR-4021" --search-mode grep # Policies gateco policies list gateco policies create --name "Policy" --type rbac --effect allow # Ingestion (.txt/.md are sent as text; PDF, Word, PowerPoint, Excel, CSV are uploaded) gateco ingest report.txt --connector-id --classification confidential --sensitivity high gateco ingest "Q3 board deck.pdf" --connector-id --classification confidential --sensitivity high --domain finance gateco ingest-batch ./documents --connector-id --glob "*.md" # Grounded answers (supports --search-mode vector|keyword|hybrid) gateco ask "What's our quarterly revenue?" --connector-id --principal-id --top-k 5 # Principals gateco principals list gateco principals resolve --email user@company.com gateco principals resolve --provider-subject google-user-123 # Classification suggestions gateco suggest-classifications # MCP server gateco mcp serve ``` --- ## 5b. MCP Server (Model Context Protocol) Gateco provides an MCP server that lets AI agents (Claude Desktop, Cursor, etc.) perform permission-aware retrieval directly. Installed as an optional extra: `pip install gateco[mcp]`. Listed in the official MCP registry as `ai.gateco/gateco` (PyPI package `gateco`, stdio transport, run via `uvx --from "gateco[mcp]" gateco-mcp`). ### Starting the Server ```bash # Via CLI subcommand gateco mcp serve # Via direct entry point (for MCP host configs) gateco-mcp ``` ### Available Tools | Tool | Description | Required Params | |------|-------------|-----------------| | `gateco_retrieve` | Permission-aware retrieval (vector/keyword/hybrid/grep) | `connector_id`, `query`, `principal_id` or `email`. Optional: `search_mode`, `alpha`, `pattern_type`, `case_sensitive` | | `gateco_ask` | Grounded answer synthesis (Team+) | `connector_id`, `query`, `principal_id` or `email`. Optional: `search_mode` (vector/keyword/hybrid), `alpha` | | `gateco_resolve_principal` | Find active principal by email or provider_subject | At least one of: `email`, `provider_subject`. Optional: `identity_provider_id` | | `gateco_check_access` | Dry-run access simulation (Growth+). User session only (`gateco login`); not callable with an API key | `principal_id` | | `gateco_list_connectors` | List connectors with readiness levels | — | | `gateco_list_principals` | List identity principals | Optional: `status`, `search`, `group` filters via SDK | | `gateco_list_groups` | List groups (IdP-synced and local directory) with live member counts. Works with a `retrieve`-scoped key against a backend from 2026-10-02 on | Optional: `search` | All tools support optional pagination (`page`, `per_page`) where applicable. `gateco_retrieve` and `gateco_ask` accept optional `top_k`. ### Authentication The MCP server reuses the SDK credential resolution chain: 1. `GATECO_API_KEY` env var (recommended for MCP hosts; the key needs the `retrieve` scope, available on every plan). Six of the seven tools work with a key; `gateco_check_access` calls a session-only endpoint and needs a stored `gateco login` with the key unset, because the key takes precedence. (`gateco_list_groups` joined the key-callable set on 2026-10-02 when `GET /api/groups` was added to the `retrieve` scope.) 2. `GATECO_BASE_URL` env var 3. `~/.gateco/credentials.json` (set by `gateco login`) ### Claude Desktop Configuration ```json { "mcpServers": { "gateco": { "command": "gateco-mcp", "env": { "GATECO_API_KEY": "gck_...", "GATECO_BASE_URL": "https://api.gateco.ai" } } } } ``` ### Tool Response Format All tools return markdown-formatted text. Retrieval and answer tools include structured tables with scores, resource IDs, and excerpts. Denied content is never exposed — only denial reasons and counts are shown. --- ## 6. Core Concepts ### Connectors Connections to your vector databases. Gateco supports 12 connectors: pgvector, Pinecone, Qdrant, Weaviate, Milvus, Chroma, OpenSearch, Supabase, Neon, Azure AI Search (`azure_ai_search`), Vertex AI Vector Search (`vertex_ai_vector_search`), and Vertex AI Search (`vertex_ai_search`). Tier 1 connectors (pgvector, Supabase, Neon, Pinecone, Qdrant) support full ingestion workflows. Each connector manages connection credentials, search configuration, and metadata resolution. ### Policies Rules that determine who can access what data. Three types: RBAC (role-based), ABAC (attribute-based), and REBAC (relationship-based). Policies have a lifecycle: Draft -> Active -> Archived. Only active policies are enforced. Deny-by-default model — without an explicit allow policy, access is denied. ### Secured Retrievals Permission-checked queries against your vector database. Every retrieval evaluates all active policies against the requesting principal and target resources. Outcomes: Allowed (all results pass), Partial (some filtered), or Denied (no access). ### Principals The identities requesting access — users, service accounts, or AI agents. Principals are synced from identity providers (Azure Entra ID, AWS IAM, Okta, GCP) or managed directly. ### Gated Resources Your vector data with security metadata attached — classification (public/internal/confidential/restricted), sensitivity (low/medium/high/critical), domain, and ownership. Resources are bound to connectors. ### Readiness Levels (L0-L4) A semantic measure of security posture per connector: - **L0: Not Ready** — No connection established - **L1: Connection Ready** — Connector authenticated and reachable - **L2: Search Ready** — Search/retrieval operations functional - **L3: Resource Policy** — Resource-level policy bindings exist and active policies configured - **L4: Chunk Policy** — Chunk-level policy metadata available (highest granularity) --- ## 7. Connectors Reference ### Supported Connectors | Connector | Type Value | Tier | Ingestion | |-----------|-----------|------|-----------| | pgvector | `pgvector` | Tier 1 | Full | | Supabase | `supabase` | Tier 1 | Full | | Neon | `neon` | Tier 1 | Full | | Pinecone | `pinecone` | Tier 1 | Full | | Qdrant | `qdrant` | Tier 1 | Full | | OpenSearch | `opensearch` | Tier 2 | Retroactive only | | Weaviate | `weaviate` | Tier 2 | Retroactive only | | Milvus | `milvus` | Tier 2 | Retroactive only | | Chroma | `chroma` | Tier 2 | Retroactive only | | Azure AI Search | `azure_ai_search` | Tier 2 | Retroactive only | | Vertex AI Vector Search | `vertex_ai_vector_search` | Tier 2 | Search only — no listing | | Vertex AI Search | `vertex_ai_search` | Tier 2 | Retroactive + hybrid search | **Connector maturity:** GA (pgvector, supabase, neon, pinecone, qdrant), Beta (weaviate, opensearch, azure_ai_search), Preview (milvus, chroma, vertex_ai_vector_search, vertex_ai_search). Beta connectors are supported in production but may have capability gaps noted in the search capabilities table. Preview connectors are functional but not yet covered by the production SLA. ### Search Capabilities by Connector | Connector | Vector | Keyword (ranked) | Keyword (filter) | Hybrid (native) | Grep (substring) | Grep (regex) | |-----------|--------|-------------------|-------------------|-----------------|-------------------|--------------| | pgvector | Yes | Yes (FTS) | — | Yes (SQL CTE) | Yes | Yes | | Supabase | Yes | Yes (FTS) | — | Yes (SQL CTE) | Yes | Yes | | Neon | Yes | Yes (FTS) | — | Yes (SQL CTE) | Yes | Yes | | Weaviate | Yes | Yes (BM25) | — | Yes (native) | Yes | No | | OpenSearch | Yes | Yes (BM25) | — | Yes (native) | Yes | Yes | | Qdrant | Yes | — | Yes (filter) | Yes (prefetch+RRF) | Yes | No | | Milvus | Yes | — | Yes (filter) | Yes (reranker) | Yes | No | | Pinecone | Yes | No | No | No | No | No | | Chroma | Yes | No | No | No | Yes | Yes | | Azure AI Search | Yes | Yes (BM25) | — | Yes (native) | No (deferred) | No (deferred) | | Vertex AI Vector Search | Yes | No | — | No (vector-only) | No | No | | Vertex AI Search | Yes | Yes (ranked) | — | Yes (native) | No | No | Connectors report granular `search_capabilities` in their response. When a mode is unsupported, the API returns an error naming the limitation. **Vertex AI Vector Search note:** Set `distance_measure` in the connector config to match the index's `distanceMeasureType` (`"COSINE"` default, `"DOT_PRODUCT_DISTANCE"`, `"SQUARED_L2_DISTANCE"`). Mismatches cause incorrect similarity scores. The connection tester emits a warning if it detects a mismatch by fetching the underlying Index resource. ### Metadata Resolution Modes Configured per-connector via `metadata_resolution_mode`: - **`sidecar`** (default) — Metadata managed in Gateco's own registry - **`inline`** — Metadata extracted from vector payloads (requires `metadata_field_mapping` in config) - **`sql_view`** — Metadata read from a Postgres view (Postgres-family connectors only: pgvector, supabase, neon) - **`auto`** — Tries inline -> sql_view -> sidecar in order Every resolution mode feeds the same policy engine: a vector resolved inline or through a SQL view is evaluated exactly like a registered resource, and deny-by-default applies to it. Denied results never carry text. > **Note:** `"inline"` mode requires a `metadata_field_mapping` object in the connector's search config. For most setups, use `"sidecar"` (default) — it reads from Gateco's registered resource metadata which is always available after ingestion. ### DB Schema Introspection (Postgres-family) Postgres-family connectors (pgvector, supabase, neon) that are in `connected` status support live schema introspection: ``` GET /api/connectors/{id}/db-schema ``` Returns all tables in the connected database that contain at least one `vector`-typed column, with all column names and types: ```json { "tables": [ { "name": "embeddings", "columns": [ { "name": "id", "type": "uuid" }, { "name": "content", "type": "text" }, { "name": "embedding", "type": "vector" }, { "name": "classification", "type": "text" } ] } ] } ``` Use this to populate the Search Config dialog (table name, content column, metadata columns) without requiring users to know their schema upfront. Returns 422 for non-Postgres connectors or connectors that are not yet connected. ### Preflight Check `GET /api/connectors/:id/preflight` evaluates production readiness against 6 blocking checks and 1 advisory: | Check | Type | Description | |-------|------|-------------| | `check_connected` | Blocking | Connector status is `connected` | | `check_search_config` | Blocking | Valid search configuration is set | | `check_active_policy` | Blocking | At least one active policy targets this connector | | `check_resources` | Blocking | At least 10 gated resources registered | | `check_readiness_level` | Blocking | Readiness level is L3 or higher | | `check_idp_synced` | Advisory | At least one IDP has synced principals (warning only, does not block) | Returns `PreflightResult`: - `ready_for_production: bool` — true only when all blocking checks pass - `checks: list` — per-check result with `name`, `passed`, `blocking`, and `detail` fields - `recommendation: str` — plain-language next step when not ready ### Create and Test a Connector ```python connector = client.connectors.create( name="Production Pinecone", type="pinecone", config={ "api_key": "pk-...", "environment": "us-east-1", "index_name": "knowledge-base", }, ) result = client.connectors.test(connector.id) print(result.status, result.latency_ms) ``` --- ## 8. Policies Reference ### Create and Activate a Policy ```python policy = client.policies.create( name="Engineering Data Access", type="rbac", effect="allow", description="Allow engineering team to access internal docs", resource_selectors=[{"field": "domain", "op": "eq", "value": "engineering"}], rules=[{ "effect": "allow", "priority": 1, "description": "Engineers can access internal docs", "conditions": [ {"field": "resource.classification", "operator": "lte", "value": "internal"}, {"field": "principal.roles", "operator": "contains", "value": "engineer"}, ], }], ) client.policies.activate(policy.id) ``` ### Policy Condition Fields Condition fields **must** be prefixed with `resource.` or `principal.`: | Prefix | Available Fields | |--------|-----------------| | `resource.` | `classification`, `sensitivity`, `domain`, `labels`, `encryption_mode` | | `principal.` | `roles`, `groups`, `attributes.*` | > **WARNING:** Bare field names (e.g., `classification` without prefix) silently resolve against the **principal**, not the resource. Always use `resource.classification`. **Operators:** `eq`, `ne`, `in`, `contains`, `lte` (ordered level comparison), `gte` (ordered level comparison). **Policy validation (enforced at create and activate time):** - Condition field must start with `resource.`, `principal.`, or `relation.` prefix. Bare names (e.g., `"classification"` without prefix) silently resolve against the principal, not the resource. The backend rejects bare names with 422 to prevent this footgun. - Each resource selector is an object `{"field": , "op": , "value": ...}`. Valid fields: `classification`, `sensitivity`, `domain`, `labels`, `encryption_mode`, `connector_id`, `source_connector_id`, `owner_principal`, with or without the `resource.` prefix (both resolve identically). A selector on a field the engine does not resolve would match no resource, so the policy would silently apply to nothing; the backend rejects such selectors with 422 (including the `key:value` mistake, e.g. `{"field": "domain:finance"}`). The shorthand `{"connector_id": ""}` is also accepted. - Empty `resource_selectors` require `apply_to_all_resources: true` in the request body. Without this flag, a policy with no selectors would otherwise apply to no resources, which is almost never intentional. - Built-in policy templates auto-set `apply_to_all_resources: true` for intentionally broad policies (e.g., `global_internal_read`). Templates created via `POST /api/policies/from-template` respect these defaults automatically. **Deny policy default-effect:** When a deny policy's selectors match but none of its rules match, the policy-level `effect=deny` fires. To deny only specific conditions, add a catch-all allow rule at lowest priority. ### Policy Lifecycle - **Draft** — Created but not enforced. Safe to edit and test. - **Active** — Enforced on all retrievals. Use Access Simulator (Growth+) to test before activating. - **Archived** — Retained for audit history but no longer enforced. ### Policy Types - **RBAC** — Match by role (e.g., `engineer`, `admin`) - **ABAC** — Match by attributes (e.g., department, clearance level) - **REBAC** — Match by relationships (e.g., document owner, team member) --- ## 9. Retrieval Flow ### Search Modes Four search modes — three lenses over the same data: | Mode | How it thinks | Best for | |------|---------------|----------| | `vector` (default) | "What does this *mean*?" | Natural language, conceptual queries | | `keyword` | "Which docs mention these words most?" | Topic search, term relevance | | `hybrid` | Vector + keyword fused | Production RAG (industry standard) | | `grep` | "Does this exact pattern exist?" | Error codes, IDs, compliance, audit | ### Execute a Retrieval ```python # Vector search (default) result = client.retrievals.execute( connector_id="conn_abc", principal_id="user_123", query_vector=[0.1, 0.2, 0.3, ...], top_k=10, ) # Keyword search (ranked FTS) result = client.retrievals.execute( connector_id="conn_abc", principal_id="user_123", query="quarterly revenue report", search_mode="keyword", top_k=10, ) # Hybrid search (vector + keyword fused) result = client.retrievals.execute( connector_id="conn_abc", principal_id="user_123", query="quarterly revenue report", search_mode="hybrid", alpha=0.5, # 1.0=all-vector, 0.0=all-keyword top_k=10, ) # Grep search (exact pattern matching) result = client.retrievals.execute( connector_id="conn_abc", principal_id="user_123", query="ERR-4021", search_mode="grep", ) print(result.match_count, result.sort_order) # total matches, "natural" ``` ### Search Mode Parameters - `search_mode` — `"vector"` (default), `"keyword"`, `"hybrid"`, `"grep"` - `alpha` — Hybrid weight 0.0–1.0 (1.0=all-vector, 0.0=all-keyword). Only for hybrid mode - `pattern_type` — `"substring"` (default) or `"regex"`. Only for grep mode - `case_sensitive` — Case-sensitive grep matching (default: false). Only for grep mode ### Verified End-User Subject (X-End-User-Token) `principal_id` on a retrieval is who the CALLER says the end user is, authenticated by the caller's own credential (a session JWT or an API key). Gateco enforces that principal's policy faithfully but, by default, does not verify the claim: a multi-user agent that names the wrong principal, or a stolen key that impersonates one, gets a correct answer for the wrong person. Every retrieval record therefore also stores who asserted the subject (`actor_type`, `actor_name`, `actor_id` for a session, `api_key_id` for a key), so a mis-assertion can be investigated. To make it preventable, an organization can require proof. Send the end user's own identity token, issued by one of the org's identity providers, in the `X-End-User-Token` header: ```python result = client.retrievals.execute( connector_id="conn_abc", principal_id="user_123", query="quarterly revenue report", end_user_token=id_token_from_your_login_flow, # sent as X-End-User-Token ) print(result.subject_verified) # True only if the token verified AND named principal_id ``` ```typescript const result = await client.retrievals.execute({ connectorId: "conn_abc", principalId: "user_123", query: "quarterly revenue report", endUserToken: idTokenFromYourLoginFlow, }); ``` How it is verified: the token's `iss` selects the identity provider whose `oidc_config.issuer` matches; signature, expiry, issuer and audience are checked against that provider's JWKS (`oidc_config.jwks_uri`, fetched over HTTPS and cached, refetched once on an unknown key id so rotation does not fail closed); the subject claim (`sub` by default, configurable as `oidc_config.subject_claim`) is mapped to an active principal's `provider_subject` under that provider; and the resolved principal MUST equal the request's `principal_id`. Symmetric algorithms (HS256 etc.) are refused at configuration time. Configure it per provider: `PATCH /api/identity-providers/{id}` with `{"oidc_config": {"issuer": "...", "audience": "...", "jwks_uri": "https://..."}}`. Modes, set with `PATCH /api/organization/settings` `{"subject_verification": ...}` (available on every plan; a safety control, not a feature): - `none` (default): the token is optional. Absent, the request proceeds with `subject_verified=false`. Present, it must verify; an invalid token is refused even in this mode. This is the rollout path: send tokens, watch the verified share of retrieval records climb, then flip the mode with evidence. - `verified_token`: the token is required on `/api/retrievals/execute`, `/api/retrievals/filter` and `/api/answers/execute`. Every failure is a distinct `403`: `SUBJECT_TOKEN_REQUIRED` (no header), `SUBJECT_TOKEN_ISSUER_UNKNOWN` (no provider configured for that `iss`), `SUBJECT_TOKEN_INVALID` (signature, expiry, audience, or not a JWT), `SUBJECT_NOT_RESOLVED` (verified, but no active principal has that subject), `SUBJECT_MISMATCH` (verified, but names a different principal than `principal_id`; the message includes both ids because a caller naming the wrong subject has a bug the operator needs to see). Nothing is retrieved or recorded on a refusal. What this is not: on-behalf-of token exchange. Gateco validates a token the caller already holds for the user and never mints or exchanges one. Say "Gateco can be configured to refuse any retrieval not accompanied by a verified end-user token", not "Gateco performs OBO". Agent and batch flows that have no end-user token should stay in `none` mode. ### Response Structure - `outcome` — `"allowed"` (all pass), `"partial"` (some filtered), `"denied"` (no access) - `results` — List of matched items with `vector_id`, `score`, `metadata` - `matched_chunks` — The candidate set fetched from the vector store for policy evaluation. It is deliberately larger than `top_k` (over-fetched) so that chunks denied by policy can be replaced by the next candidates (`diagnostics.refill_rounds`); it is not the number of results returned. `allowed_chunks` + `denied_chunks` + `unresolved_chunks` account for it - `search_mode` — Echoed search mode used - `total_candidates` — Number of vectors matched before policy filtering - `filtered_count` — Number of results removed by policy enforcement - `policy_traces` — Per-result policy evaluation details (which policies matched, effect) - `keyword_latency_ms` / `vector_latency_ms` — Per-leg timing (when available) **Grep-specific response fields:** - `pattern_type` — `"substring"` or `"regex"` - `match_count` — Total matches in connector before `top_k` truncation - `sort_order` — `"natural"` (insertion order, not relevance-ranked) **Retrieval diagnostics:** All retrieval and answer responses include a `diagnostics: RetrievalDiagnostics | null` field with: - `candidates_fetched` — vectors pulled from the index before filtering - `candidates_allowed` — vectors that passed all policy checks - `candidates_denied` — vectors blocked by at least one active policy - `refill_rounds` — number of additional fetch rounds triggered by the post-filter refill loop (see below) - `policies_evaluated` — count of active policies evaluated against each candidate - `active_denial_reasons` — list of distinct denial reason codes seen across all denied candidates - `metadata_resolution_mode` — which resolution path was used: `sidecar`, `inline`, `sql_view`, or `auto` - `readiness_level` — connector readiness level at the time of the retrieval (L0-L4) - `outcome_detail` — human-readable explanation of the retrieval outcome: describes `no_access` (all candidates denied), `insufficient_context` (too few allowed chunks for synthesis), `not_ready` (preflight gap blocked retrieval), or a success summary ### TypeScript ```typescript // Vector search (default) const result = await client.retrievals.execute({ connectorId: "conn_abc", principalId: "user_123", queryVector: [0.1, 0.2, 0.3], topK: 10, }); // Keyword search const keyword = await client.retrievals.execute({ connectorId: "conn_abc", principalId: "user_123", query: "quarterly revenue", searchMode: "keyword", }); // Grep search const grep = await client.retrievals.execute({ connectorId: "conn_abc", principalId: "user_123", query: "ERR-4021", searchMode: "grep", }); ``` ### Post-Filter Refill Loop When the majority of top-K candidates are denied by policy, Gateco automatically re-fetches with a wider window to surface authorized content that exists deeper in the index. The refill loop: - Triggers when denied candidates exceed allowed candidates after the first evaluation pass - Re-fetches up to `min(top_k * 10, 200)` candidates per round, max 2 rounds - Stops early if `top_k` allowed results are found before the round limit - `diagnostics.refill_rounds` records how many rounds ran (0 means the initial pass was sufficient) This is transparent to callers: the response always returns up to `top_k` allowed results regardless of how many rounds it took. ### Policy Filter (Bring Your Own Retrieval) For organizations with their own retrieval infrastructure, the Policy Filter endpoint decouples policy enforcement from vector search. Customers bring their own retrieval results and Gateco applies policy filtering, audit, and access control. Three usage modes: - **Simple**: Gateco full pipeline (`POST /api/retrievals/execute`) - **Hybrid**: Gateco retrieval + external LLM (`POST /api/answers/execute`) - **Advanced**: External retrieval → Gateco policy filter (`POST /api/retrievals/filter`) ```python result = client.retrievals.filter( connector_id="conn_abc", principal_id="user_123", candidates=[ {"vector_id": "v1", "score": 0.95, "text": "Q3 revenue...", "resource_id": "registered-resource-uuid"}, {"vector_id": "v2", "score": 14.2, "text": "Merger docs", "metadata": {"classification": "confidential", "sensitivity": "high"}}, {"vector_id": "v3", "score": 0.5, "text": "Some text"}, ], include_trace=True, ) for item in result.results: print(item["vector_id"], item["granted"], item["resource_mode"]) ``` ### TypeScript ```typescript const result = await client.retrievals.filter({ connectorId: "conn_abc", principalId: "user_123", candidates: [ { vector_id: "v1", score: 0.95, text: "Q3 revenue...", resource_id: "registered-resource-uuid" }, { vector_id: "v2", score: 14.2, text: "Merger docs", metadata: { classification: "confidential", sensitivity: "high" } }, { vector_id: "v3", score: 0.5, text: "Some text" }, ], includeTrace: true, }); ``` **Candidate resolution precedence:** 1. `resource_id` provided and found → uses registered `GatedResource` metadata (`resource_mode: "registered"`) 2. `resource_id` provided but not found → denied with `"resource_not_found"` (no silent fallback) 3. No `resource_id` but `metadata` provided → evaluated via inline metadata (`resource_mode: "synthetic"`) 4. Neither → denied with `"no_metadata_provided"` (`resource_mode: "unresolved"`) **Text handling:** Denied candidates have `text: null` in the response. Customers who don't want Gateco to see raw denied text should send only IDs + metadata, or use registered resources. **Scores:** Passed through as opaque floats — cosine similarity, BM25 scores, reranker logits all accepted. **Limits:** Max 100 candidates per request, 10K chars per candidate text. --- ## 10. Ingestion Available for Tier 1 connectors only (pgvector, Supabase, Neon, Pinecone, Qdrant). Ingestion endpoints accept both JWT bearer tokens and API keys (X-API-Key header), so server-to-server pipelines can ingest without a user session. Plan gating and limits: - Single-document ingestion (`POST /api/v1/ingest`, `/file`): all plans. - Batch ingestion (`POST /api/v1/ingest/batch`, `/files`): Team plan and above (`batch_ingestion` feature). Free-plan orgs receive 403 with `error.reason = "feature_not_in_plan"`. - Monthly ingested-document limits: Free 100, Team 10,000, Growth 100,000, Enterprise unlimited. Limits are enforced: an over-limit org receives 403 with `error.reason = "resource_limit_reached"` (the SDKs expose this as `is_limit` / `isLimit` on `EntitlementError`). Limits reset at the start of each billing period; upgrading the plan raises the allowance immediately. Batch requests embed all records' chunks in coalesced embedding API calls, so large batches are significantly faster than per-record loops. If the embedding provider fails after retries, every record in that batch fails with `error_category = "embedding_provider_error"`. ### File extraction (self-hosted by default) File ingestion (`POST /api/v1/ingest/file`, `/files`) extracts office documents (Word .doc/.docx/.docm, PowerPoint .ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm, Excel .xls/.xlsm/.xlsb, OpenDocument .odt/.ods/.odp, RTF, EPUB) with anydoc running inside the Gateco deployment, and PDF plus images with Docling, also inside the deployment: customer documents never leave the deployment by default. CSV/XLSX/JSON/TXT/MD always use local native parsers. Cloud extraction via LlamaParse exists only as an explicit operator opt-in (`EXTRACTION_BACKEND=llama_parse`) and applies to PDF and images only; office formats are always extracted locally. ### Chunking Each connector pins a chunking generation write-once on first ingest: connectors created before chunking v2 keep v1 (fixed 512-character windows, no re-chunking of existing corpora); fresh connectors get v2 — structure-aware markdown chunking (heading-path context prefixes like "# Doc > ## Section" on every chunk) at 512 tokens with 15% overlap. Per-request override: pass `chunking: {"strategy": ..., "chunk_size": ..., "chunk_overlap": ...}` with strategy `characters`, `tokens`, `recursive`, or `markdown`; the override applies to that request only. ### Async ingestion jobs (Team+) Large or recurring ingestion runs as background jobs: `POST /api/v1/ingest/jobs` (202 + job id) with `job_type` `document` or `batch`, `GET /api/v1/ingest/jobs/{id}` for status and progress, list and cancel endpoints. Jobs retry transient failures up to `max_attempts` then park in `dead_letter`. At-least-once execution is safe: vector ids are deterministic and writes are upserts. SDK: `client.ingest.jobs` with a `wait_for`/`waitFor` helper. Requires the `async_ingestion` feature (Team plan and above). ### Deleting ingested resources `DELETE /api/v1/ingest/resources/{external_resource_id}?connector_id=...` tombstones a resource: vectors deleted from the vector DB, registry chunks removed, gated resource soft-deleted, audited as `resource_deleted`. ### Pre-embedding dedup Re-ingesting a document skips embedding and insertion for chunks whose position and content hash are unchanged: re-syncing unchanged corpora costs zero embedding spend. Shifted content re-embeds (vector ids are positional). ### Source connectors with permission import (Growth+) Connect Google Drive, SharePoint, Confluence, or Notion as document sources (`POST /api/source-connections`; secrets are envelope-encrypted and never returned). Attach a source to a pipeline and run it: Gateco syncs changed documents since the delta cursor, extracts them self-hosted, ingests them, and imports each document's effective viewer ACL. Matched users (by email against IDP-synced principals) get REBAC viewer relationships; a DRAFT "Source ACL: imported viewers" policy is created for admin review — imported permissions are never auto-enforced. Deletions in the source tombstone the corresponding resources on the next sync. The ACL coverage report (`GET /api/source-connections/{id}/acl-coverage`) lists matched/unmatched principals — unmatched emails are people with source access who are unknown to Gateco. Notion limitation: its API exposes no per-page permissions, so Notion pages map to a workspace-scope entry. ### Embedding providers Supported providers: `openai` (default), `openai_compatible` (any OpenAI-wire endpoint such as Ollama, TEI, or vLLM — keeps embeddings fully self-hosted; requires `base_url` + `model`), `cohere`, and `voyage`. Pass `embedding: {"provider": ..., "model": ..., "base_url": ...}` on an ingest request to select one; requests never carry API keys (keys resolve server-side from `OPENAI_API_KEY` / `COHERE_API_KEY` / `VOYAGE_API_KEY`). The connector records the resolved provider profile on first ingest, and both later ingests and server-side query embedding follow it automatically, keeping the corpus consistent. If you connect an EXISTING vector DB (no ingestion through Gateco), there is no first-ingest to pin the profile, so declare it explicitly with `PATCH /api/connectors/:id/embedding-profile` ({provider, model, dimensions, base_url}); server-side text-query embedding then matches your corpus's vector space. This is write-once per connector. ### Single Document ```python response = client.ingest.document( connector_id="conn_abc", external_resource_id="doc-quarterly-report", text="Q4 2025 financial results...", classification="confidential", sensitivity="high", domain="finance", labels=["quarterly", "financial"], owner_principal_id="user_cfo", ) print(f"Ingested {response.chunk_count} chunks") ``` ### Batch Ingestion ```python response = client.ingest.batch( connector_id="conn_abc", records=[ { "external_resource_id": "doc-001", "text": "First document content...", "classification": "internal", }, { "external_resource_id": "doc-002", "text": "Second document content...", "classification": "public", }, ], ) print(f"Ingested {response.total_chunks} total chunks") ``` ### Idempotency Ingestion is idempotent on `external_resource_id` — re-ingesting the same ID replaces the previous version. If the new version produces fewer chunks than the previous one, the stale chunks are pruned from both Gateco's registry and the vector DB (audited as `resource_chunks_pruned`), so no orphaned chunk of the old version remains retrievable. --- ## 11. Classification Suggestions Rule-based engine that scans connector resources and suggests classifications via keyword pattern matching. ### Workflow: Suggest -> Review -> Apply ```python # 1. Scan and get suggestions suggestions = client.connectors.suggest_classifications( connector_id="conn_abc", scan_limit=1000, sample_size=10, ) # 2. Review for s in suggestions.suggestions: print(f"{s.resource_key}: {s.suggested_classification} " f"({s.confidence:.0%}) — {s.reasoning}") # 3. Apply approved suggestions result = client.connectors.apply_suggestions( connector_id="conn_abc", suggestions=suggestions.suggestions, ) print(f"Applied {result.applied}, created {result.resources_created} resources") ``` ### Classification Levels | Classification | Sensitivity | Example | |---------------|-------------|---------| | Public | Low | Blog posts, FAQs, product docs | | Internal | Medium | Wiki pages, onboarding materials | | Confidential | High | HR records, employee data | | Restricted | Critical | Financial reports, legal contracts | --- ## 12. Grounded Answer Synthesis Natural language question answering with policy-aware source citations. Answers are synthesized exclusively from chunks allowed by active policies — denied chunks are never included in the LLM context. ### Execute Answer Synthesis ```python result = client.answers.execute( query="What's our quarterly revenue projection?", principal_id="user_123", connector_id="conn_abc", top_k=5, # max chunks for context (default: 5) search_mode="hybrid", # optional: vector (default), keyword, or hybrid alpha=0.5, # optional: hybrid weight (1.0=all-vector, 0.0=all-keyword) ) print(result.answer) # Synthesized answer text (or None) print(result.outcome) # "answered", "no_access", or "insufficient_context" for cit in result.citations: print(f" [{cit.index}] {cit.text_excerpt} (score: {cit.score})") ``` ### Response Structure - `answer` — Synthesized text, or `None` if outcome is `"no_access"` or `"insufficient_context"` - `outcome` — `"answered"` (synthesis succeeded), `"no_access"` (no allowed chunks with text), `"insufficient_context"` (model abstained) - `is_partial` — `true` when the model answered but noted incomplete coverage; `false` for full answers and abstentions - `citations` — List of `Citation` objects with `index`, `resource_id`, `vector_id`, `text_excerpt`, `score` - `retrieval_id` — Links to the underlying retrieval audit record - `allowed_chunks` / `denied_chunks` — Chunk counts after policy evaluation - `model` — LLM model used (default: `gpt-4o-mini`, configurable via `GATECO_ANSWER_MODEL`) - `GATECO_ANSWER_TEMPERATURE` — Env override for LLM temperature (0.0–1.0, default 0.1) - `retrieval_latency_ms` / `synthesis_latency_ms` / `total_latency_ms` — Timing breakdown - `cap_reached` — `true` when the response has reached the admin-configured query cap on the org's own key (synthesis still succeeds; rotate the key to reset the counter) ### LLM Key Model (BYOK) Answer synthesis calls the OpenAI API. The key is resolved in this order: 1. **Org's own key** — configured via `PATCH /api/organization/settings` with `llm_api_key`. Stored encrypted (AES-256-GCM, KMS v2 envelope with `{"organization_id": ...}` binding). Each call increments `llm_key_uses`; an optional `llm_key_query_cap` triggers a soft rotation reminder (`cap_reached: true`) without blocking synthesis. 2. **Gateco shared fallback** (paid tiers only — `team`, `growth`, `enterprise`) — up to **100 lifetime calls** on the server-side `OPENAI_API_KEY` env var. After 100 calls, the endpoint returns `422 LLM_CREDIT_EXHAUSTED`. 3. **Free tier** — no fallback available. Returns `422 LLM_KEY_NOT_CONFIGURED` immediately. Must configure own key. Setting a new key via PATCH resets `llm_key_uses` to 0. `GET /api/organization/settings` returns `llm_fallback_credits_used`, `llm_fallback_credits_limit` (100), `llm_key_uses`, `llm_key_query_cap`, and `llm_fallback_available` for display in org dashboards. ### CLI ```bash gateco ask "What was the CBS wire amount?" --connector-id --principal-id ``` ### Interactive REPL ``` gateco query > ask What's our quarterly revenue? ``` --- ## 13. Access Simulator Dry-run policy evaluation to test what a principal can access before going live. Available on Growth plan and above. ```python result = client.simulator.run( principal_id="user_123", connector_id="conn_abc", ) print(f"Matched: {result.matched_count}") print(f"Allowed: {result.allowed_count}") print(f"Denied: {result.denied_count}") for trace in result.traces: print(f" {trace.resource_id}: {trace.decision} — {trace.reason}") ``` --- ## 14. Audit Trail Every operation is logged with 50+ audit event types covering retrievals, policy changes, connector operations, ingestion, and more. ### Query Events ```python page = client.audit.list( event_types="retrieval_allowed,retrieval_denied", per_page=50, ) for event in page.items: print(f"{event.event_type} by {event.actor} at {event.created_at}") ``` ### Export (Growth+) ```python export = client.audit.export_csv( date_from="2025-01-01", date_to="2025-03-31", format="csv", ) ``` ### Event Categories - **User** — login, logout, settings_changed - **Connector** — added, updated, tested, removed, sync events - **Policy** — created, updated, activated, archived, deleted - **Retrieval** — allowed, denied (with full policy trace) - **Data** — metadata_bound, document_ingested, batch_ingested, resource_chunks_pruned, ingestion_limit_exceeded, resource_deleted - **Ingestion Jobs** — ingestion_job_enqueued, ingestion_job_completed, ingestion_job_failed - **Source Sync** — source_sync_started, source_sync_completed, source_sync_failed, acl_imported - **Identity Provider** — added, updated, removed, synced - **Pipeline** — created, updated, run, error - **Onboarding** — onboarding_dismissed - **Simulator** — simulator_preview_executed - **SCIM** — scim_user_created, scim_user_updated, scim_user_deactivated, scim_token_generated, scim_token_revoked, scim_group_created, scim_group_updated, scim_group_deleted - **Policy Suggestions** — policy_suggestions_generated, policy_suggestion_applied --- ## 15. Pagination All list endpoints return paginated responses in an envelope: ```json { "data": [...], "page": 1, "per_page": 20, "total": 100 } ``` Query parameters: `page` (default 1), `per_page` (default 20, max 100). ### Python — iterate all pages ```python page = client.connectors.list(page=1, per_page=50) # Access page.items, page.total, page.page, page.per_page ``` ### TypeScript — iterate all pages ```typescript const page = await client.connectors.list({ page: 1, perPage: 50 }); // Access page.items, page.total, page.page, page.perPage ``` --- ## 16. Error Handling ### HTTP Error Codes | Code | Meaning | |------|---------| | 400 | Bad request — validation failed | | 401 | Unauthorized — invalid or expired token | | 403 | Forbidden — insufficient permissions or plan entitlement | | 404 | Not found | | 409 | Conflict — duplicate resource | | 422 | Validation error — request body schema mismatch | | 429 | Rate limited — too many requests | | 500 | Internal server error | ### SDK Error Hierarchy Both SDKs raise typed errors. All inherit from `GatecoError`, which carries `code`, `message`, and `status_code` (`statusCode` in TypeScript). | Class | Status | Raised when | |-------|--------|-------------| | `GatecoError` | any | Base class; also used for unmapped codes and 5xx | | `AuthenticationError` | 401 | Missing, expired, or invalid credentials | | `AuthorizationError` | 403 | Authenticated but lacks permission (`AUTH_PERMISSION_DENIED`), or the retrieval principal is not active (`PRINCIPAL_NOT_ACTIVE`: retrieval/filter/answers requests naming an inactive or suspended principal are denied fail-closed), or the end-user token check failed (`SUBJECT_TOKEN_REQUIRED`, `SUBJECT_TOKEN_ISSUER_UNKNOWN`, `SUBJECT_TOKEN_INVALID`, `SUBJECT_NOT_RESOLVED`, `SUBJECT_MISMATCH`; see Verified End-User Subject) | | `EntitlementError` | 403 | Plan does not allow the action (`ENTITLEMENT_REQUIRED`) | | `NotFoundError` | 404 | Resource does not exist | | `ConflictError` | 409 | Duplicate resource | | `ValidationError` | 422 | Request body schema mismatch | | `LlmCreditExhaustedError` | 422 | 100 paid-tier fallback synthesis calls used (`LLM_CREDIT_EXHAUSTED`) | | `LlmKeyNotConfiguredError` | 422 | Answer synthesis on free tier with no OpenAI key (`LLM_KEY_NOT_CONFIGURED`) | | `RateLimitError` | 429 | Too many requests; carries `retry_after` / `retryAfter` | ### EntitlementError: distinguish a missing feature from a full quota A 403 `ENTITLEMENT_REQUIRED` covers two materially different situations, and **both carry `upgrade_to`**. Use `reason` — never parse the message: - `reason == "feature_not_in_plan"` — the plan does not grant the feature. Upgrading to `upgrade_to` is the only remedy. - `reason == "resource_limit_reached"` — the plan *does* grant the feature, but the org has hit its quota (e.g. 3/3 connectors on Team). Deleting an unused resource also resolves it, so advising an upgrade would be wrong. Convenience accessors: `err.is_limit` / `err.is_feature_gate` (Python), `err.isLimit` / `err.isFeatureGate` (TypeScript). When `reason` is absent (older servers), both treat the error as a feature gate, matching the previous behaviour. ```python from gateco_sdk.errors import EntitlementError try: client.connectors.create(name="prod", type="pgvector", config={...}) except EntitlementError as e: if e.is_limit: print(f"Quota full: {e.message}. Delete one, or upgrade to {e.upgrade_to}.") else: print(f"Not on your plan. Upgrade to {e.upgrade_to}.") ``` Note: error classes live in the `gateco_sdk.errors` submodule; they are not re-exported at the top level (only `GatecoClient` / `AsyncGatecoClient` are). ### Auto-Retry Both SDKs automatically retry on 429 and 5xx with exponential backoff (configurable via `max_retries` and `retry_backoff_factor`). ### Error Response Format Errors are returned in an `error` envelope: ```json { "error": { "code": "ENTITLEMENT_REQUIRED", "message": "Plan limit reached: 3/3 connectors", "upgrade_to": "growth", "reason": "resource_limit_reached" } } ``` `upgrade_to` appears only on `ENTITLEMENT_REQUIRED`; `reason` only on `ENTITLEMENT_REQUIRED`; `retry_after_seconds` only on `RATE_LIMIT_EXCEEDED`. SCIM endpoints (`/api/scim/v2/*`) instead return RFC 7644 error bodies. --- ## 17. Rate Limits & Plan Tiers | Resource | Free | Team | Growth | Enterprise | |----------|------|------|--------|------------| | Secured retrievals/mo | 1,000 | 50,000 | 500,000 | Unlimited | | Connectors | 1 | 3 | 10 | Unlimited | | Identity providers | 1 | 1 | 5 | Unlimited | | Policies | 3 | Unlimited | Unlimited | Unlimited | | Team members | 1 | 5 | 25 | Unlimited | | ABAC / ReBAC policies | No | Yes | Yes | Yes | | Grounded Answers | No | Yes | Yes | Yes | | Access Simulator | No | No | Yes | Yes | | Audit export (CSV/JSON) | No | No | Yes | Yes | | SSO / SCIM | No | No | Yes | Yes | | MCP server | Yes | Yes | Yes | Yes | | Ask AI in-product guide | Yes | Yes | Yes | Yes | | SIEM integration | No | No | No | Yes | | Custom KMS | No | No | No | Yes | | Private Data Plane | No | No | No | Roadmap (not available today) | **Per-endpoint rate limits (per org per minute, all plans):** | Endpoint | Limit | Relaxed (DEBUG=true) | |----------|-------|----------------------| | `POST /api/simulator/preview` | 10/min | 100/min | | `POST /api/retrievals/execute` | 60/min | 600/min | | `POST /api/answers/execute` | 20/min | 200/min | | `POST /api/help/ask` | 20/min | 200/min | Ask AI also has a durable daily ceiling per organization (50 questions on Free, 200 on paid plans), reset at midnight UTC. It runs on Gateco's own model key, so it never consumes your Grounded Answers credits or your own LLM key. Exceeded limits return HTTP 429. Limits are org-scoped (not user-scoped). The current implementation is an in-memory limiter — it resets on process restart and is not shared across multiple worker processes. **Pricing:** Free ($0/mo), Team ($499/mo), Growth ($2,000/mo), Enterprise (custom pricing, typically from $48K/yr). Annual billing saves ~17% on Team and Growth plans. --- ## 18. API Endpoints | Method | Path | Description | |--------|------|-------------| | POST | /api/auth/login | Authenticate and get JWT tokens | | POST | /api/auth/signup | Create account and organization | | POST | /api/auth/refresh | Refresh access token | | POST | /api/auth/logout | Invalidate session | | POST | /api/auth/exchange | Exchange OAuth code for tokens | | GET | /api/auth/google | Initiate Google OAuth | | GET | /api/auth/github | Initiate GitHub OAuth | | GET | /api/users/me | Get current user profile | | PATCH | /api/users/me | Update current user profile | | GET | /api/team/invites | List pending team invites | | POST | /api/team/invites | Invite a team member (counts toward the team_members limit) | | DELETE | /api/team/invites/:id | Revoke a pending invite | | GET | /api/organization/settings | Get org settings (includes llm_api_key_configured, llm_fallback_credits_used, llm_key_uses, llm_key_query_cap, llm_fallback_available). **org_admin only.** | | PATCH | /api/organization/settings | Update org name / failure_mode / llm_api_key / llm_provider / llm_key_query_cap / clear_llm_api_key. **org_admin only.** | | POST | /api/billing/sync-subscription | Re-sync org plan from Stripe (resolves post-checkout webhook limbo) | | GET | /api/capabilities/connectors | Connector capability matrix (search modes, ingestion tier, metadata modes) | | GET | /api/connectors | List connectors | | POST | /api/connectors | Create connector | | GET | /api/connectors/:id | Get connector | | PATCH | /api/connectors/:id | Update connector | | DELETE | /api/connectors/:id | Delete connector | | POST | /api/connectors/:id/test | Test connector connectivity | | POST | /api/connectors/:id/bind | Bind metadata to resources | | GET | /api/connectors/:id/coverage | Get metadata coverage | | GET | /api/connectors/:id/search-config | Get search configuration | | PATCH | /api/connectors/:id/search-config | Update search configuration | | GET | /api/connectors/:id/ingestion-config | Get ingestion configuration | | PATCH | /api/connectors/:id/ingestion-config | Update ingestion configuration | | PATCH | /api/connectors/:id/embedding-profile | Declare the query-embedding profile (provider/model/dimensions/base_url) for a bring-your-own vector DB whose vectors were not ingested through Gateco. Write-once. | | POST | /api/connectors/:id/suggest-classifications | Get classification suggestions | | POST | /api/connectors/:id/apply-suggestions | Apply approved suggestions | | GET | /api/connectors/:id/db-schema | Introspect live DB schema — tables with vector columns (Postgres-family, connected only) | | GET | /api/connectors/:id/preflight | Production readiness check: 6 blocking checks + 1 advisory. Returns PreflightResult with ready_for_production flag and recommendation string | | GET | /api/policies | List policies | | POST | /api/policies | Create policy | | GET | /api/policies/:id | Get policy | | PATCH | /api/policies/:id | Update policy | | DELETE | /api/policies/:id | Delete policy | | GET | /api/policies/:id/versions | List policy versions (Team+, policy_versioning) | | POST | /api/policies/:id/versions/:version/restore | Restore a prior policy version (Team+) | | POST | /api/policies/:id/activate | Activate policy | | POST | /api/policies/:id/archive | Archive policy | | GET | /api/policies/templates | List 7 built-in policy templates (all plans) | | POST | /api/policies/from-template | Create draft policy from template (Team+) | | POST | /api/answers/execute | Synthesize grounded answer from allowed chunks (rate-limited: 20/min per org) | | POST | /api/retrievals/execute | Execute secured retrieval (rate-limited: 60/min per org) | | POST | /api/retrievals/filter | Apply policy filtering to external candidates | | GET | /api/retrievals | List past retrievals | | GET | /api/retrievals/:id | Get retrieval detail | | POST | /api/v1/ingest | Ingest single document | | POST | /api/v1/ingest/batch | Batch ingest documents (Team+, batch_ingestion) | | POST | /api/v1/ingest/file | Upload one file (multipart): self-hosted extraction, then chunk/embed/register | | POST | /api/v1/ingest/files | Upload up to 20 files with per-file results (Team+, batch_ingestion) | Both SDKs upload files: Python `await client.ingest.file(connector_id, "/path/report.pdf", classification="internal")` and `client.ingest.files(connector_id, [paths])`; TypeScript (1.10.0+) `await client.ingest.file(connectorId, blob, "report.pdf", { classification: "internal" })` and `client.ingest.files(connectorId, [{ file, filename }])` over native `fetch` multipart. The Python sync client `GatecoClient` exposes every method the async client has (enforced by the SDK's own test suite), and both clients accept `access_token` / `refresh_token` as well as `api_key`. | POST | /api/v1/retroactive-register | Register existing vectors | | GET | /api/identity-providers | List identity providers | | POST | /api/identity-providers | Create identity provider | | GET | /api/identity-providers/:id | Get identity provider | | PATCH | /api/identity-providers/:id | Update identity provider | | DELETE | /api/identity-providers/:id | Delete identity provider | | POST | /api/identity-providers/:id/sync | Sync principals from provider | | GET | /api/principals | List principals (optional status/search/group filters; default active-only) | | GET | /api/principals/:id | Get principal | | POST | /api/principals/resolve | Resolve principal by email or provider_subject | | POST | /api/principals | Create a principal in the org's built-in local directory (all plans; limit Free 10 / Team 100 / Growth+ unlimited) | | PATCH | /api/principals/:id | Update a local principal (display_name, groups, roles, attributes, status); synced principals return 422 | | DELETE | /api/principals/:id | Deactivate a local principal (status -> inactive, never a hard delete) | | GET | /api/groups | List groups, IdP-synced and local directory, with live member counts (optional search). User session or API key with the `retrieve` scope (since 2026-10-02) | | GET | /api/data-catalog | List gated resources | | GET | /api/data-catalog/:id | Get resource detail | | PATCH | /api/data-catalog/:id | Update resource metadata | | PATCH | /api/v1/resources/:id/metadata | Update classification/sensitivity/domain/labels on an ingested resource | | POST | /api/v1/resources/bulk-classify | Set classification/sensitivity/domain/labels on many resources at once (by resource_ids or source_connector_id) | | GET | /api/pipelines | List pipelines | | POST | /api/pipelines | Create pipeline | | GET | /api/pipelines/:id | Get pipeline | | PATCH | /api/pipelines/:id | Update pipeline | | POST | /api/pipelines/:id/run | Run pipeline: enqueues a source-sync job group tied to a PipelineRun (422 PIPELINE_HAS_NO_SOURCE without an attached source) | | POST | /api/v1/ingest/jobs | Enqueue async ingestion job, 202 (Team+, async_ingestion) | | GET | /api/v1/ingest/jobs | List ingestion jobs (status filter, newest first) | | GET | /api/v1/ingest/jobs/:id | Ingestion job status + progress | | POST | /api/v1/ingest/jobs/:id/cancel | Cancel a queued job (running jobs return 409) | | DELETE | /api/v1/ingest/resources/:external_resource_id | Tombstone an ingested resource: vectors + registry + soft delete | | POST | /api/source-connections | Create a document source (Growth+, source_connectors) | | GET | /api/source-connections | List source connections (secrets masked) | | GET | /api/source-connections/:id | Get one source connection | | DELETE | /api/source-connections/:id | Soft-delete a source connection | | POST | /api/source-connections/:id/test | Run the adapter connectivity check | | GET | /api/source-connections/:id/acl-coverage | Imported-ACL coverage report (matched/unmatched principals) | | GET | /api/pipelines/:id/runs | List pipeline runs | | POST | /api/simulator/run | Run access simulation (Growth+) | | POST | /api/simulator/preview | Live preview with allowed/denied split (Growth+, rate-limited: 10/min per org) | | POST | /api/simulator/preview-batch | Batch preview for up to 5 principals (Growth+) | | GET | /api/benchmark/measured-overhead | Policy-layer overhead recomputed from your own retrievals (all plans, session auth) | | POST | /api/benchmark/policy-overhead | Live policy-engine self-test over a fixed candidate set (all plans, session auth, rate-limited: 6/min per org) | | GET | /api/audit-log | List audit events | | POST | /api/audit-log/export | Export audit log (Growth+) | | GET | /api/audit-log/stream | SIEM streaming export (Enterprise, siem_export): reserved, returns NOT_IMPLEMENTED in v1 | | GET | /api/plans | List available plans | | GET | /api/billing/usage | Current period usage: a `{used, limit}` pair for every plan-limited resource (secured_retrievals, connectors, identity_providers, policies, principals, api_keys, team_members), counted exactly as the entitlement gate counts them | | GET | /api/billing/subscription | Get subscription details | | GET | /api/billing/invoices | List invoices | | POST | /api/billing/portal | Create Stripe billing portal | | POST | /api/checkout/start | Start plan checkout | | POST | /api/webhooks/stripe | Stripe webhook handler (no JWT, signature-verified) | | GET | /api/dashboard/stats | Get dashboard statistics | | GET | /api/dashboard/activation | Activation funnel metrics: ActivationFunnelStats with funnel steps, 30-day retrieval counts, and p95 latency (all plans) | | GET | /api/onboarding/status | Computed onboarding checklist (6 steps) | | POST | /api/onboarding/dismiss | Dismiss onboarding wizard | | POST | /api/onboarding/profile | Save signup onboarding profile answers | | POST | /api/identity-providers/:id/suggest-policies | Generate policy suggestions from IDP data (Growth+) | | POST | /api/identity-providers/:id/apply-policy-suggestions | Apply accepted suggestions as draft policies (Growth+) | | POST | /api/identity-providers/:id/scim-token | Generate SCIM bearer token (Growth+) — plaintext returned once; token stored as bcrypt hash with 8-char prefix for fast lookup | | DELETE | /api/identity-providers/:id/scim-token | Revoke SCIM token (Growth+) | | GET | /api/scim/v2/ServiceProviderConfig | SCIM discovery: provider capabilities (bearer auth) | | GET | /api/scim/v2/ResourceTypes | SCIM discovery: resource types (bearer auth) | | GET | /api/scim/v2/Schemas | SCIM discovery: schemas (bearer auth) | | GET | /api/scim/v2/Users | List SCIM Users (Growth+, bearer token auth) | | POST | /api/scim/v2/Users | Create SCIM User (Growth+) | | GET | /api/scim/v2/Users/:id | Get SCIM User (Growth+) | | PUT | /api/scim/v2/Users/:id | Replace SCIM User (Growth+) | | PATCH | /api/scim/v2/Users/:id | Patch SCIM User (Growth+) | | DELETE | /api/scim/v2/Users/:id | Deactivate SCIM User (Growth+) | | GET | /api/scim/v2/Groups | List SCIM Groups (Growth+, bearer token auth) | | POST | /api/scim/v2/Groups | Create SCIM Group (Growth+) | | GET | /api/scim/v2/Groups/:id | Get SCIM Group with members (Growth+) | | PUT | /api/scim/v2/Groups/:id | Replace SCIM Group (Growth+) | | PATCH | /api/scim/v2/Groups/:id | Patch SCIM Group (Growth+) | | DELETE | /api/scim/v2/Groups/:id | Delete SCIM Group (Growth+) | | POST | /api/api-keys | Create API key (returns plaintext once) | | GET | /api/api-keys | List API keys (prefix + name + last_used, no plaintext) | | DELETE | /api/api-keys/:id | Revoke API key | | POST | /api/api-keys/:id/rotate | Rotate API key (new plaintext returned once; old key keeps working during a grace period) | | POST | /api/relationships | Create 1-hop REBAC relation | | GET | /api/relationships | List relations (filter by subject_id, relation, object_id) | | DELETE | /api/relationships/:id | Delete relation | --- ## 19. API Key Authentication API keys are the machine credential. Since 2026-09-01 (SDK 1.9.0, plan Phase 2) every key carries explicit **scopes**, chosen at creation and required: | Scope | Unlocks | |-------|---------| | `retrieve` | `POST /api/retrievals/execute`, `POST /api/retrievals/filter`, `POST /api/answers/execute`, `GET /api/principals`, `POST /api/principals/resolve`, `GET /api/principals/:id`, `GET /api/groups` (since 2026-10-02), `GET /api/connectors` (minimal shape: id, name, type, status) | | `ingest` | `/api/v1/ingest`, `/batch`, `/file`, `/files`, `DELETE /resources/:id`, `/api/v1/ingest/jobs*` | | `relationships` | `POST/GET/DELETE /api/relationships` | | `principals` | `POST/PATCH/DELETE /api/principals` (local directory) | | any key | `GET /api/users/me` (returns an `auth` block: kind, key_id, key_name, scopes) | Everything else (policies, connectors CRUD, identity providers, API-key management, audit, retrieval history, billing) requires a user session (JWT). A key sent there gets `401 AUTH_JWT_REQUIRED`. A key sent to a scoped route without the needed scope gets `403 API_KEY_SCOPE_MISSING`; the key is valid, the scope is not there. A request that carries no credential header at all (no `Authorization`, no `X-API-Key`, no `X-Admin-Token`) is refused `401 AUTH_REQUIRED` before routing, on every path that is not intentionally public, unknown paths included. An unexpected server error answers in the same envelope as every other error: `500 INTERNAL_ERROR` with a `request_id` to quote in a support ticket; the response is never a bare text body. Failure behavior at retrieval time is deny-or-error, never content: a policy evaluation error denies every candidate (deny-by-default, `failure_mode=closed`; Enterprise can opt into audited fail-open); an unreachable connector or vector database answers a 4xx error envelope with no results; the identity provider is never called during retrieval (policy is evaluated against the synced principal record), so an IdP outage stalls sync rather than opening access, and a deactivated principal is denied on the next retrieval. Nothing in this matrix returns content on a dead dependency. Format: `gck_<8 hex>_`; header `X-API-Key`. Keys are available on every plan (limit: Free 2 / Team 10 / Growth 25 / Enterprise unlimited) and are the audit actor for what they do (`actor_type=api_key`, `api_key_id`), never the human who created them. Rotation preserves scopes. `last_used_at` updates on every authenticated call. Keys created before scopes existed were migrated to `ingest` + `relationships`; `retrieve` is always an explicit opt-in. A RAG service needs a `retrieve` key. An ingestion pipeline needs `ingest`. A provisioning job that creates local users needs `principals`. Create keys under Settings > API Keys, or with `client.api_keys.create(name, scopes=[...])`. ## 20. Relationship-Based Access Control (REBAC) REBAC enables policy conditions based on named direct relations between principals and resources. Gateco supports 1-hop direct relations only — no graph traversal. ### Create and Use Relations ```python # Create a relation client.relationships.create( subject_principal_id="principal-uuid", relation_name="owner_of", object_resource_id="resource-uuid", ) # List relations for a principal page = client.relationships.list(subject_id="principal-uuid", relation="owner_of") # Delete a relation client.relationships.delete(relationship_id="rel-uuid") ``` ### REBAC Policy Condition ```python policy = client.policies.create( name="Owner Access", type="rebac", effect="allow", rules=[{ "effect": "allow", "priority": 1, "conditions": [ {"field": "relation.owner_of", "operator": "eq", "value": True}, ], }], ) ``` The `relation.` field prefix checks for a direct 1-hop Relationship row matching `(principal_id, relation_name, resource_id)`. ### Internal Implementation Notes - `has_relation(session, org_id, subject_id, relation_name, object_id) -> bool` — single indexed DB query - 60-second TTL LRU cache per `(org_id, subject_id, relation_name, object_id)` tuple - `invalidate_cache(org_id)` — call after any Relationship mutation to flush stale cache entries - REBAC conditions require `async_evaluate_policies()` — the sync `evaluate_policies()` does not support DB lookups - In the ingestion path (`_evaluate_principal_conditions`), `relation.*` conditions always return `False` — no resource context is available during ingestion ### Endpoints ``` POST /api/relationships — create relation GET /api/relationships?subject_id=&relation=&object_id= — list with filters DELETE /api/relationships/{id} — delete relation ``` --- ## 21. Failure Modes The `failure_mode` setting controls what happens when a policy evaluation error occurs (e.g., DB timeout, rule parse error) during a retrieval. | Mode | Behavior on eval error | |------|------------------------| | `closed` (default) | Deny the retrieval. Audit record includes `[policy_eval_error=... failure_mode=closed]`. | | `open_with_audit` | Allow the retrieval. Audit record includes a red-flag entry. Enterprise only, via support ticket. | The `failure_mode` column is on the `organizations` table (added in migration `027_add_org_failure_mode.py`). Default is `closed` — fail-closed is the safe production default. Use `open_with_audit` only in environments where availability takes precedence over strict access control. --- ## 22. Performance Gateco's policy enforcement pipeline is designed to add minimal overhead to vector database retrievals. | Metric | Value | |--------|-------| | p95 retrieval overhead | 21ms measured over the raw vector DB call (five published runs 13.6-26.2ms; gateco.ai/benchmarks) | | Policy compilation cache TTL | 60s per-org | | Negative-result cache TTL | 30s per-query (denied queries) | | REBAC `has_relation()` cache TTL | 60s per `(org_id, subject_id, relation_name, object_id)` | **Policy compilation cache:** Active policies for an organization are compiled once and cached for 60 seconds. Policy mutations (create, update, activate, archive) invalidate the cache immediately. **Negative-result cache:** Repeated denied queries from the same principal against the same connector use a short-lived negative cache (30s TTL) to avoid redundant policy evaluation for known-denied requests. **REBAC cache:** `has_relation()` results are cached at the tuple level. Call `invalidate_cache(org_id)` after any Relationship mutation to flush stale entries before the TTL expires. --- ## 23. Competitive Positioning ### Gateco vs Cerbos Cerbos is a generic policy engine (open-source, Apache 2.0) that solves application-layer authorization via Cedar/Rego-style policies. It is **engine-shaped** — it evaluates authorization decisions for any resource type but requires the caller to supply all principal and resource attributes. Gateco is **product-shaped** — it solves the RAG retrieval authorization problem specifically, with 12 vector DB connectors built in, deny-by-default retrieval semantics, and a classification layer. **Choose Cerbos when:** you need generic enterprise authz across many resource types, require 6-language SDK coverage, want GitOps-driven policy distribution, or need on-premises deployment today. **Choose Gateco when:** you are building a RAG pipeline and need multi-vector-DB coverage, classification suggestions, MCP server integration, audit-ready denial logging, and EU AI Act evidence packs out of the box. **Can you use both?** Yes — Cerbos for application-layer authorization (who can submit a ticket, who can view a report), Gateco for retrieval-layer authorization (what knowledge the AI is allowed to retrieve on behalf of a user). ### Gateco vs pgvector RLS PostgreSQL Row-Level Security is the most common DIY pattern for single-DB tenants. It works well for simple tenant isolation but hits a ceiling at scale: cross-table principal attribute joins become fragile, RLS policies don't propagate to other vector databases (Pinecone, Qdrant, Weaviate, etc.), there is no built-in IDP sync or SCIM provisioning, and the audit trail requires custom instrumentation. **pgvector RLS is sufficient when:** you have a single Postgres/pgvector deployment, all principal data lives in the same database, you have no SSO/SCIM requirements, and you have no compliance deliverables requiring a structured audit trail. **You outgrow pgvector RLS when:** you add a second vector DB, need to enforce classifications on chunk-level data, require identity sync from an external IDP (Okta, Azure Entra ID, AWS IAM), or need to produce EU AI Act or SOC 2 CC6 evidence. ### Gateco vs Microsoft Purview Microsoft Purview secures Microsoft 365 content — SharePoint, OneDrive, Teams, and Copilot for M365. It is the right tool if your AI is Microsoft Copilot and your data lives in M365. Gateco secures the AI **you are building** — custom RAG pipelines that query any of 12 vector DBs, regardless of cloud provider. **Key differences:** Purview is Microsoft-ecosystem-only (requires E5 licensing + Copilot SKUs). Gateco is cloud-agnostic and vector-DB-agnostic. Purview enforces document-level sensitivity labels; Gateco enforces at retrieval time with principal-aware ABAC/REBAC policies. Both produce audit trails; only Gateco produces denial-per-retrieval audit records suitable for EU AI Act Article 12 compliance. Adjacent tools in the same category: AWS S3 Access Grants + Bedrock Guardrails (AWS-only), GCP Sensitive Data Protection + Vertex AI Search access controls (GCP-only). Gateco covers all three cloud footprints from a single control plane. --- ## 24. EU AI Act Mapping The EU AI Act enters enforcement for high-risk AI systems on **August 2, 2026** (Annex III categories: employment, credit, education, health, biometrics, critical infrastructure, law enforcement, migration, justice). ### Penalty tiers | Category | Maximum fine | |----------|-------------| | Prohibited practices (Article 5) | €35M or 7% of global annual turnover | | High-risk violations (Articles 9–15) | €15M or 3% of global annual turnover | | Provision of incorrect information | €7.5M or 1.5% of global annual turnover | ### Gateco capability ↔ EU AI Act obligation | AI Act Article | Obligation | Gateco Capability | |---------------|-----------|------------------| | Article 9 | Risk management system | Policy-as-code with versioning + Access Simulator dry runs | | Article 10 | Data governance | Resource classification + chunk-level metadata + sidecar labels | | Article 12 | Record-keeping and logs | 50+ audit event types, SIEM streaming, 90-day retention on Growth/Enterprise | | Article 13 | Transparency | Denial reasons surfaced in retrieval responses and audit trail | | Article 14 | Human oversight | Policy approval workflow, manual policy activation (never auto-activate) | | Article 15 | Accuracy and robustness | Fail-closed default, circuit breaker, retrieval SLO enforcement | Gateco does **not** replace an AI Act risk assessment, conformity assessment, or CE marking process. It provides the technical controls and audit evidence that those processes require.