Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0379d14
[maven-release-plugin] prepare for next development iteration
namedgraph Oct 4, 2026
cd6e022
Set the RDF library and CLI versions to 6.1.1-SNAPSHOT
namedgraph Oct 4, 2026
1ab9067
The release workflow builds linkeddatahub-rdf before the CLI it attaches
namedgraph Oct 4, 2026
2ad169d
An assistant: a request in words becomes a Web-Algebra plan, in a cha…
namedgraph Oct 5, 2026
36d1555
Every canvas is drawn by one template: a chart, a map or a 3D graph, …
namedgraph Oct 5, 2026
a6e8c07
The UI tests follow the assistant out of the drawer, and skip the mod…
namedgraph Oct 6, 2026
be8dbe9
Web-Algebra gets the test stack's certificates, and a chart's datetim…
namedgraph Oct 7, 2026
615f56e
A blank node described in place lays out for its own cell, not for th…
namedgraph Oct 8, 2026
9cc6394
The proxy serves an ASK's boolean as one, instead of reading it as a …
namedgraph Oct 8, 2026
f9a15f8
A proxied response varies on Accept, whatever the upstream's Vary says
namedgraph Oct 8, 2026
03913a6
A malformed Accept header or accept parameter no longer turns a 403 i…
namedgraph Oct 8, 2026
9c26b01
A document imported under another name than its ontology's no longer …
namedgraph Oct 9, 2026
c98e4f6
LDH's ontologies import SPIN, SP and OWL by the IRIs those ontologies…
namedgraph Oct 9, 2026
732bd22
ldh:query-result is the one SPARQL call shared stylesheets make, and …
namedgraph Oct 9, 2026
2eb9a5a
An external HTML page with no JSON-LD opens in a new tab instead of s…
namedgraph Oct 10, 2026
3528356
Top-level XSLT params and variables carry the ldh: prefix
namedgraph Oct 12, 2026
775e764
The assistant draws its results as a view does, so a graph can be sho…
namedgraph Oct 12, 2026
240ac6f
GraphVersioningServiceTest parses N-Triples through RDFParser, not th…
namedgraph Oct 12, 2026
f45494d
The version history link announces that it opens a dialog
namedgraph Oct 12, 2026
2f65906
The clients' connection pools are marked shared rather than subclasse…
namedgraph Oct 12, 2026
ca3e3f4
A block the assistant adds is scrolled to when the page reloads, not …
namedgraph Oct 13, 2026
031848f
Web-Client 6.0.5, and the changelog lists what develop holds since th…
namedgraph Oct 13, 2026
237823c
A GET on /sparql without a query returns the endpoint's service descr…
namedgraph Oct 14, 2026
f67150e
The code block is hovered once the page stops moving, and the rate-li…
namedgraph Oct 14, 2026
9fbe050
A block saved through its edit form labels its resource values instea…
namedgraph Oct 14, 2026
0e4f783
SPARQL endpoints are capped by concurrency, and a Fuseki out of heap …
namedgraph Oct 14, 2026
4432771
A block the assistant adds is scrolled to without the reload stopping…
namedgraph Oct 15, 2026
9f0ee66
A new XHTML block stays on the page after its save in Chrome
namedgraph Oct 15, 2026
f94943a
Web-Client version bump
namedgraph Oct 15, 2026
7d12551
/context and lds:admin replace the admin. subdomain derivation and st…
namedgraph Oct 16, 2026
89167a8
/context and /settings build their responses with Core's Response, as…
namedgraph Oct 16, 2026
bcad0e2
A view's body carries its definition as hidden RDFa, so an ontology-d…
namedgraph Oct 16, 2026
8319030
An ldh Claude Code plugin for building on LinkedDataHub with the CLI …
namedgraph Oct 16, 2026
5547ccb
Version bumped to 6.2.0-SNAPSHOT
namedgraph Oct 16, 2026
0d27e7b
rdf:XMLLiteral values are Exclusive XML Canonicalization again, via t…
namedgraph Oct 16, 2026
af6ba22
The changelog lists what develop holds since /context and lds:admin l…
namedgraph Oct 16, 2026
573593d
The changelog marks the assistant's openai_api_key secret as optional
namedgraph Oct 16, 2026
cb6d7cf
Embedded cards and tables keep their corners: a block radius token, a…
namedgraph Oct 16, 2026
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
15 changes: 15 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "linkeddatahub",
"description": "Skills for building Linked Data dataspaces, datasets and applications on LinkedDataHub",
"owner": {
"name": "AtomGraph",
"url": "https://atomgraph.com"
},
"plugins": [
{
"name": "ldh",
"source": "./plugins/ldh",
"description": "Build Linked Data dataspaces, datasets and applications on LinkedDataHub with the ldh CLI"
}
]
}
5 changes: 5 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ OWNER_STATE_OR_PROVINCE=Denmark
OWNER_COUNTRY_NAME=DK

MAX_CONTENT_LENGTH=2097152

# the assistant's plan service image tag (ghcr.io/atomgraph/webalgebra-server-ldh); `local` for an image built from a REST-VKG checkout
# WEBALGEBRA_VERSION=latest
# the OpenAI model the assistant plans and writes queries with; gpt-4o when unset
# OPENAI_MODEL=gpt-4o
34 changes: 34 additions & 0 deletions .github/workflows/load-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ jobs:
- name: Add bin/ and its subdirectories to PATH
run: |
find "$GITHUB_WORKSPACE/bin" -type d >> "$GITHUB_PATH"
# the SPARQL load tests make the end-user dataspace public first (ldh admin make-public), the way a
# real deployment does - anonymous /sparql is what they put under load. That needs the ldh CLI, built
# here from this checkout exactly as the HTTP-tests workflow does (JDK 21, then the sibling library
# cli/ resolves by version, then the CLI).
- name: Set up Java 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Build the linkeddatahub-rdf library
run: mvn -B install
working-directory: rdf
- name: Build the ldh CLI
run: mvn -B package
working-directory: cli
- name: Add the ldh launcher to PATH
run: echo "$GITHUB_WORKSPACE/cli/bin" >> "$GITHUB_PATH"
- name: Generating server certificate
run: |
server-cert-gen.sh .env nginx ssl
Expand All @@ -28,10 +45,27 @@ jobs:
run: docker compose -f docker-compose.yml -f ./tests/http/docker-compose.http-tests.yml -f ./tests/load/docker-compose.load-tests.yml --env-file ./tests/http/.env up --build -d
- name: Wait for the server to start...
run: while ! (status=$(curl -k -s -w "%{http_code}\n" https://localhost:4443 -o /dev/null) && echo "$status" && echo "$status" | grep "403") ; do sleep 1 ; done # wait for the webapp to start (returns 403 by default)
- name: Make the end-user dataspace public
# the platform writes the owner keystore into the bind-mounted ssl/owner on first boot; the suite's
# SPARQL tests send anonymous queries, which only reach the store once the dataspace is public
run: |
ldh admin make-public \
-c tests/http/ssl/owner/keystore.p12 \
-p "$(cat secrets/owner_cert_password.txt)" \
https://localhost:4443/
- name: Run load test scripts
run: ./run.sh
shell: bash
working-directory: tests/load
env:
# the same stack the steps above brought up (project name from tests/http/.env); the SPARQL tests
# drive the loadgen containers and inspect fuseki through this
COMPOSE_CMD: >-
docker compose
-f ${{ github.workspace }}/docker-compose.yml
-f ${{ github.workspace }}/tests/http/docker-compose.http-tests.yml
-f ${{ github.workspace }}/tests/load/docker-compose.load-tests.yml
--env-file ${{ github.workspace }}/tests/http/.env
- name: Generate test summary
if: always()
run: python3 scripts/generate_test_summary.py tests/load/out tests/load/out/report.md
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ jobs:
# strip the literal prefix
echo "FULL_VERSION=${RAW_REF#linkeddatahub-}" >> $GITHUB_ENV

# The CLI resolves linkeddatahub-rdf at its own version, which the tagged checkout holds. Maven
# Central does not: release.sh deploys the library after pushing the tag, and Central publishes
# it minutes later, while this job runs seconds after the push.
- name: Build the linkeddatahub-rdf library
run: mvn -B install
working-directory: rdf

# release.sh keeps cli/pom.xml at the platform version, so the tagged commit already carries it
# and the jar manifest ldh --version reads gets it from there
- name: Build the ldh CLI
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ The dataspace exposes a **read-only SPARQL 1.1 Query** endpoint (advertised via

Write portable, standard SPARQL: use explicit `GRAPH` patterns, no engine-specific extensions.

**The default graph is empty.** Every document is a named graph whose name is the document's URL, so a triple pattern outside `GRAPH` matches nothing. Put the patterns inside `GRAPH ?g { ... }` to query across every document, or inside `GRAPH <document URL> { ... }` for one document; a subquery needs its own `GRAPH` as well. A query that returns no rows without a `GRAPH` clause is not evidence that the data is absent.

**One document per resource, so a join crosses graphs.** A resource is described in the document about it (`foaf:primaryTopic`), and the resources it links to - an order's customer, a product's category, an employee's manager - are each described in their own document, in another graph. A pattern that follows such a link therefore needs a `GRAPH` of its own for each resource: `GRAPH ?o { ?order schema:customer ?customer } GRAPH ?c { ?customer schema:legalName ?name }`, never both triples in one `GRAPH ?g`, which asks for them in the same document and matches nothing. Only a resource's own companions - its address, its contact point, an order's line items - share its document. When in doubt, give every subject variable its own graph variable: it costs nothing when two resources do share a document, and it is the only pattern that works when they do not.

**A property is used on instances of its domain.** The ontology (`Link rel=lds:ontology`) declares each property's `rdfs:domain` and `rdfs:range`, and the data follows it: a property declared on `schema:Person` is not on the `schema:Corporation` that employs the person. To reach it, follow the ontology's path - the corporation's `schema:employee`, then the person's `schema:address`.

Outbound `SERVICE` and `LOAD` — from the triplestore and from the platform alike — are routed through the `egress` forward proxy, which refuses loopback, private and link-local destinations. A federated query reaches public endpoints and cannot reach the deployment's own services. With no proxy configured and `ALLOW_INTERNAL_URLS` unset, in-JVM `SERVICE` (in a `PATCH` update or an import mapping) is disabled outright.

## Content & document model
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
## [Unreleased]
### Migration
- Top-level LDH stylesheet params and variables carry the `ldh:` prefix; overrides of the unprefixed names are ignored
- Shared stylesheets query through `ldh:query-result()`; `ac:value-intersect()`/`ac:value-except()` are removed
- **BREAKING**: the `$lds:Context` stylesheet param is removed, as it exposed service credentials to stylesheets; use `lds:context()`
- Existing dataspaces need an authorization granting public read access to `/context` on both origins (new ones get it seeded)
- **BREAKING**: `ldh import csv|rdf` print the URI of the created import instead of the target document's
- **BREAKING**: `ldh --proxy` keeps the request's `Host`; pass the plain proxy instead of an `admin.`-prefixed one

### Added
- `/context` serves the public description of every dataspace, and end-user responses link their admin application (`lds:admin`)
- `ldh admin` commands take the end-user base URI
- `ldh admin import ontology --file` installs the namespace ontology from a local file in one step
- `ldh import csv|rdf --wait` waits for the import to finish
- An assistant that turns a natural-language request into a Web-Algebra plan, runs it on Execute and shows the result as a view
- Assistant conversations are stored as `ldh:Chat` blocks in the document
- A `web-algebra` service in the stack, acting on the reader's behalf; the assistant writes plans only when the optional `openai_api_key` secret is declared
- The agent guide is served at `/AGENTS.md` on every dataspace
- An `ldh` Claude Code plugin with skills for building dataspaces, datasets, apps and packages with the CLI
- A `GET` on `/sparql` without a query returns the endpoint's SPARQL Service Description

### Changed
- An external HTML page with no RDF representation opens in a new tab; the proxy answers `406` instead of `502`
- Every chart, map and 3D graph canvas is initialised by one template, wherever it is rendered
- External `/sparql` requests are capped per client and in total by nginx (`NGINX_SPARQL_*`), answering `429` beyond the limits
- Fuseki restarts on an out-of-memory error instead of running on broken
- Web-Client dependency bumped to 6.0.6

### Fixed
- A malformed `Accept`-family header no longer turns an error response into a `500`; an invalid `accept` parameter is refused with `400`
- Proxied responses always vary on `Accept`, so a cached RDF response is no longer served to a browser navigation
- The proxy serves the boolean result of an upstream `ASK` query
- A document imported under another name than its ontology's no longer takes the dataspace down
- LDH's ontologies import SPIN, SP and OWL by the IRIs those ontologies declare
- Nested property lists of blank nodes lay out for their own cell instead of collapsing to one word per line
- Charts no longer fail on datetimes with fractional milliseconds
- The version history link announces that it opens a dialog
- `rdf:XMLLiteral` values are stored as Exclusive XML Canonicalization again
- A block saved through its edit form labels its resource values instead of showing the URI's fragment
- An ontology-defined view no longer flashes its own metadata before its results

## [6.1.0] - 2026-10-05
### Migration
- **BREAKING**: `ldh` drops `-b`/`--base`; dataspace-level commands take the base URI as their positional, defaulting to `LDH_BASE`
Expand Down
25 changes: 24 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ make drop # Complete reset (down -v, then wip

make rdf # Install the linkeddatahub-rdf library
make cli # Build the ldh CLI (installs rdf first), print the PATH export
make cli-version # Set rdf/ and cli/ poms to the platform version in pom.xml
make cli-version # Set the rdf/ and cli/ poms and the ldh plugin to the platform version in pom.xml
```

### Testing
Expand Down Expand Up @@ -102,12 +102,23 @@ Configuration is split across two files:

Multiple dataspaces can share the same backend SPARQL service.

The admin pairing is the `admin.` subdomain and nothing else: the server derives it
(`EndUserDataspaceImpl.getAdminDataspace()`) and publishes it, so no client derives it again. An
end-user response carries `Link: <admin base>; rel="…/dataspaces#admin"` (the CLI's admin commands
follow it), and `/context` serves `PublicContext`: every dataspace's description (type, title,
origin, ontology, stylesheet, `lds:public`) plus `lds:admin`, readable by anyone through a seeded
authorization. Stylesheets read it through `lds:context()`: the server twin dereferences `/context`
(same-site, so `SameSiteSourceResolver` fetches it), the client twin reads the copy the bootstrap
preloads. No stylesheet param carries context: the full context dataset also holds the service
endpoints and credentials from `system.trig` and `secrets/credentials`, and stays in Java.

### Service Architecture
The application runs as a multi-container setup:
- **nginx**: Reverse proxy and SSL termination (wildcard subdomain routing for dataspaces)
- **linkeddatahub**: Main Java application (Tomcat)
- **fuseki**: One SPARQL server holding a TDB2 dataset per dataspace role (`config/fuseki/config.ttl`), named after the dataspace origin (deployment host dropped, role appended: `end-user`, `admin`, `northwind-traders.demo.end-user`, …), each under `fuseki/<dataset>/`; bound to apps in `config/system.trig`
- **egress**: Squid forward proxy for the store's and platform's outbound requests (SPARQL `SERVICE`, `LOAD`): public destinations only, so a query cannot reach another dataset, Varnish or the platform
- **web-algebra**: REST-VKG's `webalgebra-server-ldh` image — executes Web-Algebra plans and writes them from natural language for the assistant drawer (`xsl/client/chat.xsl`). nginx forwards `/webalgebra` (a reserved path, like `/static/` and `/uploads/`) to it with the caller's certificate; it presents the secretary's certificate to this instance with `On-Behalf-Of` naming the caller, which `WebIDFilter` honours because every agent's WebID document carries `<secretary> acl:delegates <agent>`. It addresses the instance by its public origin, rewritten to `nginx:9443` (`WEBALGEBRA_PROXY_HOST`), the same trick the platform's own `ClientUriRewriteFilter` plays
- **varnish-frontend/varnish-admin/varnish-end-user**: Caching layers (admin and end-user caches both front the single `fuseki`)

### Data Flow
Expand Down Expand Up @@ -162,6 +173,12 @@ ldh create container --parent "$LDH_BASE" --title "Some" --slug some
ldh admin add agent --agent "$AGENT_URI" "${ADMIN_BASE}acl/groups/writers/"
```

The `ldh` Claude Code plugin (`plugins/ldh/`, published by the marketplace in `.claude-plugin/`)
teaches agents to build on LinkedDataHub with this CLI: one skill each for provisioning a dataspace,
modelling and importing a dataset, building an app, and writing a package. Its skills quote command
options and config shapes, so a change to either updates them in the same commit. `plugin.json`
carries the platform version, set by `release.sh` and `make cli-version` along with the poms.

## The RDF library

`rdf/` builds `com.atomgraph:linkeddatahub-rdf` — the vocabularies (`com.atomgraph.linkeddatahub.rdf.vocabulary`)
Expand Down Expand Up @@ -200,6 +217,12 @@ by extension. Commands that create or append to a document print its URL as the
stdout (diagnostics go to stderr), so `item=$(ldh create item ...)` works; exit codes are `0`
success, `1` HTTP or runtime failure, `2` usage error.

The admin commands take the end-user base and find the admin application from its `lds:admin` link,
so scripts never prefix `admin.` themselves. `--proxy` connects to the proxy but keeps the logical
`Host`, which is how nginx routes dataspaces. `admin import ontology --file ns.ttl` installs the
namespace ontology in one step, and `import csv|rdf` print the import's URI, with `--wait` to block
until it ends.

Packages are declarative: an application imports one with a single `<app> ldh:import <package>`
triple in its settings, and `ldh packages list`, `ldh packages add` and
`ldh packages remove` write that triple through `PATCH /settings`. `packages list` reads the
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ RUN mvn -B -Pstandalone dependency:go-offline

COPY src /usr/src/platform/src

# the agent guide the WAR serves at /AGENTS.md (pom.xml, maven-war-plugin webResources)
COPY AGENTS.md /usr/src/platform/AGENTS.md

RUN mvn -Pstandalone clean install
# ==============================

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The [`ldh` command line interface](#command-line-interface) is attached to every
- `secrets/client_truststore_password.txt`
- `secrets/owner_cert_password.txt`
- `secrets/secretary_cert_password.txt`
- `secrets/openai_api_key.txt` — optional: an OpenAI API key for the assistant drawer, declared in `docker-compose.override.yml` like the OAuth secrets; without it the assistant runs plans but cannot write them
The one you will need to remember in order to authenticate with LinkedDataHub using WebID client certificate is `owner_cert_password`.
5. Launch the application services by running this from command line:
```shell
Expand Down Expand Up @@ -217,6 +218,8 @@ _:warning: Do not use blank nodes to identify applications or services. We recom
<dd>Password of the secretary's WebID certificate</dd>
<dt><code>client_truststore_password</code></dt>
<dd>Password of the client truststore</dd>
<dt><code>openai_api_key</code></dt>
<dd>OpenAI API key the <code>web-algebra</code> service writes the assistant's plans with. Optional, declared in the override together with <code>OPENAI_API_KEY_FILE</code>; the service executes plans without it, and the drawer then reports that a plan could not be written</dd>
<dt><code>google_client_id</code></dt>
<dd>Google's OAuth client ID</dd>
<dd>Login with Google authentication is enabled when this value is provided</dd>
Expand Down Expand Up @@ -359,7 +362,6 @@ LinkedDataHub includes an HTTP [test suite](https://github.com/AtomGraph/LinkedD
* [SPARQL.js](https://github.com/RubenVerborgh/SPARQL.js)
* [OpenLayers](https://openlayers.org)
* [Google Charts](https://developers.google.com/chart)
* [xml-c14n-sync](https://github.com/AtomGraph/xml-c14n-sync)

### Java

Expand Down
Loading
Loading