dent8 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dent8-0.6.0/.gitignore ADDED
@@ -0,0 +1,40 @@
1
+ /target/
2
+
3
+ # Local runtime configuration.
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+
8
+ # Project-local dogfood/adoption state. These contain machine-local paths, grants,
9
+ # private source keys, local stores, and agent MCP config.
10
+ .dent8/
11
+ .dent8
12
+ .codex/
13
+ .cursor/
14
+ .grok/
15
+ .claude/*
16
+ !.claude/settings.json
17
+ /.mcp.json
18
+
19
+ # The CLI's default file-backed event log (DENT8_LOG) and authority registry (DENT8_AUTHORITY).
20
+ /dent8-log.jsonl
21
+ dent8-log.jsonl
22
+ /dent8-authority.json
23
+ dent8-authority.json
24
+
25
+ # Witness keys + signed-tree-head log (DENT8_WITNESS_KEY / DENT8_WITNESS_LOG).
26
+ dent8-witness.key
27
+ dent8-witness.key.pub
28
+ dent8-witness.jsonl
29
+
30
+ # The `dent8 export` default Parquet output (--features export).
31
+ /dent8-events.parquet
32
+ dent8-events.parquet
33
+
34
+ # Python bytecode (from running the example scripts / tooling).
35
+ __pycache__/
36
+ *.pyc
37
+
38
+ # Editor and OS noise.
39
+ .DS_Store
40
+ *.swp
dent8-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: dent8
3
+ Version: 0.6.0
4
+ Summary: A memory firewall for coding agents — thin Python SDK over the dent8 CLI's JSON contract.
5
+ Project-URL: Homepage, https://github.com/xyzzylabs/dent8
6
+ Project-URL: Changelog, https://github.com/xyzzylabs/dent8/blob/main/CHANGELOG.md
7
+ Author: xyzzylabs
8
+ License-Expression: Apache-2.0 OR MIT
9
+ Keywords: agent,firewall,mcp,memory,provenance
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # dent8 (Python)
18
+
19
+ A memory firewall for coding agents — the thin Python SDK.
20
+
21
+ Every call runs the [`dent8`](https://github.com/xyzzylabs/dent8) binary with
22
+ `--output json` and returns the parsed payload as a `dict`. The wire contract is
23
+ the product: each payload carries `schema_version`; every error carries a stable
24
+ `status` plus a machine-readable `code` (`insufficient-authority`,
25
+ `authority-ceiling`, `content-rejected`, …), raised as `Dent8Rejected` /
26
+ `Dent8Invalid` so you branch on `error.code`, never on prose.
27
+
28
+ Because the CLI resolves the store itself (repo-confined `.dent8/` discovery,
29
+ `DENT8_LOG` / `DENT8_STORE_URL`), routes writes through a local daemon when
30
+ `DENT8_DAEMON_SOCKET` is set, and defaults `--source`/`--authority` from the
31
+ active signed grant, the SDK inherits all of it with zero configuration.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ pip install dent8
37
+ cargo install dent8-cli --locked # the binary the SDK drives
38
+ ```
39
+
40
+ ## Use
41
+
42
+ ```python
43
+ from dent8 import Dent8, Dent8Rejected
44
+
45
+ d8 = Dent8() # finds `dent8` on PATH; Dent8(binary=..., env={"DENT8_LOG": ...}) to pin
46
+
47
+ d8.assert_fact("repo:myproj", "database", "postgres",
48
+ authority="high", source="user:alice")
49
+
50
+ try:
51
+ d8.supersede("repo:myproj", "database", "mysql",
52
+ authority="low", source="web:scrape")
53
+ except Dent8Rejected as rejection:
54
+ assert rejection.code == "insufficient-authority" # branch on the code
55
+
56
+ fact = d8.explain("repo:myproj", "database")
57
+ assert fact["value"]["text"] == "postgres" # the firewall held
58
+
59
+ report = d8.verify() # findings are a result: status ok | integrity_issues
60
+ disputes = d8.conflicts() # status contested when live disputes exist
61
+ ```
62
+
63
+ The belief surface maps 1:1 onto the CLI: `assert_fact` (Python keyword), `supersede`,
64
+ `contradict`, `retract`, `reinforce`, `expire`, `derive(basis=(subject, predicate))`,
65
+ `explain`, `replay`, `facts`, `verify`, `conflicts`. Temporal keywords accept the CLI's
66
+ whole grammar — unix millis, `"now"`, `"-7d"`, RFC 3339, or a bare UTC date.
67
+
68
+ For LLM tool-calling agents, prefer the MCP server (`dent8 mcp serve`) — see
69
+ [examples/langchain](https://github.com/xyzzylabs/dent8/tree/main/examples/langchain) and
70
+ [examples/vercel-ai-sdk](https://github.com/xyzzylabs/dent8/tree/main/examples/vercel-ai-sdk). This SDK is for
71
+ *programmatic* access from Python code.
72
+
73
+ ## Test
74
+
75
+ ```sh
76
+ cargo build -p dent8-cli
77
+ DENT8_BIN=../../target/debug/dent8 python -m pytest
78
+ ```
dent8-0.6.0/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # dent8 (Python)
2
+
3
+ A memory firewall for coding agents — the thin Python SDK.
4
+
5
+ Every call runs the [`dent8`](https://github.com/xyzzylabs/dent8) binary with
6
+ `--output json` and returns the parsed payload as a `dict`. The wire contract is
7
+ the product: each payload carries `schema_version`; every error carries a stable
8
+ `status` plus a machine-readable `code` (`insufficient-authority`,
9
+ `authority-ceiling`, `content-rejected`, …), raised as `Dent8Rejected` /
10
+ `Dent8Invalid` so you branch on `error.code`, never on prose.
11
+
12
+ Because the CLI resolves the store itself (repo-confined `.dent8/` discovery,
13
+ `DENT8_LOG` / `DENT8_STORE_URL`), routes writes through a local daemon when
14
+ `DENT8_DAEMON_SOCKET` is set, and defaults `--source`/`--authority` from the
15
+ active signed grant, the SDK inherits all of it with zero configuration.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ pip install dent8
21
+ cargo install dent8-cli --locked # the binary the SDK drives
22
+ ```
23
+
24
+ ## Use
25
+
26
+ ```python
27
+ from dent8 import Dent8, Dent8Rejected
28
+
29
+ d8 = Dent8() # finds `dent8` on PATH; Dent8(binary=..., env={"DENT8_LOG": ...}) to pin
30
+
31
+ d8.assert_fact("repo:myproj", "database", "postgres",
32
+ authority="high", source="user:alice")
33
+
34
+ try:
35
+ d8.supersede("repo:myproj", "database", "mysql",
36
+ authority="low", source="web:scrape")
37
+ except Dent8Rejected as rejection:
38
+ assert rejection.code == "insufficient-authority" # branch on the code
39
+
40
+ fact = d8.explain("repo:myproj", "database")
41
+ assert fact["value"]["text"] == "postgres" # the firewall held
42
+
43
+ report = d8.verify() # findings are a result: status ok | integrity_issues
44
+ disputes = d8.conflicts() # status contested when live disputes exist
45
+ ```
46
+
47
+ The belief surface maps 1:1 onto the CLI: `assert_fact` (Python keyword), `supersede`,
48
+ `contradict`, `retract`, `reinforce`, `expire`, `derive(basis=(subject, predicate))`,
49
+ `explain`, `replay`, `facts`, `verify`, `conflicts`. Temporal keywords accept the CLI's
50
+ whole grammar — unix millis, `"now"`, `"-7d"`, RFC 3339, or a bare UTC date.
51
+
52
+ For LLM tool-calling agents, prefer the MCP server (`dent8 mcp serve`) — see
53
+ [examples/langchain](https://github.com/xyzzylabs/dent8/tree/main/examples/langchain) and
54
+ [examples/vercel-ai-sdk](https://github.com/xyzzylabs/dent8/tree/main/examples/vercel-ai-sdk). This SDK is for
55
+ *programmatic* access from Python code.
56
+
57
+ ## Test
58
+
59
+ ```sh
60
+ cargo build -p dent8-cli
61
+ DENT8_BIN=../../target/debug/dent8 python -m pytest
62
+ ```
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dent8"
7
+ version = "0.6.0"
8
+ description = "A memory firewall for coding agents — thin Python SDK over the dent8 CLI's JSON contract."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0 OR MIT"
12
+ authors = [{ name = "xyzzylabs" }]
13
+ keywords = ["agent", "memory", "provenance", "firewall", "mcp"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Libraries",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/xyzzylabs/dent8"
23
+ Changelog = "https://github.com/xyzzylabs/dent8/blob/main/CHANGELOG.md"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/dent8"]
@@ -0,0 +1,331 @@
1
+ """dent8 — a memory firewall for coding agents, from Python.
2
+
3
+ A deliberately thin SDK: every call runs the ``dent8`` binary with ``--output json``
4
+ and returns the parsed payload as a ``dict``. The wire contract is the product —
5
+ each payload carries ``schema_version``; every error carries a stable ``status``
6
+ (``rejected`` / ``invalid``) and a machine-readable ``code``
7
+ (``insufficient-authority``, ``authority-ceiling``, ``content-rejected``, …), raised
8
+ here as :class:`Dent8Rejected` / :class:`Dent8Invalid` so callers branch on
9
+ ``error.code`` instead of parsing prose.
10
+
11
+ Because the CLI itself resolves the store (repo-confined ``.dent8/`` discovery,
12
+ ``DENT8_LOG`` / ``DENT8_STORE_URL`` env), routes writes through a local daemon when
13
+ ``DENT8_DAEMON_SOCKET`` is set, and defaults ``--source`` / ``--authority`` from the
14
+ active signed grant, this SDK inherits all of that for free.
15
+
16
+ from dent8 import Dent8
17
+
18
+ d8 = Dent8()
19
+ d8.assert_fact("repo:myproj", "database", "postgres",
20
+ authority="high", source="user:alice")
21
+ fact = d8.explain("repo:myproj", "database")
22
+ assert fact["status"] == "ok"
23
+
24
+ Requires the ``dent8`` binary on ``PATH`` (``cargo install dent8-cli --locked``) or an
25
+ explicit ``Dent8(binary=...)``.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import subprocess
33
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
34
+
35
+ __all__ = [
36
+ "Dent8",
37
+ "Dent8Error",
38
+ "Dent8Invalid",
39
+ "Dent8Rejected",
40
+ "SCHEMA_VERSION",
41
+ ]
42
+
43
+ #: The JSON output shape this SDK was written against (the CLI stamps it on every payload).
44
+ SCHEMA_VERSION = 1
45
+
46
+ #: A wall-clock instant, in any grammar the CLI accepts: unix millis (int), ``"now"``,
47
+ #: a ±duration offset (``"-7d"``), RFC 3339, or a bare UTC date/datetime.
48
+ Time = Union[int, str]
49
+
50
+
51
+ class Dent8Error(Exception):
52
+ """A failed dent8 operation. ``status`` says what happened (``rejected`` /
53
+ ``invalid``), ``code`` says why (stable kebab-case, e.g.
54
+ ``insufficient-authority``), ``payload`` is the full JSON error object."""
55
+
56
+ def __init__(self, payload: Dict[str, Any]) -> None:
57
+ self.payload = payload
58
+ self.status: str = payload.get("status", "failed")
59
+ self.code: str = payload.get("code", "operation-failed")
60
+ self.message: str = payload.get("message", payload.get("error_reason", ""))
61
+ super().__init__(f"[{self.code}] {self.message}")
62
+
63
+
64
+ class Dent8Rejected(Dent8Error):
65
+ """The firewall (or a write-boundary gate) refused a well-formed request."""
66
+
67
+
68
+ class Dent8Invalid(Dent8Error):
69
+ """The request was malformed or the configuration unusable."""
70
+
71
+
72
+ class Dent8:
73
+ """A handle on the dent8 belief surface, one subprocess per call.
74
+
75
+ :param binary: path to the ``dent8`` binary (default: ``dent8`` on ``PATH``, or
76
+ the ``DENT8_BIN`` environment variable when set).
77
+ :param cwd: working directory for store discovery (default: the process cwd).
78
+ :param env: extra environment entries merged over ``os.environ`` — e.g.
79
+ ``{"DENT8_LOG": "/tmp/memory.jsonl"}`` to pin a store instead of discovery.
80
+ :param timeout: per-call subprocess timeout in seconds.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ binary: Optional[str] = None,
86
+ *,
87
+ cwd: Optional[str] = None,
88
+ env: Optional[Dict[str, str]] = None,
89
+ timeout: float = 60.0,
90
+ ) -> None:
91
+ self.binary = binary or os.environ.get("DENT8_BIN", "dent8")
92
+ self.cwd = cwd
93
+ self.env = env or {}
94
+ self.timeout = timeout
95
+
96
+ # ---- writes ---------------------------------------------------------------
97
+
98
+ def assert_fact(
99
+ self,
100
+ subject: str,
101
+ predicate: str,
102
+ value: str,
103
+ *,
104
+ authority: Optional[str] = None,
105
+ source: Optional[str] = None,
106
+ valid_from: Optional[Time] = None,
107
+ valid_to: Optional[Time] = None,
108
+ ttl: Optional[str] = None,
109
+ ) -> Dict[str, Any]:
110
+ """Assert a fact through the firewall. Named ``assert_fact`` because
111
+ ``assert`` is a Python keyword; every other verb matches the CLI 1:1."""
112
+ return self._run(
113
+ "assert",
114
+ subject,
115
+ predicate,
116
+ value,
117
+ authority=authority,
118
+ source=source,
119
+ valid_from=valid_from,
120
+ valid_to=valid_to,
121
+ ttl=ttl,
122
+ )
123
+
124
+ def supersede(
125
+ self,
126
+ subject: str,
127
+ predicate: str,
128
+ value: str,
129
+ *,
130
+ authority: Optional[str] = None,
131
+ source: Optional[str] = None,
132
+ valid_from: Optional[Time] = None,
133
+ valid_to: Optional[Time] = None,
134
+ ttl: Optional[str] = None,
135
+ ) -> Dict[str, Any]:
136
+ """Revise the believed fact via the sanctioned supersession path."""
137
+ return self._run(
138
+ "supersede",
139
+ subject,
140
+ predicate,
141
+ value,
142
+ authority=authority,
143
+ source=source,
144
+ valid_from=valid_from,
145
+ valid_to=valid_to,
146
+ ttl=ttl,
147
+ )
148
+
149
+ def contradict(
150
+ self,
151
+ subject: str,
152
+ predicate: str,
153
+ value: str,
154
+ *,
155
+ authority: Optional[str] = None,
156
+ source: Optional[str] = None,
157
+ valid_from: Optional[Time] = None,
158
+ valid_to: Optional[Time] = None,
159
+ ttl: Optional[str] = None,
160
+ ) -> Dict[str, Any]:
161
+ """Record dissent: keep both facts, mark the pair contested."""
162
+ return self._run(
163
+ "contradict",
164
+ subject,
165
+ predicate,
166
+ value,
167
+ authority=authority,
168
+ source=source,
169
+ valid_from=valid_from,
170
+ valid_to=valid_to,
171
+ ttl=ttl,
172
+ )
173
+
174
+ def retract(
175
+ self,
176
+ subject: str,
177
+ predicate: str,
178
+ *,
179
+ authority: Optional[str] = None,
180
+ source: Optional[str] = None,
181
+ ) -> Dict[str, Any]:
182
+ """Retract the believed fact (terminal; taints derivatives)."""
183
+ return self._run("retract", subject, predicate, authority=authority, source=source)
184
+
185
+ def reinforce(
186
+ self,
187
+ subject: str,
188
+ predicate: str,
189
+ *,
190
+ authority: Optional[str] = None,
191
+ source: Optional[str] = None,
192
+ ) -> Dict[str, Any]:
193
+ """Corroborate the believed fact from another source (earned entrenchment)."""
194
+ return self._run("reinforce", subject, predicate, authority=authority, source=source)
195
+
196
+ def expire(
197
+ self,
198
+ subject: str,
199
+ predicate: str,
200
+ *,
201
+ authority: Optional[str] = None,
202
+ source: Optional[str] = None,
203
+ ) -> Dict[str, Any]:
204
+ """Expire the believed fact (terminal)."""
205
+ return self._run("expire", subject, predicate, authority=authority, source=source)
206
+
207
+ def derive(
208
+ self,
209
+ subject: str,
210
+ predicate: str,
211
+ value: str,
212
+ *,
213
+ basis: Tuple[str, str],
214
+ authority: Optional[str] = None,
215
+ source: Optional[str] = None,
216
+ valid_from: Optional[Time] = None,
217
+ valid_to: Optional[Time] = None,
218
+ ttl: Optional[str] = None,
219
+ ) -> Dict[str, Any]:
220
+ """Assert a fact derived from ``basis = (subject, predicate)``, recording the
221
+ dependency edge — if the basis is later retracted, this derivative is flagged
222
+ tainted by ``verify``."""
223
+ basis_subject, basis_predicate = basis
224
+ return self._run(
225
+ "derive",
226
+ subject,
227
+ predicate,
228
+ value,
229
+ "--basis",
230
+ basis_subject,
231
+ basis_predicate,
232
+ authority=authority,
233
+ source=source,
234
+ valid_from=valid_from,
235
+ valid_to=valid_to,
236
+ ttl=ttl,
237
+ )
238
+
239
+ # ---- reads / audit ----------------------------------------------------------
240
+
241
+ def explain(
242
+ self,
243
+ subject: str,
244
+ predicate: str,
245
+ *,
246
+ as_of: Optional[Time] = None,
247
+ valid_at: Optional[Time] = None,
248
+ ) -> Dict[str, Any]:
249
+ """The believed (or terminal) fact with its integrity receipt; time-travel
250
+ with ``as_of`` / ``valid_at``."""
251
+ return self._run("explain", subject, predicate, as_of=as_of, valid_at=valid_at)
252
+
253
+ def replay(
254
+ self,
255
+ subject: str,
256
+ predicate: str,
257
+ *,
258
+ as_of: Optional[Time] = None,
259
+ valid_at: Optional[Time] = None,
260
+ ) -> Dict[str, Any]:
261
+ """The full event history behind a fact — why it is believed."""
262
+ return self._run("replay", subject, predicate, as_of=as_of, valid_at=valid_at)
263
+
264
+ def facts(self, *, include_diagnostics: bool = False) -> Dict[str, Any]:
265
+ """Every known fact stream, with freshness flags."""
266
+ args: List[str] = ["facts", "list"]
267
+ if include_diagnostics:
268
+ args.append("--include-diagnostics")
269
+ return self._run(*args)
270
+
271
+ def verify(self) -> Dict[str, Any]:
272
+ """Integrity checks: hash chain, lineage, taint, attestations. Findings are a
273
+ *result*, not an exception: the payload's ``status`` is ``ok`` or
274
+ ``integrity_issues`` (mirroring the MCP tool). Only a malformed invocation
275
+ raises."""
276
+ try:
277
+ return self._run("verify")
278
+ except Dent8Rejected as error:
279
+ return error.payload
280
+
281
+ def conflicts(self) -> Dict[str, Any]:
282
+ """Contested facts. ``status`` is ``contested`` when disputes exist, ``ok``
283
+ when none do."""
284
+ return self._run("conflicts")
285
+
286
+ # ---- plumbing ---------------------------------------------------------------
287
+
288
+ def _run(self, *args: str, **flags: Optional[Union[Time, str]]) -> Dict[str, Any]:
289
+ command: List[str] = [self.binary, "--output", "json", *args]
290
+ for name, value in flags.items():
291
+ if value is None:
292
+ continue
293
+ command.append("--" + name.replace("_", "-"))
294
+ command.append(str(value))
295
+ completed = subprocess.run(
296
+ command,
297
+ capture_output=True,
298
+ text=True,
299
+ timeout=self.timeout,
300
+ cwd=self.cwd,
301
+ env={**os.environ, **self.env},
302
+ check=False,
303
+ )
304
+ payload = self._parse(completed, command)
305
+ if completed.returncode == 0:
306
+ return payload
307
+ raise (Dent8Invalid if completed.returncode == 2 else Dent8Rejected)(payload)
308
+
309
+ @staticmethod
310
+ def _parse(
311
+ completed: "subprocess.CompletedProcess[str]", command: Sequence[str]
312
+ ) -> Dict[str, Any]:
313
+ # The machine contract: every --output json result — success and error — is one
314
+ # JSON object on stdout. Anything else (e.g. a clap usage error on stderr) is
315
+ # surfaced as invalid with the raw text preserved.
316
+ text = completed.stdout.strip()
317
+ if text:
318
+ try:
319
+ payload = json.loads(text)
320
+ if isinstance(payload, dict):
321
+ return payload
322
+ except json.JSONDecodeError:
323
+ pass
324
+ raise Dent8Invalid(
325
+ {
326
+ "status": "invalid",
327
+ "code": "invalid-argument",
328
+ "message": (completed.stderr or completed.stdout or "").strip()
329
+ or f"no JSON output from: {' '.join(command)}",
330
+ }
331
+ )
@@ -0,0 +1,137 @@
1
+ """End-to-end tests against the real dent8 binary (the SDK is a thin wrapper, so the
2
+ binary IS the unit under test). Point DENT8_BIN at a built binary, or have `dent8` on
3
+ PATH; the suite skips cleanly otherwise:
4
+
5
+ cargo build -p dent8-cli
6
+ DENT8_BIN=../../target/debug/dent8 python -m pytest
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+
15
+ import pytest
16
+
17
+ from dent8 import SCHEMA_VERSION, Dent8, Dent8Invalid, Dent8Rejected
18
+
19
+ BINARY = os.environ.get("DENT8_BIN") or shutil.which("dent8")
20
+
21
+ pytestmark = pytest.mark.skipif(
22
+ BINARY is None, reason="no dent8 binary (set DENT8_BIN or install dent8-cli)"
23
+ )
24
+
25
+
26
+ @pytest.fixture()
27
+ def d8(tmp_path, monkeypatch):
28
+ """A client pinned to a throwaway store in permissive dev mode: every ambient
29
+ DENT8_* variable is scrubbed (the developer's own shell may carry a dogfood store
30
+ with identity enforcement), then the store paths are pinned to the tmp dir."""
31
+ for key in list(os.environ):
32
+ if key.startswith("DENT8_"):
33
+ monkeypatch.delenv(key)
34
+ env = {
35
+ "DENT8_LOG": str(tmp_path / "memory.jsonl"),
36
+ "DENT8_AUTHORITY": str(tmp_path / "authority.json"),
37
+ }
38
+ return Dent8(binary=BINARY, cwd=str(tmp_path), env=env)
39
+
40
+
41
+ def test_assert_explain_round_trip(d8):
42
+ written = d8.assert_fact(
43
+ "repo:myproj", "database", "postgres", authority="high", source="user:alice"
44
+ )
45
+ assert written["schema_version"] == SCHEMA_VERSION
46
+ assert written["status"] == "accepted"
47
+ assert written["accepted"] is True
48
+
49
+ fact = d8.explain("repo:myproj", "database")
50
+ assert fact["status"] == "ok"
51
+ assert fact["value"]["text"] == "postgres"
52
+ assert fact["authority"] == "high"
53
+
54
+
55
+ def test_firewall_rejection_carries_the_code(d8):
56
+ # An unregistered predicate exercises pure arbitration: low cannot displace high.
57
+ d8.assert_fact("person:alice", "favorite_drink", "tea", authority="high", source="user:alice")
58
+ with pytest.raises(Dent8Rejected) as exc:
59
+ d8.supersede("person:alice", "favorite_drink", "coffee", authority="low", source="note:old")
60
+ assert exc.value.status == "rejected"
61
+ assert exc.value.code == "insufficient-authority"
62
+ assert exc.value.payload["schema_version"] == SCHEMA_VERSION
63
+
64
+ # A registered predicate (`database` has a floor in the coding-agent registry) is
65
+ # refused by the policy gate first, with its own code.
66
+ d8.assert_fact("repo:myproj", "database", "postgres", authority="high", source="user:alice")
67
+ with pytest.raises(Dent8Rejected) as floor:
68
+ d8.supersede("repo:myproj", "database", "mysql", authority="low", source="web:scrape")
69
+ assert floor.value.code == "below-authority-floor"
70
+
71
+ # The incumbents survived both challenges.
72
+ assert d8.explain("person:alice", "favorite_drink")["value"]["text"] == "tea"
73
+ assert d8.explain("repo:myproj", "database")["value"]["text"] == "postgres"
74
+
75
+
76
+ def test_invalid_input_is_invalid_not_rejected(d8):
77
+ with pytest.raises(Dent8Invalid):
78
+ d8.assert_fact("no-colon", "p", "v", authority="high", source="user:alice")
79
+
80
+
81
+ def test_derive_and_taint_flow(d8):
82
+ d8.assert_fact("repo:myproj", "database", "postgres", authority="high", source="user:alice")
83
+ derived = d8.derive(
84
+ "service:api",
85
+ "datastore",
86
+ "postgres",
87
+ basis=("repo:myproj", "database"),
88
+ authority="high",
89
+ source="user:alice",
90
+ )
91
+ assert derived["status"] == "accepted"
92
+
93
+ d8.retract("repo:myproj", "database", authority="high", source="user:alice")
94
+ report = d8.verify()
95
+ # The basis retraction taints the derivative — a finding, not an exception.
96
+ assert report["status"] == "integrity_issues"
97
+ assert report["ok"] is False
98
+
99
+
100
+ def test_reads_and_time_travel(d8):
101
+ d8.assert_fact(
102
+ "repo:myproj",
103
+ "database",
104
+ "postgres",
105
+ authority="high",
106
+ source="user:alice",
107
+ valid_to="2036-01-01",
108
+ )
109
+ listing = d8.facts()
110
+ assert listing["count"] == 1
111
+
112
+ history = d8.replay("repo:myproj", "database")
113
+ assert history["events"][0]["kind"] == "fact.asserted"
114
+
115
+ # Human time grammar flows through: a week ago the fact did not exist.
116
+ with pytest.raises(Dent8Rejected):
117
+ d8.explain("repo:myproj", "database", as_of="-7d")
118
+
119
+ clean = d8.conflicts()
120
+ assert clean["status"] == "ok"
121
+ d8.contradict("repo:myproj", "database", "mysql", authority="high", source="user:bob")
122
+ contested = d8.conflicts()
123
+ assert contested["status"] == "contested"
124
+ assert contested["count"] == 1
125
+
126
+
127
+ def test_missing_binary_reports_clearly(tmp_path):
128
+ ghost = Dent8(binary=str(tmp_path / "definitely-not-dent8"))
129
+ with pytest.raises((FileNotFoundError, OSError)):
130
+ ghost.facts()
131
+
132
+
133
+ def test_binary_contract_matches_sdk_version():
134
+ version = subprocess.run(
135
+ [BINARY, "--version"], capture_output=True, text=True, check=True
136
+ ).stdout.strip()
137
+ assert version.startswith("dent8 ")