Skip to content

Releases: enrichmeai/cistern

Cistern 0.2.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 12:19
ebc4817

The MCP front door. Any MCP client — Claude Desktop first — now connects to a pod and gets
exactly what its bound identity was granted: reads inside the grant, structured refusals
outside it, revocation that lands on the very next tool call, and receipts for all of it.
The four-beat flagship demo (works · refusal · live revocation · the receipt) runs end to
end against a real MCP client. Every MCP call crosses the same enforcement filter as every
HTTP caller — no privileged path, by architecture decision.

Added

MCP front door (cistern-mcp, T6.1 + T6.2, #37/#38)

  • The MCP server: seven tools — read-resource, list-container, write-resource (ETag
    preconditions honoured), delete-resource, grant/revoke (the same .acl writes the
    CLI performs; Control enforced by the server), receipts (Control enforced) — built on
    the official MCP Java SDK. Deliberately no search tool, and no MCP resources in v1:
    nothing the door serves may outlive a revocation.
  • No privileged internal path (ARCHITECTURE decision 6): every tool call is a real HTTP
    request to the running server — the standalone bridge over the network, the embedded
    shape over its own loopback port — crossing AuthorizationFilter, decided by WAC afresh,
    and leaving a receipt like any other client's request.
  • Static identity binding (T6.2, per the #89 ruling): one connection, one bearer
    credential (cistern.mcp.credential / CISTERN_MCP_CREDENTIAL), resolved through the
    ordinary resolver chain. An enabled door without a credential is refused at bind time.
  • Refusals are honest and structured: a 401/403 becomes an isError result whose text
    begins REFUSED, naming the resource and the required mode (computed by the server's own
    RequiredAccess table) — never an empty success, never a retry with other credentials.
  • Two launch shapes: the shaded bridge jar (cistern-mcp-<version>-bridge.jar) a
    desktop client launches against a running pod, and cistern.mcp.enabled=true, which makes
    cistern-app serve MCP on its own stdio (with the mcp-stdio profile keeping stdout
    protocol-clean). Claude Desktop configuration and the four-beat walkthrough:
    docs/demo/claude-desktop.md.

Cistern 0.1.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 09:45
1b6121e

First tagged release: everything on main from the scaffold (2026-07-17) to the production
posture (T7.7, #122). It is a usable multi-pod server with real authentication: humans
authenticate through your own OIDC issuer and applications as service principals with hashed
credentials (or the owner uses a local bearer token on a private network), Web Access Control
is enforced on every request, grants are authored with one command and revoked on the very
next request, and every allow and deny leaves a receipt. It is not yet a Solid-OIDC/DPoP
server — interop with Solid identity providers is Phase 4 proper — and although this release
ships the production tooling ADR 0002 requires, the private-network posture remains the
default (see Known limitations).

Added

Core (cistern-core)

  • Resource model and the storage SPI (ResourceStore), with the shared
    ResourceStoreContractTest kit every backend must pass and an in-memory reference
    implementation (#50).
  • RDF I/O: Turtle and JSON-LD parse/serialize with base-URI resolution, over Jena (#51).
  • Containment: ldp:contains is derived from storage, never stored; client attempts to write
    server-managed triples are rejected (#52).
  • N3 Patch engine — solid:inserts / solid:deletes / solid:where (#56); resource kind
    (container vs document) is a core-carried fact rather than inferred from the path (#70).
  • Ground-rule-7 retrofit: per-module message catalogues, an RDF media-type enum, a sealed
    CisternException hierarchy, one interface-metadata writer (#74).

Storage (cistern-storage-file)

  • File-per-resource backend with metadata sidecars and crash-safe writes
    (temp file + ATOMIC_MOVE); passes the contract kit (#53).

HTTP (cistern-webflux, Spring WebFlux, fully reactive)

  • GET/HEAD with content negotiation and LDP headers; HEAD = GET minus body (#62).
  • PUT create/replace, intermediate containers created on demand, trailing-slash
    container semantics (#66).
  • POST to a container: sanitised Slug, non-overwriting name collisions, Link
    interaction model (#68).
  • DELETE: 204; 409 on a non-empty container; 405 for the storage root (#63).
  • Conditional requests: If-Match / If-None-Match, 412 decided before any write, 304
    on reads (#69).
  • PATCH with N3 Patch over HTTP (#70).
  • OPTIONS and CORS: Allow / Accept-* from one table; open CORS with the request origin
    echoed (Solid Protocol §8.1), narrowable with cistern.cors.* (#72).
  • Discovery: a storage description resource, pim:Storage on the root,
    solid:storageDescription link on every resource (#73).
  • One RFC 9457 error mapper — every error is application/problem+json; handlers never
    speak status codes (#61).
  • cistern.base-url mints every identifier (configured, never taken from Host), so
    identifiers are stable behind a proxy and cannot be poisoned by a forged header.

Access control (cistern-wac, enforced in cistern-webflux)

  • WAC engine evaluating acl:Authorization graphs, deny by default; acl:Append is a
    subset of acl:Write; acl:Control implies nothing else (#84).
  • ACL discovery: the resource's own ACL, else the nearest ancestor's acl:default, stopping
    at the storage root — and failing closed: an ACL that exists but does not parse denies,
    an empty ACL still terminates the walk (#85).
  • Enforcement as a WebFilter ahead of every handler, so deny-by-default is structural:
    401 + WWW-Authenticate: Bearer realm="cistern" when no identity was proved, 403 when
    an authenticated agent lacks the mode, WAC-Allow on GET/HEAD. DELETE requires
    Write on the parent container as well; POST/PATCH require Append (#86).
  • Owner seeding: setting cistern.owner.web-id turns enforcement on and seeds the root ACL
    (acl:accessTo and acl:default, never overwritten) granting that WebID full access.
    cistern.owner.token adds the local bearer way in (Authorization: Bearer <token>) — one
    of the three credential shapes below, and unset in production (#86, refined by #122).

Authentication (T4.0, #88, PR #110cistern-auth)

  • One resolver seam, three credential shapes: ChainedPrincipalResolver (first authenticated
    resolver wins) chains the OIDC/JWT resolver, the service-principal registry and the owner's
    local token, with anonymous as the fallthrough. The enforcement path is untouched — WAC
    does not care how a WebID was proved.
  • OidcJwtPrincipalResolver (Nimbus): bring your own issuer. iss is compared verbatim to
    cistern.auth.oidc.issuer, aud must contain a configured audience, keys come from
    .well-known/openid-configuration (or a pinned jwks-uri), exp/nbf honour a
    configurable clock skew, and the WebID is read from a claim (webid-claim) or minted from
    a template (webid-template). Fixtures captured from a real IdP, never invented.
  • ServicePrincipalRegistry: an application is its own principal —
    cistern.auth.service-principals[n].web-id + .credential-hash (sha256:<hex> at rest),
    no issuer required for machine clients.

Provisioning, grants and the cistern CLI (T5.6, #90, PR #113 · T5.7, #91, PR #111)

  • Multi-pod provisioning, idempotent on every restart: cistern.pods.seed[n] provisions pods
    at boot (never overwrites an existing ACL; a malformed or duplicate root is refused at bind
    time), PodProvisioner does the same for embedders, and cistern pod create --root </firms/acme/> --owner <webid> does it over HTTP under the caller's credential — the
    server enforces Write and Control where the root goes.
  • Grant authoring that cannot lock the owner out: GrantService and cistern grant <webid|public> --read|--write|--append|--control <path> write <path>.acl re-stating
    whoever holds Control there today; cistern revoke takes back everything a grantee was
    given and refuses to remove an authorization that grants Control. Exit codes: 0 ok,
    1 failure, 2 refused (the server decides, not the tool), 3 conflict (the ACL changed
    underneath; nothing written).
  • The CLI is a shaded executable jar (cistern-cli-<version>.jar, picocli, no Spring) with a
    bin/cistern wrapper.

Decision log and receipts (T5.9, #93, PR #114)

  • Every authorization decision — allow and deny, every method — leaves one DecisionRecord
    naming the agent, the target, the mode required, the outcome and, when allowed, the ACL
    that granted it; the record is written before the response is sent. AccessDecision
    carries decidedBy and the matched authorization IRIs. JSON Lines, one file per UTC day
    under <storage root>/.cistern/decisions/, written through the storage SPI but outside
    the pod's URI space (never listed, no HTTP path reaches it). GET <resource>?receipts[&from&to]
    returns them as application/x-ndjson to a holder of Control on the resource;
    GET /?receipts&agent=<webid> is the owner's per-agent query. X-Request-Id is honoured
    or minted and echoed on every response. cistern.audit.required (default false) makes
    an unrecordable decision fail closed with 503 (CisternException.ServiceUnavailable); by
    default the outcome stands and the failure is logged. k8s/demo.sh's sixth beat is the
    receipt.

Packaging & operations

  • Multi-stage Dockerfile (non-root uid 10001, /data volume, TCP healthcheck) and a
    docker-compose.yml bound to 127.0.0.1:3737 (#78, #80).
  • Kubernetes manifests for a local cluster (k8s/): restricted Pod Security Standard,
    RWO PVC, single replica with Recreate, ClusterIP only, deny-all NetworkPolicy; the
    owner credential from a Secret; k8s/demo.sh shows a scoped grant and live revocation
    (#79, #86, #87).
  • Integration kit (integration-kit/, T7.10, #102, PR #108): Cistern + Keycloak — a real
    OIDC issuer with a ready realm (two humans, two service principals, audience cistern) —
    plus a seeded pod and a sample app performing the allow/refuse sequence, in one
    docker compose up; loopback only.
  • CI: build + full test suite on every PR; the Solid conformance harness as a report-only job
    with honest numbers (#77); the Dockerfile built on every PR; k8s manifests and Terraform
    validated without credentials.
  • Release pipeline (T7.14, #107, PR #109): a v* tag runs the full suite, builds the image
    natively per architecture, and publishes the multi-arch image to GHCR and a GitHub Release
    whose body is this section — carrying cistern-app-<version>.jar, the cistern CLI
    (cistern-cli-<version>.jar and the bin/cistern wrapper, smoke-run before either can
    become an asset) and SHA256SUMS over all three. RELEASE.md is the runbook: the gate,
    the workflow_dispatch rehearsal (publishes nothing), the tag commands, the post-release
    stranger test.

Production posture (T7.7, #94, PR #122)

  • Enforcement guard. A credential source — cistern.auth.oidc.issuer or
    cistern.auth.service-principals[] — configured without cistern.owner.web-id refuses
    to start at bind time (ENFORCEMENT_REQUIRES_OWNER, naming the fix): enforcement is keyed
    on the owner, so the credentials would never have been asked for and the pod would have
    been open while its configuration read as locked. The owner's WebID and token do different
    jobs: the WebID alone turns enforcement on and seeds the root ACL (Owner.isNamed()); the
    token only adds the local bearer resolver (Owner.hasLocalCredential()) — the production
    shape is WebID set, token unset, owner authenticating via OIDC or a hashed service
    credential. Nothing configured still starts and warns (NO_OWNER_CONFIGURED: enforcement
    is off); an owner named with no way to authenticate starts enforced and warns
    ENFORCEMENT_WITHOUT_CREDENTIAL.
  • ADR 0002 supersedes ADR 0001: an instance may face the internet under eight conditions
    (TLS in front; cistern.owner.web-id set; cistern.owner.token unset; per-tenant
    isolation; backups drilled; `X-Requ...
Read more