{"id":"2f5ff0c0-ee8d-41e1-ab6c-c4567c8ee49a","slug":"canterbury","kind":"mcp","name":"canterbury","summary":"An experimental system for connecting AI agents to an Obsidian vault through a controlled service layer.","owner":"cthierer","repoUrl":"https://github.com/cthierer/canterbury","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-04-25T20:38:24.000Z","addedAt":"2026-10-21T04:31:01.104Z","logoUrl":"https://avatars.githubusercontent.com/u/8357772?u=1e4578b9cac477e868ed2ddf4ac3ce32b94f3b26&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["files-documents","cloud-infra"],"tags":["connect-rpc","golang","nodejs","protobuf","typescript"],"runtime":"go","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-23T02:17:11.157Z","sample":false,"url":"https://conduits.dev/tools/canterbury","language":"Go","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Canterbury\n\nCanterbury is an experimental system for connecting AI agents to an Obsidian\nvault through a controlled service layer. The long-term goal is to let agents\nread, search, and eventually write vault content while enforcing explicit access\npolicies and recording an independent audit trail of every interaction.\n\nThis repository currently implements a Dockerized sync worker, a local Go vault\nservice with scoped `ReadNote` and `SearchNotes` RPCs, a stateless HTTP MCP\ngateway for those RPCs, and local authentication tooling.\n\n## Project Status\n\nCanterbury is in early development. The current implementation includes:\n\n- A Node-based sync worker container.\n- `obsidian-headless` integration.\n- Docker Compose configuration.\n- A local Go vault service that reads and searches scoped Markdown notes from a\n  filesystem vault mirror.\n- Local Docker Compose stack with Dex, Pomerium Core, and the vault service for\n  testing the deployed-style auth gateway flow.\n- JWT-based request authentication with JWKS verification and TOML scope\n  mappings.\n- Scope-based authorization using note-declared access scopes and authenticated\n  principals.\n- Date-rotated JSONL audit logging for vault read/search attempts and\n  authentication failures.\n- Connect/gRPC health, reflection, `ReadNote`, and `SearchNotes` handlers.\n- A stateless Streamable HTTP MCP gateway exposing the allowlisted `read_note`\n  and `search_notes` tools at `POST /mcp`, with internal liveness and readiness\n  endpoints at `GET /health/live` and `GET /health/ready`.\n- A development auth CLI that starts a local Connect/gRPC service for minting\n  local JWTs and serving its public verification key as JWKS.\n- Repository formatting, test, and linting tooling.\n\nPlanned or incomplete components include:\n\n- Indexing and plugin-style vault operations.\n- Production identity-provider runbooks and policy-management workflows beyond\n  the local Pomerium/Dex and development auth helpers.\n\n## Project Description\n\nCanterbury aims to act as secure middleware between AI agents and an Obsidian\nvault. AI agents should not read or write vault files directly. Instead, agents\nwill interact with a service that authorizes requests, exposes structured query\ninterfaces, and records actions outside the vault.\n\nThe sync worker is the trusted component that mirrors the vault. The vault\nservice consumes that mirrored vault without receiving Obsidian account\ncredentials.\n\n## Who This Project Is For\n\nUse Canterbury if you want to explore AI-assisted workflows over an Obsidian\nvault while keeping clear boundaries around access, authorization, and audit\nrecords.\n\nThe current sync worker is useful if you want to run Obsidian Sync in Docker on\na server, NAS, homelab machine, or cloud container environment.\n\nThe current vault service and MCP gateway are useful for local development of\ncontrolled, read-only agent access over a filesystem mirror of the vault.\nIndexing, write workflows, MCP-native OAuth discovery, and production\nidentity-provider integration are still planned.\n\nCanterbury is not a replacement for an Obsidian Sync subscription. The sync\nworker requires valid Obsidian Sync credentials and an existing remote vault.\n\n## Architecture\n\nThe intended system has several components:\n\n- **Sync worker**: Mirrors an Obsidian vault using `obsidian-headless`.\n- **Vault service**: Provides the primary controlled interface to vault data.\n- **MCP server**: Adapts an explicit allowlist of vault RPCs into stateless\n  Streamable HTTP tools and forwards each request's bearer assertion.\n- **Authorization and classification**: Grants access only to explicitly scoped\n  notes when the caller principal has matching scopes.\n- **Audit system**: Records operations outside the vault in an append-only log.\n- **Indexing layer**: Parses notes into searchable metadata.\n- **Plugin and operation framework**: Runs extensible processing over vault\n  events and content.\n\nThe current repository implements the sync worker, the vault service read and\nsearch paths, the read-only MCP gateway, JWT-authenticated vault RPCs, a local\nPomerium/Dex gateway stack, a development JWT issuer, and filesystem JSONL audit\nlogging for read, search, and authentication failure events. Indexing and write\nworkflows are not implemented yet.\n\nSee [Canterbury Architecture](docs/architecture.md) for the planned Go package\nstructure and dependency boundaries. See [Auth V1 Design](docs/auth-v1-design.md)\nand [Audit System Design](docs/audit-v1-design.md) for the implemented\nauthentication and audit boundaries.\n\n## Access Model\n\nThe access model is default deny. Notes opt in to controlled service exposure by\ndeclaring access scopes in frontmatter:\n\n```yaml\naccess:\n  scopes:\n    - personal-agent\n    - public-site\n```\n\nMissing `access.scopes` means the note is not available through controlled\nservice interfaces. The current vault service validates signed bearer JWTs,\nmaps each token issuer and subject to Canterbury scopes from a TOML file outside\nthe vault, and authorizes note access from those mapped scopes.\n\nValid JWTs whose `(issuer, subject)` pair is not present in the mapping file are\nrejected as unauthenticated because Canterbury cannot resolve them to a local\nprincipal. Mapped principals whose scopes do not match a note's declared access\nscopes are authenticated but denied by vault policy.\n\n## Project Dependencies\n\nTo run the current sync worker, you need:\n\n- Docker with Docker Compose.\n- An Obsidian account with Sync enabled.\n- An Obsidian auth token.\n- The name or ID of the remote vault to sync.\n- The end-to-end encryption password for the remote vault.\n\nTo develop Canterbury or run the local vault service, you also need:\n\n- Go 1.26 or newer.\n- `golangci-lint` 2.11.4 or a compatible 2.x release.\n- Node.js 24 or a compatible version supported by the sync dependencies.\n- npm.\n- Make.\n\nInstall development dependencies and tools:\n\n```bash\nmake setup\n```\n\nRun the full repository check:\n\n```bash\nmake check\n```\n\n## Deployment Images\n\nCanterbury publishes Linux/amd64 deployment images for its MCP server, vault\nservice, and sync worker to GHCR. Images receive immutable full-commit SHA tags;\nproduction deployments must use the reported manifest digest. See\n[Publishing Canterbury Images](docs/releasing-images.md) for package names,\nmanual build and publish modes, provenance/SBOM verification, and the required\npost-publish public-package and anonymous-pull checks.\n\n## Install Canterbury\n\n1. Clone this repository.\n2. Generate local Pomerium config, certificates, and secrets:\n\n```bash\nscripts/setup-local-pomerium.mts\n```\n\n3. Build the default local stack:\n\n```bash\nCANTERBURY_UID=\"$(id -u)\" CANTERBURY_GID=\"$(id -g)\" docker compose build\n```\n\n## Configure The Sync Worker\n\nThis step is only required when running the optional `sync` profile.\n\nCopy the example environment file:\n\n```bash\ncp sync/.env.example sync/.env\n```\n\nEdit `sync/.env` and set the required values:\n\n```env\nSYNC_VAULT_NAME=your-vault-name-or-id\nSYNC_VAULT_PASSWORD=your-vault-encryption-password\nSYNC_OBSIDIAN_AUTH_TOKEN=your-obsidian-auth-token\n```\n\nOptional values:\n\n```env\nSYNC_DEVICE_NAME=canterbury-sync\nSYNC_VAULT_PATH=/vault\n```\n\n| Variable                   | Required | Description                                                                |\n| -------------------------- | -------- | -------------------------------------------------------------------------- |\n| `SYNC_VAULT_NAME`          | Yes      | Remote Obsidian vault name or ID.                                          |\n| `SYNC_VAULT_PASSWORD`      | Yes      | End-to-end encryption password for the remote vault.                       |\n| `SYNC_OBSIDIAN_AUTH_TOKEN` | Yes      | Obsidian auth token used by `obsidian-headless`.                           |\n| `SYNC_DEVICE_NAME`         | No       | Device name shown in Obsidian Sync history. Defaults to `canterbury-sync`. |\n| `SYNC_VAULT_PATH`          | No       | Local vault path inside the container. Defaults to `/vault` in Compose.    |\n\nDo not commit `sync/.env`. This repository ignores it because it contains\nsecrets.\n\n## Run The Sync Worker\n\nStart the sync worker:\n\n```bash\ndocker compose --profile sync up --build obsidian-sync\n```\n\nRun it in the background:\n\n```bash\ndocker compose --profile sync up --build -d obsidian-sync\n```\n\nStop it:\n\n```bash\ndocker compose down\n```\n\nThe service uses the `obsidian-vault` Docker volume by default:\n\n```yaml\nvolumes:\n  - obsidian-vault:/vault\n```\n\nThis is the portable default. If you replace it with a host bind mount such as\n`./vault:/vault`, you must ensure the container user can write to that host\ndirectory.\n\n### Sync Worker Readiness And Lifecycle\n\nThe sync worker deliberately separates startup progress from readiness. It\nvalidates its environment, writes the Obsidian token to its private config\nfile, configures the vault when needed, and runs a one-time `ob sync`. Only\nafter that initial sync succeeds does it start `ob sync --continuous` and\nreport ready.\n\nThe image includes its own credential-free probe:\n\n```bash\ndocker compose --profile sync exec obsidian-sync /app/sync.js healthcheck\ndocker compose --profile sync exec obsidian-sync /app/sync.js healthcheck --mode live\n```\n\nThe command defaults to `--mode ready`. A successful command prints only\n`READY` or `LIVE` and exits with status `0`; an unsuccessful command prints\n`NOT_READY` or `NOT_LIVE` and exits with status `1`.\n\n| Mode    | Contract                                                                                                                                                           |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `live`  | The supervisor heartbeat is fresh, shutdown has not begun, and the active `ob` child is alive when a child command is running.                                     |\n| `ready` | Liveness is satisfied, the one-time initial sync completed successfully, and the continuously syncing `ob` child is alive. This is the image's Docker healthcheck. |\n\nThe worker writes only its lifecycle phase, worker and child process IDs,\ninitial-sync completion, heartbeat timestamp, version, and revision to\n`/run/canterbury-sync/health.json`. The file is mode `0600`, is outside the\nvault, and is replaced atomically. Health checks never invoke `ob`, read notes,\nwrite the vault or audit log, or put credentials in command arguments.\n\nReadiness does not prove that the current upstream WebSocket is connected after\nstartup. `obsidian-headless` does not expose a reliable ongoing connectivity\nprobe. A stalled Canterbury heartbeat or exited continuous child makes the\ncontainer unhealthy; transient upstream failures that the child handles\ninternally remain visible only in container logs.\n\nOn SIGTERM or SIGINT, the worker first reports not ready and forwards the\nsignal to `ob`. It allows 20 seconds for cleanup, then sends SIGKILL if the\nupstream child has not exited. Compose grants the sync container 30 seconds to\nstop. The MCP and vault services retain their 10-second application shutdown\ndeadlines and receive 15-second Compose stop grace periods.\n\nAll three services log non-sensitive build version and revision values at\nstartup. Image builds accept `CANTERBURY_VERSION` and `CANTERBURY_REVISION`\nbuild arguments; local builds default to `dev` and `unknown`.\n\n## Run The Local Pomerium Stack\n\nThe default Docker Compose stack starts a local deployed-style auth path:\n\n- Dex as a self-contained OIDC provider.\n- Pomerium Core as the identity-aware gateway.\n- The Canterbury vault service behind Pomerium.\n- The internal Canterbury MCP server, routed through Pomerium at `/mcp`.\n\nStart it from the repository root:\n\n```bash\nCANTERBURY_UID=\"$(id -u)\" CANTERBURY_GID=\"$(id -g)\" docker compose up --build\n```\n\nThose `CANTERBURY_UID` and `CANTERBURY_GID` values make audit files in the\nbind-mounted `./audit` directory owned by your host user. When both values are\n`1000`, Docker Compose's local defaults already match; setting them explicitly is\nsafer on systems that use different host IDs.\n\nThe protected vault route is available at:\n\n```text\nhttps://vault.localhost.pomerium.io:8443\n```\n\nThe MCP endpoint uses the same protected hostname:\n\n```text\nhttps://vault.localhost.pomerium.io:8443/mcp\n```\n\nThe stack uses local-only fixture keys, a self-signed certificate, and static\nDex users. See [Local Pomerium Stack](docs/local-pomerium.md) for account\ndetails, manual testing steps, and the opt-in smoke test:\n\n```bash\nmake smoke-pomerium\n```\n\n## Run The Vault Service\n\nThe vault service reads from a local filesystem mirror of the vault. If you use\nthe default Docker-managed `obsidian-vault` volume, that volume is not exposed as\n`./vault` on the host. For local service development, point\n`VAULT_SERVICE_ROOT` at a host-accessible mirror, or deliberately use a bind\nmount override such as `./vault:/vault` after accounting for host filesystem\npermissions.\n\nCopy the vault service example environment file:\n\n```bash\ncp .env.example .env\n```\n\nConfigure the vault service values:\n\n```env\nVAULT_SERVICE_ROOT=./sample-vault\nVAULT_SERVICE_AUTH_ISSUER=devauth.canterbury.local\nVAULT_SERVICE_AUTH_AUDIENCE=canterbury.vault.local\nVAULT_SERVICE_AUTH_JWKS_URL=http://127.0.0.1:50052/.well-known/jwks.json\nVAULT_SERVICE_AUTH_MAPPING_FILE=./sample-auth/scopes.toml\nVAULT_SERVICE_AUDIT_ROOT=./audit\nVAULT_SERVICE_AUDIT_WRITER_ID=\nVAULT_SERVICE_ADDR=127.0.0.1:50051\nVAULT_SERVICE_HEALTHCHECK_URL=\nVAULT_SERVICE_HEALTHCHECK_TIMEOUT=2s\nVAULT_SERVICE_AUDIT_HMAC_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=\n```\n\n| Variable                            | Required | Description                                                                                                                                           |\n| ----------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `VAULT_SERVICE_ROOT`                | Yes      | Local filesystem path to the mirrored vault read by the Go vault service.                                                                             |\n| `VAULT_SERVICE_AUTH_ISSUER`         | Yes      | Expected JWT issuer.                                                                                                                                  |\n| `VAULT_SERVICE_AUTH_AUDIENCE`       | Yes      | Expected JWT audience.                                                                                                                                |\n| `VAULT_SERVICE_AUTH_JWKS_URL`       | Yes      | JWKS endpoint used to verify signed bearer JWTs. Its initial key set must load before the service becomes ready.                                      |\n| `VAULT_SERVICE_AUTH_MAPPING_FILE`   | Yes      | TOML file mapping exact `(issuer, subject)` pairs to Canterbury scopes.                                                                               |\n| `VAULT_SERVICE_AUDIT_ROOT`          | Yes      | Local filesystem directory where date-rotated JSONL audit logs are written outside the vault.                                                         |\n| `VAULT_SERVICE_AUDIT_HMAC_KEY`      | Yes      | Base64-encoded HMAC key, at least 32 decoded bytes, used to hash remote client addresses before writing audit records.                                |\n| `VAULT_SERVICE_AUDIT_WRITER_ID`     | No       | Optional filesystem-safe identifier included in audit filenames. When unset, the service generates one from hostname, PID, and a short random suffix. |\n| `VAULT_SERVICE_ADDR`                | No       | Address for the Connect server. Defaults to `127.0.0.1:50051` when not set.                                                                           |\n| `VAULT_SERVICE_HEALTHCHECK_URL`     | No       | Complete Connect base URL for the healthcheck CLI. Defaults to `http://<VAULT_SERVICE_ADDR>`.                                                         |\n| `VAULT_SERVICE_HEALTHCHECK_TIMEOUT` | No       | Positive timeout for the healthcheck CLI. Defaults to `2s`.                                                                                           |\n\nThe `.env.example` file includes a development-only sample\n`VAULT_SERVICE_AUDIT_HMAC_KEY` so the local demo starts without shell expansion\ninside the dotenv file. Generate a real local or deployment key with:\n\n```bash\nopenssl rand -base64 32\n```\n\nKeep this key stable if you need remote address hashes to remain comparable\nacross service restarts. Rotate it when old audit address correlations should no\nlonger be linkable.\n\nUse `./sample-vault` for a quick local demo. Point `VAULT_SERVICE_ROOT` at your\nown host-accessible vault mirror when testing with real synced content.\n\nThe included sample vault contains a few fake notes for exercising the current\nread, search, tag, and authorization behavior:\n\n| Path                         | Access scopes                   | Demo use                                     |\n| ---------------------------- | ------------------------------- | -------------------------------------------- |\n| `Projects/Canterbury.md`     | `personal-agent`                | Basic `ReadNote` and `SearchNotes` success.  |\n| `Projects/Agent Research.md` | `personal-agent`                | Text and tag search over project notes.      |\n| `Public/Service Brief.md`    | `personal-agent`, `public-site` | Multiple-scope access checks.                |\n| `Private/Unscoped Draft.md`  | None                            | Default-deny and permission-denied behavior. |\n\nThe Bruno collection in `bruno/canterbury` includes sample requests that target\nthe sample vault defaults. Add an `Authorization: Bearer <token>` header using a\ntoken minted by the development auth service before sending vault requests.\n\nThe included `sample-auth/scopes.toml` maps the development auth subject\n`user_123` to `personal-agent`. Mint a local token with subject `user_123` and\naudience `canterbury.vault.local` when using the sample configuration.\n\nThe vault service loads `.env` from the repository root when present. Real\nenvironment variables take precedence over values in `.env`.\n\n### Vault Readiness\n\nThe vault listener exposes the standard unauthenticated Connect/gRPC health\nservice. Readiness checks must request\n`canterbury.vault.v1.VaultService` through `grpc.health.v1.Health/Check`.\nThe named service reports `SERVING` only after the vault root, independent\naudit recorder, authorization mapping, and initial JWKS key set have loaded.\nInitialization failures stop the process before it binds the listener, and the\nstatus changes back to `NOT_SERVING` before graceful shutdown.\n\nThe included command provides the same bounded container-probe interface as the\nMCP service:\n\n```bash\ngo run ./cmd/vault-service healthcheck\ngo run ./cmd/vault-service healthcheck --url http://vault-service:50051 --timeout 5s\n```\n\nIt exits successfully only when the named vault service returns `SERVING`.\nConfiguration loads from `.env`, then the environment, and command flags take\nprecedence over both. The probe requires no bearer token, reads no notes, and\ncreates no audit events. It checks completed startup initialization rather than\ncontinuously testing audit filesystem writes or later JWKS refreshes.\n\nBoth production Go images use the same exec-form Docker healthcheck cadence:\n30-second intervals, a 5-second Docker timeout, a 5-second start period, and\nthree retries. Their application probe timeout defaults to 2 seconds. The MCP\nimage deliberately checks local operational health at `/health/live` and\nexcludes downstream outages; the vault image checks its own startup readiness\nthrough Connect/gRPC. Neither image installs a general-purpose network toolbox\nfor probing. The image supplies its loopback URL through\n`MCP_SERVER_HEALTHCHECK_URL` or `VAULT_SERVICE_HEALTHCHECK_URL`, so a deployment\nthat changes an internal listener can override the appropriate environment\nvariable without replacing the Docker healthcheck command.\n\n### Run The Local Auth Smoke Test\n\nThe auth smoke test is opt-in and is not part of `make check`. It starts both\nlocal services, runs Bruno CLI assertions, and then tears the services down:\n\n```bash\nmake smoke-auth\n```\n\nYou can run the same flow through npm:\n\n```bash\nnpm run smoke:auth\n```\n\nThe runner starts `go run ./cmd/dev-auth serve` on `127.0.0.1:50052`, waits for\n`/.well-known/jwks.json`, starts `go run ./cmd/vault-service` on\n`127.0.0.1:50051`, writes audit logs to a temporary directory, and runs\n`bruno/local-auth-smoke` with the `local` environment. The smoke collection uses\nConnect JSON requests so the Bruno CLI can mint tokens, chain runtime variables,\ncall the vault service, and assert success and failure cases in one ordered run.\n\nFor manual Bruno testing, start the services as shown below, then open or run the\ncollections under `bruno/devauth`, `bruno/canterbury`, or\n`bruno/local-auth-smoke`. When using `bruno/canterbury` directly, mint a token\nfor subject `user_123` and audience `canterbury.vault.local`, then set\n`VaultToken` in the `local` environment before sending vault requests.\n\nStart the development auth service first so the vault service can fetch its JWKS\nduring startup:\n\n```bash\ngo run ./cmd/dev-auth serve\n```\n\nIn another shell, start the local vault service:\n\n```bash\ngo run ./cmd/vault-service\n```\n\nThe service exposes Connect/gRPC on the configured address. Vault RPCs require\nan `Authorization: Bearer <jwt>` header. The currently implemented vault RPCs\nare:\n\n```text\n/canterbury.vault.v1.VaultService/ReadNote\n/canterbury.vault.v1.VaultService/SearchNotes\n```\n\nExample `ReadNote` request body:\n\n```json\n{\n\t\"ref\": {\n\t\t\"path\": \"Projects/Canterbury.md\"\n\t}\n}\n```\n\n`ReadNote` returns the Markdown body without frontmatter in `note.content`.\nParsed frontmatter is returned in `note.metadata.properties` after reserved\naccess-policy fields are removed. YAML timestamp values are formatted as RFC\n3339 strings so they can be represented in `google.protobuf.Struct`.\n\nExample `SearchNotes` request body:\n\n```json\n{\n\t\"query\": {\n\t\t\"text\": \"Canterbury\"\n\t},\n\t\"filter\": {\n\t\t\"includePathPrefixes\": [\"Projects\"],\n\t\t\"excludePathPrefixes\": [\"Projects/Archive\"],\n\t\t\"allTags\": [\"project\"],\n\t\t\"anyTags\": [\"ai\", \"notes\"]\n\t},\n\t\"pageSize\": 25,\n\t\"sort\": \"SEARCH_SORT_PATH_ASC\"\n}\n```\n\n`SearchNotes` returns matching note references, metadata, snippets, and a\n`nextPageToken` when another page is available. The current text query is a\nsingle trimmed search term matched case-insensitively against note content. It\nis not parsed as comma-separated syntax. Path prefix filters use vault-relative\npaths. `all_tags` requires every listed tag, while `any_tags` requires at least\none listed tag when present. Supported sort orders are\n`SEARCH_SORT_PATH_ASC` and `SEARCH_SORT_MODIFIED_DESC`.\n\nRead and search attempts are written to\ndate-rotated JSONL audit logs under `VAULT_SERVICE_AUDIT_ROOT`; each service\nprocess writes its own per-day file using `VAULT_SERVICE_AUDIT_WRITER_ID` or a\ngenerated writer ID. If a required read or search audit record cannot be\nwritten, the service fails instead of returning data. Authentication failures\nalso produce `auth.failed` audit events before the request is rejected. The\nvault service should remain bound to a trusted internal interface.\n\nThe Connect vault handler attaches request-scoped audit metadata to each vault\nRPC. It accepts or generates an `X-Request-ID`, returns that request ID on\nsuccess and error responses, extracts W3C `traceparent` trace IDs when present,\nand records a keyed HMAC-SHA256 hash of the remote client address rather than\nthe raw address.\n\n## Run The MCP Server\n\nStart the vault service first, then run the MCP gateway from the repository\nroot:\n\n```bash\ngo run ./cmd/mcp-server\n```\n\nThe gateway serves stateless Streamable HTTP with JSON responses at\n`http://127.0.0.1:50053/mcp`. Every request must contain exactly one\n`Authorization: Bearer <assertion>` header. The gateway forwards that assertion,\n`X-Request-ID`, and `traceparent` to the vault service and does not retain\nidentity-bearing MCP session state.\n\nThe same listener exposes two unauthenticated signals with deliberately\ndifferent operational meanings:\n\n| Endpoint        | Contract                                                                                                                                                                                                                                                                                 |\n| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `/health/live`  | Reports local MCP operational health. A successful request proves configuration and handler initialization completed, the listener is answering, and shutdown has not begun. It does not contact the vault or reflect any other upstream outage.                                         |\n| `/health/ready` | Reports dependency-inclusive readiness by combining local MCP health with the vault Connect/gRPC health status. Use it only when a caller specifically needs end-to-end vault availability, such as a deployment smoke test or an intentional downstream startup/traffic-admission gate. |\n\nBoth endpoints return only a gRPC-style status:\n\n```json\n{ \"status\": \"SERVING\" }\n```\n\n`SERVING` returns HTTP `200`. `NOT_SERVING` and `UNKNOWN` return HTTP `503`.\nBoth endpoints support `GET` and `HEAD`, perform no vault reads or writes, and\ndo not create audit events. The dependency-inclusive readiness check is bounded\nby `MCP_SERVER_VAULT_REQUEST_TIMEOUT`. During graceful shutdown, the local MCP\nstatus changes to `NOT_SERVING`; readiness then short-circuits without\ncontacting the vault.\n\nThe health paths are intended only for internal deployment probes. They do not\nrequire a bearer assertion and must not be routed publicly. The MCP image uses\n`/health/live` for its Docker healthcheck so a vault or identity-provider outage\ndoes not misclassify the locally functioning MCP process as broken and invite a\ncascading restart policy. `/health/ready` remains available for consumers that\ndeliberately want dependency availability included in their decision. For\nexample:\n\n```bash\ncurl --fail-with-body http://127.0.0.1:50053/health/live\ncurl --fail-with-body http://127.0.0.1:50053/health/ready\n```\n\nThe included liveness command is useful for local scripts and container probes:\n\n```bash\ngo run ./cmd/mcp-server healthcheck\ngo run ./cmd/mcp-server healthcheck --url http://mcp-server:50053/health/live --timeout 5s\n```\n\nIt exits successfully only when its target returns `SERVING`. By default, it\nchecks `http://<MCP_SERVER_ADDR>/health/live`. Set a complete endpoint URL when\nthe probe must use a different listener, scheme, or path; the command does not\nappend a health path to that override. Configuration loads from `.env`, then\nthe environment, and command flags take precedence over both.\n\n| Variable                           | Default                  | Purpose                                   |\n| ---------------------------------- | ------------------------ | ----------------------------------------- |\n| `MCP_SERVER_ADDR`                  | `127.0.0.1:50053`        | MCP HTTP listen address.                  |\n| `MCP_SERVER_VAULT_BASE_URL`        | `http://127.0.0.1:50051` | Internal Connect vault service base URL.  |\n| `MCP_SERVER_VAULT_REQUEST_TIMEOUT` | `10s`                    | Positive timeout for each downstream RPC. |\n| `MCP_SERVER_HEALTHCHECK_URL`       | Derived from server addr | Complete liveness endpoint for the CLI.   |\n| `MCP_SERVER_HEALTHCHECK_TIMEOUT`   | `2s`                     | Positive timeout for the CLI operation.   |\n\nThe public Compose deployment does not publish the MCP container port. Pomerium\nroutes only `/mcp` to the MCP server before the catch-all vault route; it does\nnot expose the `/health/` subtree. Pomerium overwrites `Authorization` with its signed\nassertion, and the gateway forwards that assertion unchanged. The shared route\nhostname keeps Pomerium's JWT issuer and audience aligned with the vault service\nchecks. See\n[Pomerium JWT claim headers](https://www.pomerium.com/docs/reference/jwt-claim-headers).\n\nThis deployment uses an ordinary protected HTTP route. It does not enable\nPomerium's experimental MCP-native OAuth mode, so MCP clients must supply a\nbearer credential accepted by the existing Pomerium route.\n\n## Run The Development Auth Service\n\nThe development auth CLI is a local helper for JWT-based authentication testing.\nIt starts a Connect/gRPC service with health and reflection enabled on\n`127.0.0.1:50052`:\n\n```bash\ngo run ./cmd/dev-auth serve\n```\n\nThe `serve` command accepts flags for its local server address and JWT issuer:\n\n```bash\ngo run ./cmd/dev-auth serve \\\n  --addr 127.0.0.1:50052 \\\n  --issuer devauth.canterbury.local\n```\n\n| Variable          | Flag       | Description                                                                                  |\n| ----------------- | ---------- | -------------------------------------------------------------------------------------------- |\n| `DEV_AUTH_ADDR`   | `--addr`   | Address for the development auth Connect server. Defaults to `127.0.0.1:50052` when not set. |\n| `DEV_AUTH_ISSUER` | `--issuer` | Issuer claim placed in minted development JWTs. Defaults to `devauth.canterbury.local`.      |\n\nWhen `.env` is present at the repository root, the development auth command loads\nthese values from it without overriding real environment variables. Command-line\nflags take precedence over environment and `.env` values. Run\n`go run ./cmd/dev-auth serve --help` to print serve flags.\n\nThe `MintToken` RPC creates an EdDSA-signed bearer JWT for requested claims:\n\n```json\n{\n\t\"claims\": {\n\t\t\"subject\": \"user_123\",\n\t\t\"audiences\": [\"canterbury.vault.local\"]\n\t},\n\t\"options\": {\n\t\t\"ttlSeconds\": 900\n\t}\n}\n```\n\nOmit `options.ttlSeconds` or set it to `0` to use the service default. The\napplication rejects missing subjects, missing audiences, negative TTLs, and TTLs\nabove the current local development maximum. The service exposes its public\nverification key as JWKS at `http://127.0.0.1:50052/.well-known/jwks.json` so\nlocal verifier work uses the same JWKS shape as deployed-style gateway auth.\nThe key is generated in memory when the service starts, so tokens minted before\na restart do not verify against the new endpoint response. The Bruno collection\nin `bruno/devauth` points at the dev-auth default address.\n\n## Develop Canterbury\n\nThe recommended setup path installs repository dependencies and project-local\nGo tooling:\n\n```bash\nmake setup\n```\n\nRun the full repository check through Make so the configured local toolchain is\nused:\n\n```bash\nmake check\n```\n\nYou can also run individual checks directly when you already have the required\ntools installed.\n\nInstall repository formatting dependencies:\n\n```bash\nnpm install\n```\n\nInstall Go linting tooling:\n\n```bash\ncurl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b \"$(go env GOPATH)/bin\" v2.11.4\n```\n\nInstall development dependencies for the sync component:\n\n```bash\nnpm --prefix sync install\n```\n\nRun all npm-orchestrated checks:\n\n```bash\nnpm run check\n```\n\nFormat files from the repository root:\n\n```bash\nnpm run format\n```\n\nRun Go tests:\n\n```bash\nnpm run test:go\n```\n\nRun Go linting:\n\n```bash\nnpm run lint:go\n```\n\nLint sync worker files:\n\n```bash\nnpm --prefix sync run lint\n```\n\nAuto-fix sync worker lint issues where possible:\n\n```bash\nnpm --prefix sync run lint:fix\n```\n\nYou can also run the sync component check directly:\n\n```bash\nnpm --prefix sync run check\n```\n\n## Troubleshoot\n\n| Problem                                                      | Cause                                                                                                      | Solution                                                                                                                                              |\n| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `SYNC_VAULT_NAME is required`                                | `sync/.env` is missing the vault name or ID.                                                               | Set `SYNC_VAULT_NAME`.                                                                                                                                |\n| `SYNC_VAULT_PASSWORD is required`                            | `sync/.env` is missing the vault encryption password.                                                      | Set `SYNC_VAULT_PASSWORD`.                                                                                                                            |\n| `SYNC_OBSIDIAN_AUTH_TOKEN is required`                       | `sync/.env` is missing the Obsidian auth token.                                                            | Set `SYNC_OBSIDIAN_AUTH_TOKEN`.                                                                                                                       |\n| `required key VAULT_SERVICE_ROOT missing value`              | `.env` or the shell environment is missing the vault service root path.                                    | Set `VAULT_SERVICE_ROOT` to `./sample-vault` for the demo or to your mirrored vault path.                                                             |\n| `required key VAULT_SERVICE_AUTH_ISSUER missing value`       | `.env` or the shell environment is missing the expected JWT issuer.                                        | Set `VAULT_SERVICE_AUTH_ISSUER` to the issuer used by your auth provider or local dev-auth service.                                                   |\n| `required key VAULT_SERVICE_AUTH_AUDIENCE missing value`     | `.env` or the shell environment is missing the expected JWT audience.                                      | Set `VAULT_SERVICE_AUTH_AUDIENCE`, and mint/request tokens with the same audience.                                                                    |\n| `required key VAULT_SERVICE_AUTH_JWKS_URL missing value`     | `.env` or the shell environment is missing the JWKS URL.                                                   | Set `VAULT_SERVICE_AUTH_JWKS_URL`, such as `http://127.0.0.1:50052/.well-known/jwks.json` for local development.                                      |\n| `required key VAULT_SERVICE_AUTH_MAPPING_FILE missing value` | `.env` or the shell environment is missing the scope mapping path.                                         | Set `VAULT_SERVICE_AUTH_MAPPING_FILE` to a TOML mapping file such as `./sample-auth/scopes.toml`.                                                     |\n| `required key VAULT_SERVICE_AUDIT_ROOT missing value`        | `.env` or the shell environment is missing the audit log directory.                                        | Set `VAULT_SERVICE_AUDIT_ROOT` to an explicit directory outside the vault, such as `./audit`.                                                         |\n| `required key VAULT_SERVICE_AUDIT_HMAC_KEY missing value`    | `.env` or the shell environment is missing the audit HMAC key.                                             | Generate a literal key with `openssl rand -base64 32` and set `VAULT_SERVICE_AUDIT_HMAC_KEY`.                                                         |\n| `HMAC key must be base64 encoded`                            | `VAULT_SERVICE_AUDIT_HMAC_KEY` is not base64.                                                              | Replace it with a literal key generated by `openssl rand -base64 32`.                                                                                 |\n| `HMAC key must decode to at least 32 bytes`                  | `VAULT_SERVICE_AUDIT_HMAC_KEY` decodes to fewer than 32 bytes.                                             | Replace it with a literal key generated by `openssl rand -base64 32`.                                                                                 |\n| `unauthenticated` from a vault RPC                           | The request is missing a bearer token, the token is invalid, or the verified issuer/subject is not mapped. | Mint/request a fresh token with the configured issuer and audience, then add a matching `(issuer, subject)` entry to the auth mapping file if needed. |\n| `permission denied; check your authorization scopes`         | The caller is authenticated, but the note does not declare a scope mapped to that subject.                 | Add a matching `access.scopes` value to the note or update the auth mapping file for local development.                                               |\n| `invalid search query`                                       | A search request contains an unsupported sort or invalid page token.                                       | Use `SEARCH_SORT_PATH_ASC` or `SEARCH_SORT_MODIFIED_DESC`, and only reuse `nextPageToken` values returned by search.                                  |\n| `Another sync instance is already running for this vault.`   | Another sync process owns the vault lock, or the vault path is not writable.                               | Stop other sync clients for the same vault and confirm the container can write to `/vault`.                                                           |\n| The container exits after a sync failure.                    | Compose is configured with `restart: 'no'`.                                                                | Inspect the logs with `docker compose logs obsidian-sync`, fix the configuration, then run `docker compose up` again.                                 |\n| Files are hard to inspect on the host.                       | The default Docker volume stores the vault outside the repository.                                         | Use Docker volume tooling to inspect it, or deliberately configure a bind mount for local development.                                                |\n\n## Roadmap\n\nThe planned MVP includes:\n\n- Production identity-provider deployment guidance and more robust\n  principal-to-scope policy management.\n- Hardening and operationalizing vault sync through Obsidian Headless.\n- Write-operation audit gating before any write paths are added.\n- Basic indexing with SQLite.\n\nInitial non-goals include:\n\n- GraphQL APIs.\n- Vector search.\n- Complex permission inheritance.\n- Autonomous write access.\n- Multi-tenant support.\n\n## Contributing\n\nBefore contributing, run:\n\n```bash\nnpm run check\n```\n\nKeep the default deployment portable. Prefer Docker-managed volumes for runtime\nstorage, and treat host bind mounts as local development overrides.\n\n## Additional Documentation\n\n- [`obsidian-headless`](https://www.npmjs.com/package/obsidian-headless)\n- [Obsidian Sync](https://obsidian.md/sync)\n- [Docker Compose](https://docs.docker.com/compose/)\n- [Canterbury Architecture](docs/architecture.md)\n- [Auth V1 Design](docs/auth-v1-design.md)\n- [Audit System Design](docs/audit-v1-design.md)\n- [Local Pomerium Stack](docs/local-pomerium.md)\n- [Dependency Maintenance](docs/maintenance.md)\n- [The Good Docs Project README template guide](https://www.thegooddocsproject.dev/template/readme)\n\n## How To Get Help\n\nStart by checking the sync worker logs:\n\n```bash\ndocker compose logs obsidian-sync\n```\n\nFor problems with Obsidian account access, vault encryption, or Sync service\nbehavior, consult Obsidian support resources. For problems with Canterbury, open\nan issue or discussion in the repository where this project is hosted.\n\n## Terms Of Use\n\nThis project is licensed as `UNLICENSED` in `sync/package.json`.\n\nObsidian, Obsidian Sync, and `obsidian-headless` are separate projects with their\nown terms and licenses.\n"}