Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@
| | |
|---|---|
| **Last updated** | 2026-06-08 |
| **Current phase** | Phase 6 — Governance & Tenancy (**6 / 10 steps**) |
| **Overall** | **70 / 84 steps** — Phases 0–5 complete |
| **Next action** | **Step 6.7dKey rotation**: zero-downtime KEK rotation — old keys retained decrypt-only until expiry, background re-encryption. Closes Step 6.7. 6.7a (library + local) + 6.7b (config/factory + AWS KMS) + 6.7c (GCP/Azure/Vault providers) shipped. |
| **Current phase** | Phase 6 — Governance & Tenancy (**7 / 10 steps**) |
| **Overall** | **71 / 84 steps** — Phases 0–5 complete |
| **Next action** | **Step 6.8SSO / SCIM**: OIDC + SAML IdP federation; SCIM 2.0 user provisioning; per-tenant IdP config. New ground (the gateway's `Auth` SPI + `NoopAuth` header-identity is the seam this builds on). |

**Recently shipped**

- **6.7c** 🚧 GCP/Azure/Vault KMS providers — `GcpKmsKeyManager` (google-cloud-kms), `AzureKeyVaultKeyManager` (`wrap_key`/`unwrap_key`), `VaultKeyManager` (Vault Transit) — each subclasses `EnvelopeKeyManager` behind a `[kms-*]` extra (lazy SDK, injectable client, fake-client unit-tested); factory + enum extended; completes all four KMS providers — [#157](https://github.com/officialCodeWork/AgentContextOS/pull/157)
- **6.7b** 🚧 Cloud KMS (AWS) + config/factory/wiring — `cfg.kms` (provider `noop`/`local`/`aws`) + `tenants[].kms_key_id`; `build_key_manager_from_config` factory + `app.state.key_manager` gateway seam; **`AwsKmsKeyManager`** (aioboto3, per-tenant CMK, KMS-error → sealing, fake-client unit-tested) — [#156](https://github.com/officialCodeWork/AgentContextOS/pull/156)
- **6.7a** 🚧 BYOK envelope encryption library — `KeyManager` SPI + `NoopKeyManager` + `EncryptingStorage` (rag-core); `EnvelopeKeyManager` (AES-256-GCM DEK + `tenant_id` AAD) + `LocalKeyManager` (in-process per-tenant KEK) (rag-backends); per-tenant isolation + **sealing** (`KeyUnavailableError`) + tamper detection; `ragctl kms` — [#155](https://github.com/officialCodeWork/AgentContextOS/pull/155)
- **6.7** ✅ BYOK envelope encryption — `KeyManager` SPI + `EncryptingStorage` + `EnvelopeKeyManager` (AES-256-GCM DEK + tenant AAD); `LocalKeyManager` + four cloud KMS providers (`Aws`/`Gcp`/`AzureKeyVault`/`Vault`, behind `[kms-*]` extras); `cfg.kms` + per-tenant key + factory; per-tenant isolation + sealing + tamper-evidence; zero-downtime rotation (`RotatingKeyManager` + `rewrap`); `ragctl kms` — [#155](https://github.com/officialCodeWork/AgentContextOS/pull/155)–[#158](https://github.com/officialCodeWork/AgentContextOS/pull/158)
- **6.6** ✅ Immutable audit log — **6.6a** shared `AuditWriter`/store on `app.state` + `GET /v1/audit` (tenant-scoped, `chain_verified`) + `GET /v1/audit/verify` (whole-log) + `cfg.audit.enabled` ([#153](https://github.com/officialCodeWork/AgentContextOS/pull/153)); **6.6b** `AuditExporter` self-verifying WORM bundle (SHA-256 `content_hash` + HMAC, offline `verify()`), `POST /v1/audit/export` (tenant-scoped) + `ragctl audit`, `cfg.audit.export_secret` ([#154](https://github.com/officialCodeWork/AgentContextOS/pull/154))
- **6.5** ✅ PII egress policies — `PiiPolicyEngine` (`rag-pii`) answers `egress_text` over the context (`list[Chunk]`) + agent answer (`str`) the gateway already passes, applying the per-tenant `pii_policy` (allow / redact / mask / block); reuses the Step 1.7 detector + rewriters; opt-in `cfg.pii.enabled`; emits `pii.egress_blocked` — [#152](https://github.com/officialCodeWork/AgentContextOS/pull/152)
- **6.4** ✅ ACL egress verifier — `AclEgressVerifier` re-checks returned `ChunkRef`s against the principal's labels at the gateway router boundary (defense-in-depth, same overlap semantics, independent of the PDP); `acl.egress_violation` event; red-team zero-violation-rate gate — [#151](https://github.com/officialCodeWork/AgentContextOS/pull/151)
Expand Down Expand Up @@ -63,9 +61,9 @@
| 3 | Gateway & Agent Runtime | 11 | **11** | 0 |
| 4 | Reliability | 6 | **6** | 0 |
| 5 | Eval & Observability | 7 | **7** | 0 |
| 6 | Governance & Tenancy | 10 | **6** | 4 |
| 6 | Governance & Tenancy | 10 | **7** | 3 |
| 7 | Pilot, Harden, GA | 10 | 0 | 10 |
| **Total** | | **84** | **70** | **14** |
| **Total** | | **84** | **71** | **13** |

---

Expand Down Expand Up @@ -658,7 +656,7 @@
| 6.4 | ACL egress verifier | ✅ | [#151](https://github.com/officialCodeWork/AgentContextOS/pull/151) — `AclEgressVerifier` re-checks returned chunks at the gateway router boundary (defense-in-depth above the 6.3 push-down); `acl.egress_violation`; zero-violation-rate red-team gate |
| 6.5 | PII policies | ✅ | [#152](https://github.com/officialCodeWork/AgentContextOS/pull/152) — `PiiPolicyEngine` egress_text decorator (allow / redact / mask / block per tenant) over answer + context; reuses Step 1.7 detector; `pii.egress_blocked` |
| 6.6 | Immutable audit log | ✅ | **6.6a** [#153](https://github.com/officialCodeWork/AgentContextOS/pull/153) — read API `GET /v1/audit` + `GET /v1/audit/verify` + shared store + `cfg.audit`. **6.6b** [#154](https://github.com/officialCodeWork/AgentContextOS/pull/154) — `AuditExporter` signed WORM bundle + `POST /v1/audit/export` + `ragctl audit` |
| 6.7 | BYOK (Bring Your Own Key) | 🚧 | **6.7a** ✅ [#155](https://github.com/officialCodeWork/AgentContextOS/pull/155) — `KeyManager` SPI + envelope encryption (`LocalKeyManager`) + `EncryptingStorage` + sealing/isolation/tamper. **6.7b** ✅ [#156](https://github.com/officialCodeWork/AgentContextOS/pull/156) — `cfg.kms` + per-tenant key + `build_key_manager_from_config` factory + `AwsKmsKeyManager`. **6.7c** ✅ [#157](https://github.com/officialCodeWork/AgentContextOS/pull/157) — `GcpKmsKeyManager` / `AzureKeyVaultKeyManager` / `VaultKeyManager` behind `[kms-*]` extras. **6.7d** ⏳ — key rotation |
| 6.7 | BYOK (Bring Your Own Key) | | **6.7a** ✅ [#155](https://github.com/officialCodeWork/AgentContextOS/pull/155) — `KeyManager` SPI + envelope encryption (`LocalKeyManager`) + `EncryptingStorage` + sealing/isolation/tamper. **6.7b** ✅ [#156](https://github.com/officialCodeWork/AgentContextOS/pull/156) — `cfg.kms` + per-tenant key + `build_key_manager_from_config` factory + `AwsKmsKeyManager`. **6.7c** ✅ [#157](https://github.com/officialCodeWork/AgentContextOS/pull/157) — `GcpKmsKeyManager` / `AzureKeyVaultKeyManager` / `VaultKeyManager` behind `[kms-*]` extras. **6.7d** ✅ [#158](https://github.com/officialCodeWork/AgentContextOS/pull/158) — `RotatingKeyManager` zero-downtime rotation + `rewrap` |
| 6.8 | SSO / SCIM | ⏳ | OIDC + SAML IdP federation; SCIM 2.0 user provisioning; per-tenant IdP config |
| 6.9 | Air-gapped install bundle | ⏳ | Signed tarball with all images + Helm chart; offline bootstrap; cosign verification |
| 6.10 | Compliance posture | ⏳ | SOC 2 Type II control mapping; GDPR data-residency config; data-retention policies |
Expand Down Expand Up @@ -754,6 +752,13 @@ New ground — the only prior crypto was HMAC signing. The V1 plan calls for en
- Each is **behind a `[kms-gcp]` / `[kms-azure]` / `[kms-vault]` extra** (the SDKs are lazy-imported, so the modules import without the SDK and selecting a provider without its extra raises a clear `ImportError`); an **injectable `client` / `client_factory` seam** makes every provider fully unit-testable with a fake KMS — no cloud creds / network. Connection uses each SDK's standard credential discovery (GCP ADC, Azure `DefaultAzureCredential`, Vault `VAULT_ADDR`/`VAULT_TOKEN`); per-tenant key resolution + sealing (`KeyUnavailableError`) are uniform with AWS
- `KmsProvider` enum + `build_key_manager_from_config` factory + `KmsConfig.vault_mount` extended. mypy overrides added for the three SDK module trees (absent at lint time). **Scope:** providers only — key rotation is **6.7d**; wiring `EncryptingStorage` into the ingest path stays deferred (tiered storage). ~18 new tests (each provider over a fake client: round-trip / cross-tenant block / sealing / SDK-error mapping; factory cloud-provider-requires-extra contract). `KmsConfig` → `rag.schema` regenerated; all gates green (ruff, mypy --strict 309 files, RAG001, policy-coverage, log-schema). [ADR-0039](docs/adr/ADR-0039-byok-envelope-encryption.md), [reference/encryption.md](docs/reference/encryption.md), [architecture/byok.md](docs/architecture/byok.md)

#### 6.7d — Zero-downtime key rotation ✅ [#158](https://github.com/officialCodeWork/AgentContextOS/pull/158)

- Closes Step 6.7. New **`RotatingKeyManager`** (`rag-core`, a **crypto-free** `KeyManager` decorator) makes rotating a tenant's KEK seamless: **encrypt** always uses the **current** key; **decrypt** tries the current key then each **non-expired** retired key (`RetiredKey(key_manager, expires_at)`). The try-all decrypt is **safe, not a guess** — AES-GCM authenticates the DEK, so a wrong KEK can't yield a valid key (only the KEK that wrapped a blob decrypts it)
- **`rewrap(ctx, ct)`** is the background-migration primitive (decrypt with whatever key still works → re-encrypt under the current key); once all blobs are migrated a retired key is dropped. An **expired** retired key is skipped, so old un-rewrapped data is **sealed** (`EncryptionError`) — "retain decrypt-only until expiry" — with no impact on current-key data. Injectable `clock` for deterministic expiry tests
- Composes with every provider (local + all four cloud KMS) since it orchestrates only the `KeyManager` SPI. `ragctl kms --rotate` demos the full flow (old + new decrypt, rewrap, expiry seals old data)
- **Scope:** the rotation *mechanism* (satisfies the planning rotation acceptance — rotate → old + new both decrypt; expired key seals old data). **Deferred:** config-driven per-tenant multi-generation rotation + the storage-side background re-encryption job land with the `EncryptingStorage` ingest wiring (tiered storage). ~11 new tests (rotate → old+new decrypt; rewrap migration; expired-key sealing; tamper; no-key sealing; multi-generation; clock-controlled expiry; health) + `ragctl kms --rotate`. No dist drift; all gates green (ruff, mypy --strict 310 files, RAG001, policy-coverage, log-schema). [ADR-0039](docs/adr/ADR-0039-byok-envelope-encryption.md), [reference/encryption.md](docs/reference/encryption.md), [architecture/byok.md](docs/architecture/byok.md)

---

## Phase 7 — Pilot, Harden, GA (Weeks 34–40) ⏳
Expand Down Expand Up @@ -915,6 +920,7 @@ Complete log of every PR. Routine Dependabot bumps are grouped; everything else
| [#155](https://github.com/officialCodeWork/AgentContextOS/pull/155) | 2026-06-08 | feat(crypto): BYOK envelope encryption library + local KMS (Step 6.7a) |
| [#156](https://github.com/officialCodeWork/AgentContextOS/pull/156) | 2026-06-08 | feat(crypto): cfg.kms + key-manager factory + AWS KMS provider (Step 6.7b) |
| [#157](https://github.com/officialCodeWork/AgentContextOS/pull/157) | 2026-06-08 | feat(crypto): GCP / Azure / Vault KMS providers (Step 6.7c) |
| [#158](https://github.com/officialCodeWork/AgentContextOS/pull/158) | 2026-06-08 | feat(crypto): zero-downtime key rotation — RotatingKeyManager (Step 6.7d) |
| #78–#80, #116–#118 | Open | Dependabot bumps — awaiting merge |
| #81 | Closed | Dependabot bump — superseded |

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
| [admin-ui.md](reference/admin-ui.md) | Admin console (Step 3.10) — Next.js 14 operator GUI (`apps/admin-ui`); 9 pages (dashboard, corpora, connectors, glossary, webhooks, audit, API keys, tenants, config), live-vs-seed hybrid + `NEXT_PUBLIC_GATEWAY_URL`, header identity, running it, internals (shell/primitives/data layer), extension points |
| [tenancy.md](reference/tenancy.md) | Logical multi-tenancy (Step 6.1) — per-tenant `rag.yaml` config (`namespace` / `acl_labels` / `pii_policy` / `quota`); `TenantResolver.resolve(id) → TenantSettings`; `RequestContext.namespace`; `GET /v1/status/tenant`; `ragctl tenant list` / `resolve`; config table + scope/boundaries (6.2/6.3/6.5) + extension points; physical tenancy (6.2), ACL push-down (6.3) + egress verifier (6.4 — `cfg.acl.verify_egress`) sections |
| [audit.md](reference/audit.md) | Audit log (Step 6.6) — `AuditEvent` / `AuditStore` (append / events / verify_chain) / `NoopAuditStore` SHA-256 hash chain / `AuditWriter` (+ `.store`); read API `GET /v1/audit` (tenant-scoped, `chain_verified`) + `GET /v1/audit/verify` (whole-log); WORM signed export (6.6b) — `AuditExporter` (content_hash + HMAC), `POST /v1/audit/export`, offline `verify()`, `ragctl audit`; `cfg.audit.enabled` / `export_secret`; durable-store extension points |
| [encryption.md](reference/encryption.md) | BYOK envelope encryption (Step 6.7) — `KeyManager` SPI + `NoopKeyManager`; `EnvelopeKeyManager` (AES-256-GCM DEK + `tenant_id` AAD) + `LocalKeyManager` + cloud providers **`AwsKmsKeyManager`** / **`GcpKmsKeyManager`** / **`AzureKeyVaultKeyManager`** / **`VaultKeyManager`** (behind `[kms-*]` extras); `EncryptingStorage` decorator; `EncryptionError` / `KeyUnavailableError` (sealing); provider table; `cfg.kms` + `tenants[].kms_key_id` + `build_key_manager_from_config` factory; `ragctl kms`; rotation deferred (6.7d) |
| [encryption.md](reference/encryption.md) | BYOK envelope encryption (Step 6.7) — `KeyManager` SPI + `NoopKeyManager`; `EnvelopeKeyManager` (AES-256-GCM DEK + `tenant_id` AAD) + `LocalKeyManager` + cloud providers **`AwsKmsKeyManager`** / **`GcpKmsKeyManager`** / **`AzureKeyVaultKeyManager`** / **`VaultKeyManager`** (behind `[kms-*]` extras); `EncryptingStorage` decorator; `EncryptionError` / `KeyUnavailableError` (sealing); provider table; `cfg.kms` + `tenants[].kms_key_id` + `build_key_manager_from_config` factory; **`RotatingKeyManager`** + `RetiredKey` + `rewrap` (6.7d zero-downtime rotation); `ragctl kms [--rotate]` |
| [webhooks.md](reference/webhooks.md) | Outbound webhooks (Step 3.9) — event catalogue (`ingest.completed` / `audit.policy_violation` / `drift.detected` / `eval.regression`), event envelope, HMAC signing + `verify()`, at-least-once delivery, `/v1/webhooks/subscriptions` CRUD + test, `rag.yaml` block, `ragctl webhooks demo`, internals + extension points |
| [integrations.md](reference/integrations.md) | Framework adapters (Step 3.8) — `agentcontextos.integrations.*` for LangChain / LlamaIndex / Haystack / DSPy / LangGraph / CrewAI / AutoGen / Semantic Kernel; per-framework extras, shared config + chunk metadata, usage per framework, internals + extension points |
| [status-api.md](reference/status-api.md) | Status & Metrics API (Step 3.11) — `/v1/status/health` / `metrics` / `logs` (+ SSE `logs/stream`), `WS /v1/status/ws`, `/v1/connectors/status`; metric catalogue + request-timing middleware, the `MetricsCollector` / `LogTail` read-side, CORS + query-param identity for browser streams, extension points |
Expand Down
11 changes: 9 additions & 2 deletions docs/adr/ADR-0039-byok-envelope-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,16 @@ where the real provider impls live. rag-core stays crypto-free (SPI + decorator
(`AzureKeyVaultKeyManager`, `wrap_key`/`unwrap_key`), **HashiCorp Vault Transit**
(`VaultKeyManager`) — each behind a `[kms-*]` extra (lazy SDK, injectable client,
fake-client unit-tested), completing all four KMS providers.
- **6.7d** added zero-downtime key rotation: `RotatingKeyManager` (a crypto-free
`KeyManager` decorator) encrypts with the current key and decrypts via the
current key then each **non-expired** retired key (`RetiredKey`), with `rewrap`
for background migration. The try-all decrypt is safe — AES-GCM authenticates
the DEK, so a wrong KEK never yields a valid key — and an expired retired key
seals old, un-rewrapped data. This satisfies the rotation acceptance criteria.
- Still deferred: wiring `EncryptingStorage` into the ingest/storage path (blocked
on tiered-storage plumbing — chunk content is inline today) and **6.7d**
zero-downtime key rotation (old keys retained decrypt-only until expiry).
on tiered-storage plumbing — chunk content is inline today) and config-driven
rotation (the `RotatingKeyManager` mechanism ships; per-tenant multi-generation
rotation config + the background re-encryption job land with that wiring).

## See also
- [architecture/byok.md](../architecture/byok.md) — envelope design, AAD, sealing, slicing
Expand Down
8 changes: 6 additions & 2 deletions docs/architecture/byok.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ asserts its reads fail while another tenant's succeed.
**Azure Key Vault** (`AzureKeyVaultKeyManager`, via `wrap_key`/`unwrap_key`),
**HashiCorp Vault Transit** (`VaultKeyManager`), each behind a `[kms-*]` extra
(lazy SDK, injectable client). Completes all four KMS providers.
- **6.7d** — zero-downtime key rotation (old keys retained decrypt-only until
expiry; background re-encryption).
- **6.7d** — zero-downtime key rotation: `RotatingKeyManager` (a crypto-free
`KeyManager` decorator) encrypts with the current key, decrypts via current +
non-expired retired keys, and `rewrap`s old blobs to the current key. The
try-all decrypt is safe (AES-GCM authenticates the DEK); an expired retired key
seals old data. Config-driven rotation + the storage re-encryption job land with
the `EncryptingStorage` ingest wiring.

## Boundary note

Expand Down
31 changes: 31 additions & 0 deletions docs/reference/encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@ missing key → `KeyUnavailableError` (sealing). A tenant with neither its own
`kms_key_id` nor `default_key_id` is sealed. The SDKs are lazy-imported, so
selecting a provider without its extra raises a clear `ImportError`.

### Key rotation (Step 6.7d)

`RotatingKeyManager` (`rag_core.rotating_key_manager`) wraps a **current**
`KeyManager` plus **retired** keys for zero-downtime KEK rotation:

```python
from datetime import UTC, datetime, timedelta
from rag_core.rotating_key_manager import RetiredKey, RotatingKeyManager

km = RotatingKeyManager(
current=new_key_manager,
retired=[RetiredKey(old_key_manager, expires_at=datetime.now(UTC) + timedelta(days=30))],
)
new_ct = await km.encrypt(ctx, data) # always the current key
plain = await km.decrypt(ctx, any_ct) # current, then non-expired retired keys
migrated = await km.rewrap(ctx, old_ct) # re-encrypt an old blob under current
```

- **encrypt** uses the current key; **decrypt** tries the current key then each
non-expired retired key. The try-all is *safe* (AES-GCM authenticates the DEK —
a wrong KEK can't yield a valid key, so it never returns wrong plaintext).
- An **expired** retired key is skipped, so old data not yet re-wrapped is sealed
(raises `EncryptionError`) — "retain decrypt-only until expiry".
- **rewrap** is the background-migration primitive: decrypt with whatever key
still works, re-encrypt under the current key; once all blobs are migrated the
retired key can be dropped. `ragctl kms --rotate` demos the full flow.

It is crypto-free (it orchestrates the `KeyManager` SPI), so it composes with the
local + every cloud provider. Config-driven rotation + the storage-side
re-encryption job land with the `EncryptingStorage` ingest wiring.

## Internals

- **Envelope wire format:** `b"RAGK" | version | u32(len(wrapped_dek)) |
Expand Down
Loading
Loading