Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
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
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [Unreleased]
### Added
- An assistant for a signed-in reader: a request in natural language becomes a Web-Algebra plan, shown with its operations and its XML, and runs only on Execute; the steps, the documents the plan wrote and its result are reported as they happen, and the result is answered in words and drawn as a table, a list, a grid or a chart
- A conversation is an `ldh:Chat` block, placed in the document by an object block: the create bar's Assistant button starts one, each ends in its own composer, and every turn is stored as an `ldh:ChatTurn` with its plan and what the execution reported, so a chat is there when the reader comes back
- A `web-algebra` service in the stack (`ghcr.io/atomgraph/webalgebra-server-ldh`), reached through nginx at the reserved `/webalgebra` path with a WebID certificate on the connection; it acts for the reader through the secretary agent's delegation, so a plan may write exactly what its reader may. Needs the `openai_api_key` secret
- The agent guide (`AGENTS.md`) is served at `/AGENTS.md` on every dataspace origin, public and outside the dispatcher, so a client writing a query against the endpoint - the assistant's `SPARQLString` reads it as service documentation - learns that every document is a named graph and the default graph is empty

## [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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The application runs as a multi-container setup:
- **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
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
3 changes: 3 additions & 0 deletions 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
2 changes: 1 addition & 1 deletion cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.atomgraph</groupId>
<artifactId>linkeddatahub-cli</artifactId>
<version>6.1.0</version>
<version>6.1.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>LinkedDataHub CLI</name>
Expand Down
65 changes: 65 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ secrets:
file: ./secrets/secretary_cert_password.txt
client_truststore_password:
file: ./secrets/client_truststore_password.txt
# openai_api_key:
# file: ./secrets/openai_api_key.txt
# google_client_id:
# file: ./secrets/google/client_id.txt
# google_client_secret:
Expand All @@ -28,6 +30,8 @@ services:
depends_on:
linkeddatahub:
condition: service_healthy
web-algebra:
condition: service_started # its upstream has to resolve when nginx starts
ports:
- ${HTTP_PORT}:8080 # allow Tomcat to do HTTP to HTTPS redirect
- ${HTTPS_PORT}:8443 # HTTPS
Expand Down Expand Up @@ -149,6 +153,38 @@ services:
target: /etc/squid/squid.conf
expose:
- 3128
web-algebra: # runs Web-Algebra plans against this instance, and writes them from natural language for the assistant drawer
image: ghcr.io/atomgraph/webalgebra-server-ldh:${WEBALGEBRA_VERSION:-latest}
mem_limit: 1024m
restart: on-failure
depends_on:
- egress
expose:
- 8080 # internal only: nginx is the sole client, and the server trusts the Client-Cert header only because nginx sets it
environment:
# the origin that receives an identity - this instance, with every dataspace subdomain under it. Its public name is
# not a route to it from inside the network, so requests for it go to nginx's client-cert listener under that name
- WEBALGEBRA_TRUSTED_ORIGIN=${PROTOCOL}://${HOST}:${HTTPS_PORT}
- WEBALGEBRA_PROXY_HOST=nginx
- WEBALGEBRA_PROXY_PORT=9443
# the secretary's WebID: the agent every user's WebID document already delegates (root-owner.trig.template, SignUp),
# so a plan submitted by a user runs under that user's own access control, not the secretary's
- WEBALGEBRA_KEYSTORE=/var/webalgebra/ssl/secretary/keystore.p12
- WEBALGEBRA_KEYSTORE_PASSWORD_FILE=/run/secrets/secretary_cert_password
- WEBALGEBRA_TRUSTSTORE=/var/webalgebra/ssl/server/server.crt # the self-signed server certificate, as PEM
# the model key is optional, like the OAuth secrets: a deployment that writes plans declares the openai_api_key
# secret and this variable in its override; without them the service runs plans and declines to write any
# - OPENAI_API_KEY_FILE=/run/secrets/openai_api_key
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o}
# public destinations (SPARQL endpoints a plan names, the model's API) go through egress like the platform's own; nginx
# is reached directly, since egress refuses internal addresses
- JAVA_TOOL_OPTIONS=-Dhttp.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttp.proxyPort=3128 -Dhttps.proxyHost=${EGRESS_PROXY_HOST:-egress} -Dhttps.proxyPort=3128 -Dhttp.nonProxyHosts=nginx
secrets:
- secretary_cert_password
# - openai_api_key
volumes:
- ./ssl/secretary:/var/webalgebra/ssl/secretary:ro
- ./ssl/server:/var/webalgebra/ssl/server:ro
varnish-frontend:
image: varnish:7.7.3
user: root # otherwise varnish user does not have permissions to the mounted folder which is owner by root
Expand Down Expand Up @@ -200,6 +236,10 @@ configs:
}

http {
upstream web-algebra {
server web-algebra:8080;
}

upstream linkeddatahub {
server ${NGINX_UPSTREAM_SERVER:-varnish-frontend:6060};
}
Expand All @@ -217,6 +257,7 @@ configs:

limit_req_zone $$limit_key zone=linked_data:10m rate=15r/s;
limit_req_zone $$limit_key zone=static_files:10m rate=20r/s;
limit_req_zone $$limit_key zone=web_algebra:10m rate=5r/s; # a plan is a model call; polling is what bursts
limit_req_status 429;

client_max_body_size ${MAX_CONTENT_LENGTH:-2097152};
Expand All @@ -231,6 +272,30 @@ configs:
ssl_prefer_server_ciphers on;
ssl_verify_client ${NGINX_SSL_VERIFY_CLIENT:-optional_no_ca};

# the assistant's plan service. A reserved path, like /static/ and /uploads/: prefix-matched ahead of the
# generic location and never rewritten, so /webalgebra, /webalgebra/plans and /webalgebra/{id}/result reach the
# sidecar as they are. Reachable only with a WebID certificate on the connection: the sidecar acts for the
# WebID in the certificate nginx forwards, and a caller without one has nobody to act for. On-Behalf-Of is
# cleared for the same reason Client-Cert is - identity comes from the connection, never from a header
location ^~ /webalgebra {
if ($$ssl_client_verify = NONE) {
return 401;
}

proxy_pass http://web-algebra;
limit_req zone=web_algebra burst=10 nodelay;
proxy_read_timeout 120s; # writing a plan is a 10-30 s model call

proxy_set_header Host $$host;
proxy_set_header X-Forwarded-Host $$host;
proxy_set_header X-Forwarded-Proto $$scheme;
proxy_set_header X-Forwarded-Port ${HTTPS_PORT};

proxy_set_header Client-Cert '';
proxy_set_header Client-Cert $$ssl_client_escaped_cert;
proxy_set_header On-Behalf-Of '';
}

location / {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS" always;
Expand Down
19 changes: 15 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>com.atomgraph</groupId>
<artifactId>linkeddatahub</artifactId>
<version>6.1.0</version>
<version>6.1.1-SNAPSHOT</version>
<packaging>${packaging.type}</packaging>

<name>AtomGraph LinkedDataHub</name>
Expand Down Expand Up @@ -46,7 +46,7 @@
<url>https://github.com/AtomGraph/LinkedDataHub</url>
<connection>scm:git:git://github.com/AtomGraph/LinkedDataHub.git</connection>
<developerConnection>scm:git:git@github.com:AtomGraph/LinkedDataHub.git</developerConnection>
<tag>linkeddatahub-6.1.0</tag>
<tag>linkeddatahub-5.5.4</tag>
</scm>

<repositories>
Expand Down Expand Up @@ -169,13 +169,13 @@
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>client</artifactId>
<version>6.0.3</version>
<version>6.0.4</version>
<classifier>classes</classifier>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>client</artifactId>
<version>6.0.3</version>
<version>6.0.4</version>
<type>war</type>
</dependency>
<dependency>
Expand Down Expand Up @@ -399,6 +399,17 @@
<webappDirectory>${project.build.directory}/${build.warName}</webappDirectory>
<failOnMissingWebXml>true</failOnMissingWebXml>
<attachClasses>true</attachClasses>
<!-- the agent guide is served at /AGENTS.md on every dataspace origin (web.xml maps it to the
default servlet), so a query-writing client can read how the endpoint is laid out; one file,
kept at the repository root where agents expect it, and shipped from there -->
<webResources>
<resource>
<directory>${basedir}</directory>
<includes>
<include>AGENTS.md</include>
</includes>
</resource>
</webResources>
<overlays>
<overlay>
<groupId>${project.groupId}</groupId>
Expand Down
2 changes: 1 addition & 1 deletion rdf/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.atomgraph</groupId>
<artifactId>linkeddatahub-rdf</artifactId>
<version>6.1.0</version>
<version>6.1.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>LinkedDataHub RDF</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ private LDH() { }
public static final Resource CSVImport = ResourceFactory.createResource(NS + "CSVImport");
/** ldh:RDFImport class */
public static final Resource RDFImport = ResourceFactory.createResource(NS + "RDFImport");
/** ldh:Chat class: a conversation with the assistant, its turns the rdf:_N members */
public static final Resource Chat = ResourceFactory.createResource(NS + "Chat");
/** ldh:ChatTurn class: one question and what came of it */
public static final Resource ChatTurn = ResourceFactory.createResource(NS + "ChatTurn");
/** ldh:MissingPropertyValue constraint class */
public static final Resource MissingPropertyValue = ResourceFactory.createResource(NS + "MissingPropertyValue");
/** ldh:ChildrenView resource */
Expand All @@ -62,6 +66,16 @@ private LDH() { }
public static final Property delimiter = ResourceFactory.createProperty(NS + "delimiter");
/** ldh:chartType property */
public static final Property chartType = ResourceFactory.createProperty(NS + "chartType");
/** ldh:question property: what a chat turn asked */
public static final Property question = ResourceFactory.createProperty(NS + "question");
/** ldh:answer property: the assistant's answer in words */
public static final Property answer = ResourceFactory.createProperty(NS + "answer");
/** ldh:plan property: the turn's Web-Algebra plan, an rdf:XMLLiteral */
public static final Property plan = ResourceFactory.createProperty(NS + "plan");
/** ldh:execution property: what running the plan reported, an rdf:XMLLiteral */
public static final Property execution = ResourceFactory.createProperty(NS + "execution");
/** ldh:outcome property: how the turn went, in one line */
public static final Property outcome = ResourceFactory.createProperty(NS + "outcome");
/** ldh:categoryVarName property */
public static final Property categoryVarName = ResourceFactory.createProperty(NS + "categoryVarName");
/** ldh:seriesVarName property */
Expand Down
Loading
Loading