All notable changes to DevBrain are tracked in this file. Versions follow Semantic Versioning.
A new EditTags tool lets callers adjust tag metadata on an existing document without re-emitting its content. Previously the only way to add or drop a tag was a full UpsertDocument round-trip that re-sent the entire body — wasteful for large documents whose content isn't changing.
EditTags(key, add?, remove?, project?)— applies a tag diff to an existing document.addandremoveare disjoint lists; a tag present in both is rejected. Already-present tags inaddare no-ops; absent tags inremoveare silently ignored (idempotent). Content is never touched — the call updatestags,updatedAt, andupdatedByonly. Emptyaddandremovereturns a "nothing to do" response without a write. A no-effective-change result (e.g. adding a tag that's already present) also short-circuits without a write.ITagEditService+TagEditService— encapsulates tag-diff computation, conflict detection, and the write decision. Mirrors theDocumentEditServicepattern so tag edits are unit-testable against a fake store without touching Cosmos.TagEditResult— structured response withFound,Changed,PreviousTags,Tags,Added,Removed,UpdatedAt,UpdatedBy, and a human-readableMessage.Added/Removedreflect the tags that actually changed, not the raw input lists — so idempotent no-ops are visible to the caller.
README.mdnow documents the newEditTagstool and the "edit tags without re-upserting" workflow.docs/seed/ref-devbrain-usage.mdnow teaches AI callers when and how to useEditTagsin place of a full upsert for tag-only changes.host.jsonMCPinstructionsupdated to mentionEditTags.host.jsonserverVersionandDevBrain.Functions.csproj<Version>bumped to1.9.0. (Note: both files had drifted at1.7.0through the 1.8.0 release — this bump catches them up.)
- Concurrency.
EditTagsuses a read-then-upsert path rather than a hash-guarded conditional write. A concurrent tag edit between the read and write could be clobbered. This matches the existingUpsertDocumentconcurrency posture and is acceptable for tag metadata in DevBrain's single-tenant deployment. If stronger guarantees are ever needed, the path can be promoted toReplaceIfHashMatchesAsyncwithout changing the tool surface. - Input normalization. Blank / whitespace-only tags in
addorremoveare dropped before diffing. Duplicates within a single list are deduplicated. Tag comparison is ordinal (case-sensitive) —"Draft"and"draft"are distinct tags.
Two new document-editing tools add a safe preview/apply workflow for exact text replacements. This keeps DevBrain's whole-document storage model intact while giving AI callers a deterministic way to edit without stale overwrites.
PreviewEditDocument(key, oldText, newText, expectedOccurrences?, caseSensitive?, project?)— previews a literal text replacement without writing. Returns match count, before/after snippets, and the currentcontentHashfor the caller to feed into apply.ApplyEditDocument(key, oldText, newText, expectedContentHash, expectedOccurrences?, caseSensitive?, project?)— applies the same literal replacement only if the stored document still matches the preview hash. Refuses ambiguous or stale edits.DocumentEditService— shared edit-planning/execution layer that finds literal matches, builds preview snippets, and computes the final whole-document replacement payload.ConditionalWriteResult+IDocumentStore.ReplaceIfHashMatchesAsync(...)— conditional write path used by apply to avoid overwriting a document that changed after preview.ContentHashinghelper — centralizes content-hash computation and normalization so compare/edit flows share the same hash semantics.
README.mdnow documents the new edit tools and the recommended preview/apply workflow.docs/seed/ref-devbrain-usage.mdnow teaches AI callers how to edit existing documents safely usingPreviewEditDocumentfollowed byApplyEditDocument.
Two new read-only tools that let callers check whether a document has changed without pulling the full content into context. Every write now stamps a SHA-256 contentHash and contentLength on the document, enabling cheap staleness checks and import-or-skip decisions.
GetDocumentMetadata(key, project?)— returns key, project, tags, updatedAt, updatedBy, contentHash, and contentLength without the content body. Use to check existence, size, and freshness without consuming tokens or Cosmos RU on the full document.CompareDocument(key, content | contentHash, project?)— compares candidate content against a stored document. Accepts either raw content (hashed server-side) or a precomputed SHA-256 hex hash. Returns{ found, match, storedContentHash, candidateHash, ... }. Use to decide whether an import or sync is needed before committing to a full upsert.contentHashandcontentLengthfields onBrainDocument. Computed server-side on every write (upsert, append, chunked finalize). Nullable — existing documents shownulluntil their next write, at which point the fields are populated automatically.- Content normalization for hashing. Before computing SHA-256, content is normalized: line endings are unified to
\n(ReplaceLineEndings) and trailing whitespace is trimmed. This ensures documents with\r\nvs\nline endings or trailing newline differences produce identical hashes. The stored content is never modified — normalization only affects hash input.
CosmosDocumentStore.UpsertAsyncnow computes and storescontentHash+contentLengthon every write. SinceAppendAsyncandUpsertChunkAsyncboth flow throughUpsertAsyncfor their final write, all write paths are covered with a single touch point.IDocumentStoregainsGetMetadataAsync(key, project)— a Cosmos projection query that excludesc.content, keeping RU cost and response size low.host.jsonMCPinstructionsupdated to mention the two new tools.host.jsonserverVersionbumped to1.7.0.
- Backward compatibility. Existing documents in Cosmos will have
contentHash: nullandcontentLength: nulluntil their next write.GetDocumentMetadatareturns these nulls gracefully.CompareDocumentagainst a pre-existing doc correctly reportsmatch: false(null != any hash), which is the safe default — it triggers a re-import that populates the hash. - Hash normalization is hash-only. The normalization (line-ending unification + trailing trim) is applied identically in both
CosmosDocumentStore.ComputeSha256andDocumentTools.ComputeSha256, so server-computed hashes and client-precomputed hashes match when the same normalization is applied. Callers providing a precomputedcontentHashshould apply the same normalization for consistent results.
Per-user OAuth replaces the function-key gate. DevBrain now acts as an RFC 7591 Dynamic Client Registration (DCR) facade in front of a single pre-registered Entra app, making it authenticatable from Claude Code CLI, Claude.ai web, VS Code, ChatGPT, and Cursor — all of which previously failed against Entra-direct MCP servers. Writes now record the real Entra UPN as updatedBy instead of "unknown".
v1.6 is a one-way deploy. There is no dual-mode feature flag and no rollback runbook. If a v1.6 deployment fails its acceptance checks, the recovery path is a v1.6.1 forward-fix, not redeploying v1.5.0. The v1.5.0 tag remains in the back pocket as a worst-case escape hatch but is no longer treated as a routine smoke-test target. This is intentional — single-tenant posture + small known user population means the blast radius is small and the cost of a dual-mode rollback test surface exceeds the cost of a forward-fix. DevBrain remains "deploy once, it's yours" single-tenant by design.
A tenant admin must create a single Entra app registration and wire its values through. Without this, the Function deploys successfully but every OAuth flow returns 500 at /callback.
- Create app registration
DevBrainin the target tenant. SetsignInAudience: AzureADMyOrg(single-tenant) — do NOT pick the multi-tenant option in the portal wizard. - Add redirect URI:
https://{function-host}/callback. - Expose API:
documents.readwritescope. - Set the
entraTenantIdandentraClientIdBicep parameters (e.g.azd env set ENTRA_TENANT_ID <guid>andazd env set ENTRA_CLIENT_ID <guid>). azd up(oraz deployment group create). Bicep provisions the Key Vault and theoauth_stateCosmos container.- Set the two Key Vault secrets manually — Bicep deliberately does not populate them:
az keyvault secret set --vault-name <kv> --name jwt-signing-secret --value $(openssl rand -base64 32)az keyvault secret set --vault-name <kv> --name entra-client-secret --value <secret from Entra app>
- Restart the Function app so it picks up the Key Vault references.
- DCR facade endpoints under
src/DevBrain.Functions/Auth/DcrFacade/:POST /register— RFC 7591 DCR. Returns an opaqueclient_idhandle backed by the pre-registered Entra app. 90-day TTL on the registration record.GET /authorize— validates the client, generates DevBrain's own upstream PKCE pair, persists anAuthTransaction, and redirects to Entra. S256-only; plain PKCE is rejected; redirect URIs match exactly.GET /callback— exchanges the Entra code with DevBrain's own PKCE verifier (never the client's), mints a pre-committed JTI, creates the upstream token vault record atupstream:{jti}, and redirects back to the client'sredirect_uriwith a DevBrain code.POST /token— handles bothauthorization_codeandrefresh_tokengrants. Atomic code redemption (single-take) and refresh token rotation (every use mints a new refresh and invalidates the old one).GET /.well-known/oauth-authorization-server+/.well-known/oauth-protected-resource— DevBrain-hosted discovery documents. Everything points at DevBrain itself, never at Entra — claude-ai-mcp issue #82 requires this.
McpJwtValidationMiddleware+JwtAuthenticator— the MCP webhook gate. ThewebhookAuthorizationLevelis nowanonymous; the JWT middleware is the sole authentication path for tool invocations. PopulatesFunctionContext.Features.Get<ClaimsPrincipal>()soDocumentTools.GetCallerIdentitycontinues to work with zero code changes — the existingpreferred_username/oidextraction now sees real values.- Single-tenant enforcement at the token layer. Every issued JWT carries the configured
tidclaim. The middleware validatestidagainstOAuth__EntraTenantIdbefore any Cosmos lookup, so cross-tenant tokens are rejected cheaply. This is a deliberate load-bearing decision — single-tenant is a permanent non-goal, not a current-phase simplification. - New Cosmos container
oauth_state(/key, TTL enabled) holding five record kinds:client:{id},txn:{state},code:{code},upstream:{jti},refresh:{token}. Cosmos native TTL is best-effort — every read defensively re-checksexpiresAtagainst an injectedTimeProvider. - Key Vault (
kvdb{resourceToken}) with soft-delete + purge protection. Function managed identity gets bothKey Vault Crypto UserandKey Vault Secrets Officerrole assignments. Single-role configurations fail silently until the first key rotation — bake both in on day one. - Upstream token encryption at rest via ASP.NET Core Data Protection.
IUpstreamTokenProtector/DataProtectionUpstreamTokenProtectorwraps the Entra access + refresh tokens (as anUpstreamTokenEnvelope) behind a stable purpose string (DevBrain.OAuth.UpstreamToken). The Data Protection key ring is persisted to a newdataprotection-keysblob container in the existing storage account and wrapped by a new Key Vault key (data-protection-key, RSA 2048). BothCosmosOAuthStateStoreandFakeOAuthStateStoreroute every upstream record save and read through the protector — a state-store-level test asserts this with aFakeUpstreamTokenProtectorcall counter. - Full id_token JWKS validation in
EntraOAuthClient. Every Entraid_tokenis now fully validated (signature, issuer, audience, lifetime) against the tenant's OpenID Connect discovery document before any claim is read. Wired viaIConfigurationManager<OpenIdConnectConfiguration>using the standardMicrosoft.IdentityModel.Protocols.OpenIdConnectdiscovery infrastructure with its default 24-hour refresh. Validation failure raises the newIdTokenValidationException;CallbackHandlertranslates it into a local 400 withinvalid_grant(a security event — the transaction is consumed but the client is not redirected, so the failure surfaces in logs rather than being papered over in the client's error UI). - Test project
tests/DevBrain.Functions.Tests/(xUnit,Microsoft.Extensions.TimeProvider.Testing). 102 tests cover all ten acceptance gates:- CVE-2025-69196 audience guard — JWTs with base-URL audience rejected
- PKCE downgrade — mismatched, empty, short, long verifiers all rejected
- Authorization code replay — atomic single-take, second redeem returns
invalid_grant - Expired transaction — transactions older than 600s rejected before any upstream call (uses
FakeTimeProvider, no sleeping) - Refresh token rotation — old refresh invalid after use, new refresh works
- Per-user identity E2E — rehydrated
ClaimsPrincipalcarries the real Entra UPN fromupstream:{jti} - Audience-scoped cross-host — token issued for host A rejected by host B
- Cross-tenant rejection — wrong
tidrejected with zero state store reads - Upstream token encryption —
EphemeralDataProtectionProvider-backed unit tests cover round-trip preservation, ciphertext-not-equal-to-plaintext, single-byte-flip tamper detection, truncation rejection, cross-key-ring rejection, and stable purpose string. Plus a state-store-level test asserting every upstream write and read invokes the protector. - id_token JWKS validation — happy path (correct signature, issuer, audience, unexpired) accepted; wrong signing key, wrong issuer, wrong audience, and expired tokens all rejected with the typed
IdTokenValidationException. Discovery-fetch failures also surface as the typed exception.
webhookAuthorizationLevel→anonymousinhost.json. The MCP extension's implicit system key is no longer the gate; the JWT middleware is. Attempting to replay a v1.5 function key at v1.6 returns 401.DocumentTools.cs: unchanged.GetCallerIdentityalready readClaimsPrincipalfromFunctionContext.Features; v1.6 just populates that feature with real identity instead of leaving it empty.Program.cs: startup fail-fast on all required OAuth config (OAuth:BaseUrl,OAuth:JwtSigningSecret,OAuth:EntraTenantId,OAuth:EntraClientId,OAuth:EntraClientSecret,CosmosDb:AccountEndpoint). Tenant ID must parse as a GUID. Missing values throw before the host starts.infra/main.bicep: newoauth_stateCosmos container, new Key Vault with both role assignments, newdataprotection-keysblob container on the existing storage account, new Key Vault key (data-protection-key) for wrapping the DP key ring, new OAuth + Data Protection app settings (double-underscore form for Linux hosting:OAuth__*,KeyVault__Name,CosmosDb__OAuthContainerName,DataProtection__BlobUri,DataProtection__KeyVaultKeyUri).DevBrain.Functions.csproj<Version>bumped to1.6.0. AddedMicrosoft.IdentityModel.JsonWebTokens 8.3.0,Microsoft.IdentityModel.Protocols.OpenIdConnect 8.3.0,Microsoft.AspNetCore.DataProtection 10.0.0,Azure.Extensions.AspNetCore.DataProtection.Blobs 1.5.0,Azure.Extensions.AspNetCore.DataProtection.Keys 1.5.0.host.jsonserverVersionbumped to1.6.0.
- JWT signing intentionally uses a plain Key Vault HMAC secret, not Data Protection. Data Protection's API is Protect/Unprotect — it is not appropriate for HMAC signing key derivation. JWT signing reads
OAuth__JwtSigningSecret(a Key Vault reference) directly as HMAC material. Data Protection is reserved exclusively for upstream token encryption (DevBrain.OAuth.UpstreamTokenpurpose). Both still depend on the same Key Vault — one KV dependency covers both concerns — but they use different secret surfaces inside it. - Single-tenant is a permanent non-goal, not a deferral. If a SaaS-style multi-tenant DevBrain ever exists, it is a fork under a different name in a different repo — not a DevBrain v2. The code leans on this:
OAuth__EntraTenantIdis hard-required, the authority URL is a baked constant, the JWT issuer stampstidinto every token, the middleware validatestidbefore any store lookup, and the Entra app manifest must be single-tenant (AzureADMyOrg). - FastMCP OAuthProxy architecture inspired this but we avoided the two bugs they shipped: CVE-2025-69196 (audience must be the webhook URL, not the base URL — see gate #1) and issue #1713 (client PKCE must not be forwarded upstream — see the independent
AuthTransaction.UpstreamPkceVerifier/ClientCodeChallengepair). - Consent screen is deferred to
sprint:devbrain-v1.7-consent-screen. Theconsent:{client_id}:{user_oid}Cosmos key shape is reserved. With single-tenant Entra as a permanent non-goal, the confused-deputy threat model essentially evaporates as long as DevBrain is deployed by the org that owns the tenant — v1.7's existence is now contingent on a future change in deployment posture (e.g., publishing DevBrain as a turnkey OSS artifact other orgs deploy), not on multi-tenancy.
Three new write primitives — DeleteDocument, AppendDocument, UpsertDocumentChunked — plus key-hygiene enforcement on the write path. Additive, no breaking API changes.
DeleteDocument(key, project?)— point-delete by key within a project. Idempotent: deleting a missing key returns{ deleted: false }with a "not found" note, not an error. Resolves the target via the project-scopedGetAsyncquery first (so it never deletes across project boundaries) and then deletes by the stored id (encoded form, URL-safe) and raw key (partition key). Accepts both colon and slash keys so legacy slash-orphans can be cleaned up through the tool.AppendDocument(key, content, separator?, tags?, project?)— append-only primitive for growing logs (session history, decision logs, audit trails). Creates the document if absent; otherwise concatenatesexisting + separator + contentserver-side. Default separator is two newlines. Tags are unioned with any existing tags. Concurrent appenders are serialized via Cosmos ETag optimistic concurrency with up to 5 retries. Refuses cross-project key collisions explicitly (never silently appends to another project's doc).UpsertDocumentChunked(key, content, chunkIndex, totalChunks, tags?, project?)— multi-part upload for documents too big to emit in a single LLM turn. Each call stages its chunk in a_staging:{realKey}document; the final chunk triggers server-side concatenation, a normal upsert to the real key, and deletion of the staging doc. Chunks may arrive out of order. A changedtotalChunksmid-upload resets the staging buffer. Staging documents self-clean via Cosmos per-item TTL (currently 4 hours) so abandoned uploads don't linger.- Slash-key rejection on write paths.
UpsertDocument,AppendDocument, andUpsertDocumentChunkednow reject keys containing/with an actionable error: "Keys must use ':' as separator. Got 'X' — did you mean 'Y'?". Reads andDeleteDocumentcontinue to accept slash keys so legacy data and cleanup operations keep working. - Per-item TTL enabled on the Cosmos container.
infra/main.bicepnow setsdefaultTtl: -1on thedocumentscontainer, enabling the TTL feature without imposing a default expiration. Real documents have nottlfield and live forever; only chunked-upload staging docs set it explicitly.
README.md"Tools Reference" table adds the three new tools. A new "When to use Append vs Chunked" subsection explains the distinction (growing logs vs. one-shot large docs).README.md"Key Conventions" note now states explicitly that writes reject slash keys while reads accept them.host.jsonMCPinstructionsmentions the three new tools and the "writes require colon keys" rule.host.jsonserverVersionbumped to1.5.0.DevBrain.Functions.csproj<Version>bumped to1.5.0.
- The Cosmos container's partition key path is
/key(not/project), so two documents with the same key in different projects would physically collide. This is a pre-existing latent issue unrelated to v1.5.AppendDocumentrefuses cross-project collisions by raising an explicit error rather than silently clobbering;UpsertDocumentretains its historical clobber-on-collision behavior for parity with v1.4 callers. DeleteDocumentusesDeleteItemAsync(EncodeId(key), PartitionKey(key)). BecauseEncodeIdstrips/from the id, theReadItemAsyncURL-path-separator problem that affected v1.2'sGetDocumentdoes not apply — the old "future: delete needs query-then-delete" note inref:known-issuesis therefore moot.ChunkedStaging.csmodels the staging payload as an order-agnostic{ totalChunks, chunks: [{ index, content }] }JSON blob stored in the staging doc'scontentfield. No schema changes were needed on the container beyond enabling TTL.
Colon keys are now the canonical user-facing convention. No breaking API changes — slash keys continue to work via the SQL-query fallback.
- "Did you mean?" project suggestions. When
ListDocumentsorSearchDocumentsreturn zero results,CosmosDocumentStorenow runs aSELECT DISTINCT VALUE c.project FROM cand looks for a similar known project name (case-insensitive startsWith / contains / reverse-contains). If one is found, a single synthetic entry withkey: "_suggestion"is returned carrying a "Did you mean project 'X'?" message. Skips the suggestion when the requested project already exists (empty result is then a legitimate miss, not a typo). The extra query only runs on the empty-result path, so the hot path is unaffected. Tool descriptions onListDocumentsandSearchDocumentsupdated to note the behavior.
host.jsonMCPinstructionsnow tells clients that keys use colon as separator, with examples (state:current,sprint:my-feature,ref:notes).host.jsonserverVersionbumped to1.4.0.DevBrain.Functions.csproj<Version>bumped to1.4.0.- README "Key Conventions" table flipped from slash to colon; added a line noting slash keys remain accepted for backward compatibility.
- README "Session Startup / AGENTS.md" example (
AGENTS.md/CLAUDE.mdblocks) now shows colon keys. - README "Why DevBrain" morning-session snippet now shows
state:currentinstead ofstate/current. - README "First Run" paragraph updated to reference
ref:devbrain-usage. docs/seed/ref-devbrain-usage.mdkey-conventions table and session-startup examples flipped to colons; added guidance that colons are canonical.scripts/seed-devbrain.ps1now seeds theref:devbrain-usagekey instead ofref/devbrain-usage.- DevBrain documents across the
devbrainanddefaultprojects re-upserted under colon keys; legacy slash-keyed originals removed.
CosmosDocumentStore.EncodeId(key.Replace('/', ':')) is unchanged and is effectively a no-op for well-formed colon keys. It stays as a safety net for any slash keys that slip through.- The
GetAsyncquery-by-c.keyfallback is unchanged, so any external integrations still using slash keys continue to work. docs/devbrain-spec-v1.mdis left as-is — it is the historical v1 spec.
Documentation, onboarding, and convention updates. No breaking API changes.
- "Why DevBrain over alternatives" README section calling out cross-project access (single deployed endpoint, shared across every AI tool) as the key differentiator vs. Serena, Claude Project Knowledge, and local markdown files.
- "Session Startup / AGENTS.md" README section with the recommended cross-tool pattern:
AGENTS.mdfor Copilot/Cursor/Codex,@AGENTS.mdimport fromCLAUDE.mdfor Claude Code. docs/project-init.md— starter-doc guide for new DevBrain projects (state:current,sprint:{name},arch:overview) with section templates.docs/seed/ref-devbrain-usage.md— canonical usage guide for AI assistants, seeded as the baselineref/devbrain-usagedocument.scripts/seed-devbrain.ps1— post-azd upbootstrap script that speaks MCP over SSE transport to seed baseline documents. ReadsAZURE_FUNCTION_URLfrom the active azd environment and the key from$env:DEVBRAIN_KEY(prompts for either if missing).- "First Run" README section pointing deployers at the new seed script.
- Tool parameter descriptions in
DocumentTools.csnow show colon-separated key examples (sprint:license-sync,sprint:) instead of slash-separated. Backend continues to accept both shapes. host.jsonserverVersionbumped to1.3.0.DevBrain.Functions.csprojnow has an explicit<Version>1.3.0</Version>.
Infrastructure, auth, and platform hardening.
- App Insights and Storage Queue Data Contributor role assignments for the Function App managed identity (
2e99030). postprovisionwait hook and Flex Consumption-specific deployment container in Bicep (2c9e8ed,6fb3295).- Cosmos doc id encoding so keys with separators round-trip cleanly (
2e99030,4a218e9).
- Hosting plan: Linux Consumption replaced with Flex Consumption Function App (
9f28f22). - Authentication: Easy Auth (Entra ID OAuth) disabled; function key auth via
x-functions-keyheader is now the only supported path due to upstream Entra/MCP OAuth incompatibilities (see README "Known Limitations"). - Cosmos doc ids now encode
/as:instead ofUri.EscapeDataString, fixing round-trip for nested keys (4a218e9). - Monitoring Metrics Publisher role definition id corrected in Bicep (
c5d48e7). - README switched to PowerShell examples throughout (
38d4a35). - Infra resources renamed to
devbrain-prefixed pattern (38d4a35,6fb3295).
GetDocumentslash bug — keys containing/failed point-reads; now handled via query (ff6e7b0).- Added explicit
Newtonsoft.Json 13.0.3dependency to satisfy the Cosmos SDK transitive requirement (69ba65c). - Added
Microsoft.Azure.Functions.Worker.Sdkreference and warm-up hook to unblock Flex Consumption deploys (2c9e8ed).
- Project encapsulation.
UpsertDocument,GetDocument,ListDocuments, andSearchDocumentsall accept an optionalprojectparameter (defaults to"default") that isolates documents by project scope (b31039c).
Initial release.
- Azure Functions MCP server exposing
UpsertDocument,GetDocument,ListDocuments,SearchDocumentstools. - Cosmos DB NoSQL backing store with managed-identity access.
azd-based deployment template.- Entra ID Easy Auth (later removed in 1.2.0 due to MCP OAuth incompatibilities).