Skip to content

Commit 75c8564

Browse files
committed
Prove Datalog product flow through packaged runtime
Add a daemon-level product-flow test that boots an install-style runtime, arms the packaged admin permissions, writes and queries facts, restarts for replay, and verifies degraded graph isolation. Document the tested operator flow and keep static packaged units compatible with builds that omit fact-store support. Constraint: Public artifacts must describe product behavior without internal tracker references. Rejected: In-process-only coverage | it would not prove the installed daemon and client flow. Rejected: Static unit --fact-root arguments | they break package smoke coverage for builds without fact-store support. Confidence: high Scope-risk: moderate Directive: Keep policy, audit, and fact storage paths physically separate in packaged deployments. Tested: product-flow packaged runtime suite; no-fact-store packaged readiness; default full test suite; fact-store replay coverage. Not-tested: Full cross-platform CI matrix before PR.
1 parent 960427a commit 75c8564

4 files changed

Lines changed: 467 additions & 0 deletions

File tree

docs/operator-runbook.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ support files from `packaging/`.
1515
systemd as credential `wyrelog-system-policy-key`
1616
- System policy store: `/var/lib/wyrelog/system/policy.sqlite`
1717
- System audit store: `/var/log/wyrelog/system/audit.duckdb`
18+
- System Datalog fact root: `/var/lib/wyrelog/system/facts`
1819
- Service KeyProvider root: `/etc/wyrelog/service/policy.key` loaded by
1920
systemd as credential `wyrelog-service-policy-key`
2021
- Service policy store: `/var/lib/wyrelog/service/policy.sqlite`
2122
- Service audit store: `/var/log/wyrelog/service/audit.duckdb`
23+
- Service Datalog fact root: `/var/lib/wyrelog/service/facts`
2224
- Runtime directory: `/run/wyrelog`
2325
- HTTP listen port: `127.0.0.1:8765` unless overridden by the service file
2426
- Production log policy: compile release builds with
@@ -40,9 +42,11 @@ Packaged profile paths:
4042
- System policy store: `/var/lib/wyrelog/system/policy.sqlite`
4143
- System KeyProvider root: `/etc/wyrelog/system/policy.key`
4244
- System audit store: `/var/log/wyrelog/system/audit.duckdb`
45+
- System Datalog fact root: `/var/lib/wyrelog/system/facts`
4346
- Service policy store: `/var/lib/wyrelog/service/policy.sqlite`
4447
- Service KeyProvider root: `/etc/wyrelog/service/policy.key`
4548
- Service audit store: `/var/log/wyrelog/service/audit.duckdb`
49+
- Service Datalog fact root: `/var/lib/wyrelog/service/facts`
4650
- Service event spool: `/var/lib/wyrelog/service/event-spool`
4751

4852
Inspect the resolved profile contract with:
@@ -86,6 +90,7 @@ PY
8690
--policy-db /var/lib/wyrelog/system/policy.sqlite \
8791
--policy-keyprovider file:/etc/wyrelog/system/policy.key \
8892
--audit-db /var/log/wyrelog/system/audit.duckdb \
93+
--fact-root /var/lib/wyrelog/system/facts \
8994
--check
9095
wyrelogd --template-info --template-dir /usr/share/wyrelog/access
9196
wyctl key status --keyprovider /etc/wyrelog/system/policy.key
@@ -235,6 +240,113 @@ wyctl --daemon-url http://127.0.0.1:8765 audit query \
235240
--access-token-file /run/wyrelog/operator.token
236241
```
237242

243+
## Datalog Product Flow
244+
245+
Wyrelog is a Datalog storage and inference engine. The packaged access-control
246+
policy is the default policy template for the daemon, while Datalog facts live in
247+
separate per-tenant, per-graph stores. Keep these paths physically separate:
248+
249+
- Policy DB: encrypted SQLite authority store, for example
250+
`/var/lib/wyrelog/system/policy.sqlite`.
251+
- Audit DB: DuckDB audit sink, for example
252+
`/var/log/wyrelog/system/audit.duckdb`.
253+
- Fact DBs: DuckDB files below the fact root, for example
254+
`/var/lib/wyrelog/system/facts/<tenant>/<graph>/facts.duckdb`.
255+
256+
Back up and restore those stores as separate artifacts. Do not place the policy
257+
or audit DB under the fact root. The static packaged units rely on the daemon's
258+
profile defaults for the fact root so the same unit files remain valid for
259+
builds with and without fact-store support; pass `--fact-root` explicitly in
260+
manual checks or local deployments that enable Datalog fact storage.
261+
262+
The commands below show a complete local product flow on the default tenant.
263+
Replace `alice` and the paths for your deployment.
264+
265+
```sh
266+
BASE_URL=http://127.0.0.1:8765
267+
TOKEN=/run/wyrelog/operator.token
268+
TENANT=__wr_default
269+
GRAPH=orders
270+
271+
wyrelogd --production \
272+
--profile system \
273+
--template-dir /usr/share/wyrelog/access \
274+
--policy-db /var/lib/wyrelog/system/policy.sqlite \
275+
--policy-keyprovider file:/etc/wyrelog/system/policy.key \
276+
--audit-db /var/log/wyrelog/system/audit.duckdb \
277+
--fact-root /var/lib/wyrelog/system/facts \
278+
--bootstrap-admin-subject alice \
279+
--bootstrap-admin-allow-skip-mfa \
280+
--listen-port 8765
281+
```
282+
283+
Mint the first token and arm the packaged administrator's Datalog authorities on
284+
the tenant scope. The bootstrap role already grants these permissions; the
285+
permission-state transition records that the operator intentionally armed them
286+
for this scope.
287+
288+
```sh
289+
python3 - <<'PY'
290+
import json, urllib.request
291+
url = "http://127.0.0.1:8765/auth/login?username=alice&tenant=__wr_default&skip_mfa=true"
292+
req = urllib.request.Request(url, method="POST")
293+
with urllib.request.urlopen(req) as response:
294+
token = json.load(response)["access_token"]
295+
open("/run/wyrelog/operator.token", "w", encoding="utf-8").write(token + "\n")
296+
PY
297+
298+
for perm in wr.graph.manage wr.schema.manage wr.fact.write wr.datalog.query; do
299+
curl -fsS -X POST \
300+
-H "Authorization: Bearer $(cat "$TOKEN")" \
301+
"$BASE_URL/policy/permissions/transition?subject=alice&perm=$perm&scope=$TENANT&event=grant&guard_timestamp=$(date +%s)&guard_loc_class=trusted&guard_risk=29"
302+
done
303+
```
304+
305+
Run the graph, schema, fact, and query commands through `wyctl`:
306+
307+
```sh
308+
wyctl --daemon-url "$BASE_URL" graph create \
309+
--tenant "$TENANT" --graph "$GRAPH" \
310+
--access-token-file "$TOKEN" \
311+
--guard-timestamp $(date +%s) --guard-loc-class trusted --guard-risk 29
312+
313+
wyctl --daemon-url "$BASE_URL" fact schema register \
314+
--tenant "$TENANT" --graph "$GRAPH" \
315+
--namespace shop --relation orders --schema-version 1 \
316+
--columns order_id:symbol,amount:int64 \
317+
--access-token-file "$TOKEN" \
318+
--guard-timestamp $(date +%s) --guard-loc-class trusted --guard-risk 29
319+
320+
printf 'order_id,amount\no-1,42\n' >/tmp/orders.csv
321+
wyctl --daemon-url "$BASE_URL" fact put \
322+
--tenant "$TENANT" --graph "$GRAPH" \
323+
--namespace shop --relation orders --schema-version 1 \
324+
--batch-id orders-1 --idempotency-key orders-1 \
325+
--format csv --input /tmp/orders.csv \
326+
--access-token-file "$TOKEN" \
327+
--guard-timestamp $(date +%s) --guard-loc-class trusted --guard-risk 29
328+
329+
wyctl --daemon-url "$BASE_URL" datalog query \
330+
--tenant "$TENANT" --graph "$GRAPH" \
331+
--query 'orders(O,A)' --output json --limit 10 \
332+
--access-token-file "$TOKEN" \
333+
--guard-timestamp $(date +%s) --guard-loc-class trusted --guard-risk 29
334+
```
335+
336+
To verify recovery, restart `wyrelogd` with the same policy DB, audit DB, key,
337+
and fact root. Mint a fresh token after restart and run the same
338+
`wyctl datalog query`; the fact graph is replayed from the per-graph DuckDB fact
339+
store. Check graph health with:
340+
341+
```sh
342+
curl -fsS "$BASE_URL/facts/status"
343+
```
344+
345+
A single corrupted graph should report a degraded graph entry while unrelated
346+
graphs remain queryable. Stop the daemon before repairing or replacing a damaged
347+
`facts.duckdb`, restore only the affected `<tenant>/<graph>` fact directory,
348+
restart, then confirm `/facts/status` returns `"status":"ready"`.
349+
238350
## Day-2 Operations
239351

240352
- Template validation from an operator shell. Use `file:` for manual checks;

tests/check-packaged-install-readiness.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,76 @@ if ! grep -q -- "/var/lib/wyrelog/service/facts" \
7878
echo "tmpfiles does not create the service fact root" >&2
7979
exit 1
8080
fi
81+
if ! grep -q '^d /var/lib/wyrelog/system/facts 0700 wyrelog wyrelog -$' \
82+
"$SOURCE_ROOT/packaging/tmpfiles.d/wyrelog.conf"; then
83+
echo "tmpfiles system fact root does not enforce 0700 wyrelog ownership" >&2
84+
exit 1
85+
fi
86+
if ! grep -q '^d /var/lib/wyrelog/service/facts 0700 wyrelog wyrelog -$' \
87+
"$SOURCE_ROOT/packaging/tmpfiles.d/wyrelog.conf"; then
88+
echo "tmpfiles service fact root does not enforce 0700 wyrelog ownership" >&2
89+
exit 1
90+
fi
91+
if ! grep -q -- "ReadWritePaths=/var/lib/wyrelog/system" \
92+
"$SOURCE_ROOT/packaging/systemd/wyrelog-system.service"; then
93+
echo "system unit does not allow writes under the system state root" >&2
94+
exit 1
95+
fi
96+
if ! grep -q -- "ReadWritePaths=/var/lib/wyrelog/service" \
97+
"$SOURCE_ROOT/packaging/systemd/wyrelog-service.service"; then
98+
echo "service unit does not allow writes under the service state root" >&2
99+
exit 1
100+
fi
81101
if ! grep -q "WYL_LOG=warn" \
82102
"$SOURCE_ROOT/packaging/systemd/wyrelog.service"; then
83103
echo "service unit does not set production log ceiling" >&2
84104
exit 1
85105
fi
86106

107+
if grep -q -- "--fact-root" "$SOURCE_ROOT/packaging/systemd/wyrelog.service" \
108+
"$SOURCE_ROOT/packaging/systemd/wyrelog-system.service" \
109+
"$SOURCE_ROOT/packaging/systemd/wyrelog-service.service"; then
110+
echo "static units must rely on profile fact-root defaults for build compatibility" >&2
111+
exit 1
112+
fi
113+
114+
SYSTEM_PROFILE_INFO="$TMPDIR/system-profile-info.out"
115+
SERVICE_PROFILE_INFO="$TMPDIR/service-profile-info.out"
116+
"$WYRELOGD" --profile=system --profile-info --production >"$SYSTEM_PROFILE_INFO"
117+
"$WYRELOGD" --profile=service --profile-info --production >"$SERVICE_PROFILE_INFO"
118+
grep -q '^policy_db=/var/lib/wyrelog/system/policy.sqlite$' "$SYSTEM_PROFILE_INFO"
119+
grep -q '^audit_db=/var/log/wyrelog/system/audit.duckdb$' "$SYSTEM_PROFILE_INFO"
120+
grep -q '^policy_db=/var/lib/wyrelog/service/policy.sqlite$' "$SERVICE_PROFILE_INFO"
121+
grep -q '^audit_db=/var/log/wyrelog/service/audit.duckdb$' "$SERVICE_PROFILE_INFO"
122+
if grep -q '^fact_root=.' "$SYSTEM_PROFILE_INFO"; then
123+
grep -q '^fact_root=/var/lib/wyrelog/system/facts$' "$SYSTEM_PROFILE_INFO"
124+
grep -q '^fact_root=/var/lib/wyrelog/service/facts$' "$SERVICE_PROFILE_INFO"
125+
fi
126+
"$PYTHON" - "$SYSTEM_PROFILE_INFO" "$SERVICE_PROFILE_INFO" <<'EOF2'
127+
import sys
128+
129+
def load(path):
130+
out = {}
131+
for line in open(path, encoding='utf-8'):
132+
line = line.strip()
133+
if '=' in line:
134+
k, v = line.split('=', 1)
135+
out[k] = v
136+
return out
137+
for path in sys.argv[1:]:
138+
info = load(path)
139+
values = [info['policy_db'], info['audit_db']]
140+
fact_root = info.get('fact_root')
141+
if fact_root:
142+
values.append(fact_root)
143+
if len(set(values)) != len(values):
144+
raise SystemExit(f'profile paths overlap in {path}: {values}')
145+
if fact_root:
146+
fact = fact_root.rstrip('/') + '/'
147+
for key in ('policy_db', 'audit_db'):
148+
if info[key].startswith(fact):
149+
raise SystemExit(f'{key} is inside fact root in {path}')
150+
EOF2
87151
INSTALL_ROOT="$TMPDIR/install"
88152
mkdir -p "$INSTALL_ROOT/usr/share/wyrelog" \
89153
"$INSTALL_ROOT/usr/share/wyrelog/tools" \

0 commit comments

Comments
 (0)