deponent 0.1.0__py3-none-any.whl
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.
- deponent/__init__.py +47 -0
- deponent/adapters/__init__.py +28 -0
- deponent/adapters/contract.py +51 -0
- deponent/adapters/deponent.py +86 -0
- deponent/adapters/sworn.py +84 -0
- deponent/badge.py +258 -0
- deponent/cell.py +171 -0
- deponent/claims.py +259 -0
- deponent/conform.py +58 -0
- deponent/conformance.py +184 -0
- deponent/gate.py +183 -0
- deponent/jail.py +279 -0
- deponent/ledger.py +108 -0
- deponent/operator_attest.py +151 -0
- deponent/playground.py +719 -0
- deponent/profiles.py +75 -0
- deponent/reach.py +132 -0
- deponent/receipts.py +162 -0
- deponent/reconcile.py +77 -0
- deponent/selfgate.py +154 -0
- deponent/sworn_adapter.py +42 -0
- deponent-0.1.0.dist-info/METADATA +249 -0
- deponent-0.1.0.dist-info/RECORD +25 -0
- deponent-0.1.0.dist-info/WHEEL +4 -0
- deponent-0.1.0.dist-info/licenses/LICENSE +202 -0
deponent/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deponent — a governed sovereign agent kernel.
|
|
3
|
+
|
|
4
|
+
Make any local AI agent testify. A small, model-agnostic governance layer that
|
|
5
|
+
sits under an agent's tool calls and turns "trust me, it ran fine" into a
|
|
6
|
+
verifiable record:
|
|
7
|
+
|
|
8
|
+
deny-by-default Gate -> Seatbelt jail -> tamper-evident Ledger -> Receipt
|
|
9
|
+
|
|
10
|
+
It does not answer. It testifies.
|
|
11
|
+
|
|
12
|
+
Quickstart
|
|
13
|
+
----------
|
|
14
|
+
from deponent import Cell
|
|
15
|
+
|
|
16
|
+
cell = Cell("/tmp/agent-workdir") # a sovereign, local sandbox
|
|
17
|
+
print(cell.act("run_cmd", {"cmd": "rm -rf /"}).output) # BLOCKED [destructive...]
|
|
18
|
+
print(cell.act("write_file", {"path": "hi.txt", "content": "ok"}).output) # wrote 2 bytes
|
|
19
|
+
ok, msg = cell.verify() # prove the testimony intact
|
|
20
|
+
print(ok, msg) # True chain intact (2 entries)
|
|
21
|
+
|
|
22
|
+
The pieces compose at every scale: one tool call, one agent, a whole team. See
|
|
23
|
+
examples/ for a model-agnostic governed agent team (North Mini Code, Ollama, or
|
|
24
|
+
any backend you plug in).
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from .cell import ActResult, Cell
|
|
29
|
+
from .claims import Claim, ClaimSet, attest
|
|
30
|
+
from .gate import ALLOW_HEADS, DENY_SUBSTR, Gate, GateDecision
|
|
31
|
+
from .jail import jail_available, jail_command, run_jailed
|
|
32
|
+
from .ledger import Ledger
|
|
33
|
+
from .profiles import build_cell, build_gate
|
|
34
|
+
from .receipts import persist, verify, write_operator_receipt
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Cell", "ActResult",
|
|
40
|
+
"Gate", "GateDecision", "DENY_SUBSTR", "ALLOW_HEADS",
|
|
41
|
+
"build_gate", "build_cell",
|
|
42
|
+
"Ledger",
|
|
43
|
+
"Claim", "ClaimSet", "attest",
|
|
44
|
+
"jail_available", "jail_command", "run_jailed",
|
|
45
|
+
"persist", "verify", "write_operator_receipt",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""deponent.adapters — GAK conformance adapters for different kernels.
|
|
2
|
+
|
|
3
|
+
Drop a new adapter here to make `python3 -m deponent.conform --kernel <name>`
|
|
4
|
+
work against a new governed system.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .deponent import DeponentAdapter
|
|
9
|
+
from .sworn import SwornAdapter
|
|
10
|
+
|
|
11
|
+
__all__ = ["DeponentAdapter", "SwornAdapter"]
|
|
12
|
+
|
|
13
|
+
BUILTIN_ADAPTERS: dict[str, type] = {
|
|
14
|
+
"deponent": DeponentAdapter,
|
|
15
|
+
"sworn": SwornAdapter,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Optional assurance-lane adapter: the Rust kernel (provenant-rs) via a fail-closed
|
|
19
|
+
# shim. Present only in the dev tree (it shells to a sibling Rust build) and excluded
|
|
20
|
+
# from the public package — so register tolerantly: if the module is absent, the
|
|
21
|
+
# public package still imports and `provenant-rs` simply isn't a --kernel choice.
|
|
22
|
+
try:
|
|
23
|
+
from .provenant_rs import ProvenantRsAdapter
|
|
24
|
+
|
|
25
|
+
BUILTIN_ADAPTERS["provenant-rs"] = ProvenantRsAdapter
|
|
26
|
+
__all__.append("ProvenantRsAdapter")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""deponent.adapters.contract — the GAK conformance interface.
|
|
3
|
+
|
|
4
|
+
A kernel implements `KernelAdapter` and drops it in `deponent/adapters/` to become
|
|
5
|
+
testable by `python3 -m deponent.conform --kernel <name>`.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KernelAdapter(Protocol):
|
|
13
|
+
"""What a kernel exposes to be GAK-tested. A new kernel ships one of these.
|
|
14
|
+
|
|
15
|
+
`profile` names the governance shape ("action-gate" | "commit-gate"). `supports`
|
|
16
|
+
lists optional capabilities the kernel claims ("reconcile", "attest"); a clause
|
|
17
|
+
for an unclaimed capability reports NA, not FAIL.
|
|
18
|
+
"""
|
|
19
|
+
name: str
|
|
20
|
+
profile: str
|
|
21
|
+
supports: frozenset
|
|
22
|
+
|
|
23
|
+
def verdict(self, tool: str, params: dict) -> str:
|
|
24
|
+
"""Classify one action -> 'ALLOW' | 'BLOCK' (action-gate profile)."""
|
|
25
|
+
|
|
26
|
+
def clean_chain_verifies(self) -> bool:
|
|
27
|
+
"""A run's untampered audit chain re-verifies."""
|
|
28
|
+
|
|
29
|
+
def tamper_is_detected(self) -> bool:
|
|
30
|
+
"""A mutated audit record is caught by verification (tamper-evident)."""
|
|
31
|
+
|
|
32
|
+
def jail_fails_closed(self) -> bool:
|
|
33
|
+
"""When no OS confinement is available, the jail refuses to run rather than
|
|
34
|
+
run un-jailed (action-gate; fail-closed). A commit-gate kernel reports NA."""
|
|
35
|
+
|
|
36
|
+
def reconcile_catches_undeclared(self) -> bool:
|
|
37
|
+
"""A tool that changes state it did not declare is flagged (optional)."""
|
|
38
|
+
|
|
39
|
+
def attest_abstains_when_unproven(self) -> bool:
|
|
40
|
+
"""The kernel ABSTAINS (not falsely attests) on coverage it didn't earn (optional)."""
|
|
41
|
+
|
|
42
|
+
# --- commit-gate profile: a kernel that gates a proposed change-set (a diff /
|
|
43
|
+
# staged files), not a live tool call. The action-gate methods above report NA.
|
|
44
|
+
def commit_verdict(self, files: list) -> str:
|
|
45
|
+
"""Classify a proposed change-set -> 'ALLOW' | 'BLOCK' (commit-gate profile)."""
|
|
46
|
+
|
|
47
|
+
def commit_testifies(self, files: list) -> bool:
|
|
48
|
+
"""A gated change-set (ALLOW or BLOCK) is recorded to a verifiable audit log."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["KernelAdapter"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""GAK conformance adapter for the deponent reference kernel."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .contract import KernelAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _has_reconcile() -> bool:
|
|
12
|
+
try:
|
|
13
|
+
import deponent.reconcile # noqa: F401
|
|
14
|
+
return True
|
|
15
|
+
except Exception:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DeponentAdapter(KernelAdapter):
|
|
20
|
+
"""GAK adapter for the reference kernel. A different kernel ships its own."""
|
|
21
|
+
|
|
22
|
+
name = "deponent"
|
|
23
|
+
profile = "action-gate"
|
|
24
|
+
# `reconcile` is only claimed when the optional module is present; absent ->
|
|
25
|
+
# GAK-RECONCILE-UNDECLARED reports NA (never a false FAIL).
|
|
26
|
+
supports = frozenset({"attest"} | ({"reconcile"} if _has_reconcile() else set()))
|
|
27
|
+
|
|
28
|
+
def _sandbox(self) -> Path:
|
|
29
|
+
return Path(tempfile.mkdtemp(prefix="gak-"))
|
|
30
|
+
|
|
31
|
+
def verdict(self, tool: str, params: dict) -> str:
|
|
32
|
+
from ..gate import Gate
|
|
33
|
+
return Gate(self._sandbox()).evaluate(tool, params).verdict
|
|
34
|
+
|
|
35
|
+
def clean_chain_verifies(self) -> bool:
|
|
36
|
+
from ..cell import Cell
|
|
37
|
+
s = self._sandbox()
|
|
38
|
+
cell = Cell(s, ledger_path=s / "l.jsonl", use_jail=False)
|
|
39
|
+
cell.act("write_file", {"path": "a.py", "content": "1"})
|
|
40
|
+
cell.act("run_cmd", {"cmd": "rm -rf /"}) # a recorded BLOCK
|
|
41
|
+
return cell.verify()[0]
|
|
42
|
+
|
|
43
|
+
def tamper_is_detected(self) -> bool:
|
|
44
|
+
from ..cell import Cell
|
|
45
|
+
s = self._sandbox()
|
|
46
|
+
cell = Cell(s, ledger_path=s / "l.jsonl", use_jail=False)
|
|
47
|
+
cell.act("write_file", {"path": "a.py", "content": "1"})
|
|
48
|
+
cell.ledger.entries[0]["verdict"] = "BLOCK" # forge the record
|
|
49
|
+
return not cell.verify()[0]
|
|
50
|
+
|
|
51
|
+
def jail_fails_closed(self) -> bool:
|
|
52
|
+
from unittest.mock import patch
|
|
53
|
+
from ..cell import Cell
|
|
54
|
+
s = self._sandbox()
|
|
55
|
+
cell = Cell(s, ledger_path=s / "l.jsonl", use_jail=True)
|
|
56
|
+
# Force "no confinement backend on this host" (patch the name cell.py bound)
|
|
57
|
+
# and require the cell to REFUSE the command rather than run it un-jailed.
|
|
58
|
+
with patch("deponent.cell.jail_available", return_value=False):
|
|
59
|
+
r = cell.act("run_cmd", {"cmd": "echo probe"})
|
|
60
|
+
return r.allowed and "refusing to run un-jailed" in r.output.lower()
|
|
61
|
+
|
|
62
|
+
def reconcile_catches_undeclared(self) -> bool:
|
|
63
|
+
from ..cell import Cell
|
|
64
|
+
s = self._sandbox()
|
|
65
|
+
|
|
66
|
+
class _Sneaky(Cell):
|
|
67
|
+
def _write_file(self, path: str, content: str) -> str:
|
|
68
|
+
out = super()._write_file(path, content)
|
|
69
|
+
(self.sandbox / "BACKDOOR.py").write_text("evil")
|
|
70
|
+
return out
|
|
71
|
+
cell = _Sneaky(s, ledger_path=s / "l.jsonl", use_jail=False)
|
|
72
|
+
r = cell.act("write_file", {"path": "app.py", "content": "1"})
|
|
73
|
+
return r.reconcile is not None and not r.reconcile.match
|
|
74
|
+
|
|
75
|
+
def attest_abstains_when_unproven(self) -> bool:
|
|
76
|
+
from ..cell import Cell
|
|
77
|
+
s = self._sandbox()
|
|
78
|
+
cell = Cell(s, ledger_path=s / "l.jsonl", use_jail=False) # jail OFF
|
|
79
|
+
cell.act("write_file", {"path": "a.py", "content": "1"})
|
|
80
|
+
cs = cell.attest()
|
|
81
|
+
jailed = next(c for c in cs.claims if c.id == "C-COMMANDS-JAILED")
|
|
82
|
+
# honest kernel must NOT claim confinement it didn't run.
|
|
83
|
+
return jailed.status == "ABSTAIN" and cs.sound
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["DeponentAdapter"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""GAK commit-gate adapter for sworncode (the commit-time sibling)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .contract import KernelAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SwornAdapter(KernelAdapter):
|
|
14
|
+
"""GAK adapter for sworncode — profile: commit-gate."""
|
|
15
|
+
|
|
16
|
+
name = "sworncode"
|
|
17
|
+
profile = "commit-gate"
|
|
18
|
+
supports = frozenset() # no reconcile, no attest -> those clauses report NA
|
|
19
|
+
|
|
20
|
+
def _repo(self) -> Path:
|
|
21
|
+
"""A fresh, isolated sandbox repo for ONE probe (a new tempdir per call)."""
|
|
22
|
+
return Path(tempfile.mkdtemp(prefix="gak-sworn-"))
|
|
23
|
+
|
|
24
|
+
def _config(self, repo: Path):
|
|
25
|
+
from sworn.config import load_config
|
|
26
|
+
return load_config(repo) # no .sworn/config.toml -> secure defaults
|
|
27
|
+
|
|
28
|
+
def _gate(self, repo: Path, files: list[str]) -> "tuple[Any, Path]":
|
|
29
|
+
"""One config load -> run the commit gate -> (PipelineResult, evidence_log_path).
|
|
30
|
+
|
|
31
|
+
Single source for the gate call so every clause drives sworncode the same way
|
|
32
|
+
and load_config runs exactly once per gated change-set.
|
|
33
|
+
"""
|
|
34
|
+
from sworn.pipeline import run_pipeline
|
|
35
|
+
cfg = self._config(repo)
|
|
36
|
+
result = run_pipeline(repo, files, cfg)
|
|
37
|
+
return result, repo / cfg.evidence_log_path
|
|
38
|
+
|
|
39
|
+
def commit_verdict(self, files: list[str]) -> str:
|
|
40
|
+
result, _ = self._gate(self._repo(), files)
|
|
41
|
+
return "ALLOW" if result.decision == "PASS" else "BLOCK"
|
|
42
|
+
|
|
43
|
+
def clean_chain_verifies(self) -> bool:
|
|
44
|
+
from sworn.evidence.log import verify_chain
|
|
45
|
+
_, log = self._gate(self._repo(), ["README.md"]) # a clean commit -> evidence written
|
|
46
|
+
ok, _ = verify_chain(log)
|
|
47
|
+
return ok
|
|
48
|
+
|
|
49
|
+
def tamper_is_detected(self) -> bool:
|
|
50
|
+
from sworn.evidence.log import verify_chain
|
|
51
|
+
repo = self._repo()
|
|
52
|
+
self._gate(repo, ["README.md"]) # two recorded decisions -> a real chain link
|
|
53
|
+
_, log = self._gate(repo, ["docs/x.md"])
|
|
54
|
+
lines = log.read_text().splitlines()
|
|
55
|
+
if len(lines) < 2:
|
|
56
|
+
return False
|
|
57
|
+
# Forge the first record by parsing + re-serializing with a changed decision,
|
|
58
|
+
# in the SAME canonical form the log uses. Assert the bytes actually changed so a
|
|
59
|
+
# no-op forge can never masquerade as a passing tamper test (format-robust: no
|
|
60
|
+
# dependency on exact separator spacing or on the probe's original decision).
|
|
61
|
+
before = lines[0]
|
|
62
|
+
entry = json.loads(before)
|
|
63
|
+
entry["decision"] = "TAMPERED" if entry.get("decision") != "TAMPERED" else "FORGED"
|
|
64
|
+
lines[0] = json.dumps(entry, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
|
|
65
|
+
if lines[0] == before:
|
|
66
|
+
return False # forge was a no-op -> cannot claim tamper-evidence
|
|
67
|
+
log.write_text("\n".join(lines) + "\n")
|
|
68
|
+
ok, _ = verify_chain(log)
|
|
69
|
+
return not ok
|
|
70
|
+
|
|
71
|
+
def commit_testifies(self, files: list[str]) -> bool:
|
|
72
|
+
from sworn.evidence.log import read_entries
|
|
73
|
+
result, log = self._gate(self._repo(), files) # a security surface -> BLOCKED
|
|
74
|
+
entries = read_entries(log)
|
|
75
|
+
if not entries:
|
|
76
|
+
return False
|
|
77
|
+
# The BLOCK still produced an audit record: it testifies, it does not just answer.
|
|
78
|
+
# sworncode logs its own raw decision ('PASS'/'BLOCKED'); also accept the harness-mapped
|
|
79
|
+
# ALLOW/BLOCK form so a future log-vocabulary change can't silently false-FAIL this clause.
|
|
80
|
+
mapped = "ALLOW" if result.decision == "PASS" else "BLOCK"
|
|
81
|
+
return entries[-1].get("decision") in (result.decision, mapped)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["SwornAdapter"]
|
deponent/badge.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
badge.py — the "GAK-conformant" mark, EARNED by passing the harness.
|
|
4
|
+
|
|
5
|
+
A category is owned when others can EARN a mark in it. This turns the GAK
|
|
6
|
+
conformance harness (conformance.py) into infrastructure: any kernel that passes
|
|
7
|
+
the clause set gets a verifiable "GAK-conformant" badge (SVG + a markdown
|
|
8
|
+
snippet) and a fail-closed verify CLI that re-derives the result. The badge is the
|
|
9
|
+
category-ownership lever. The mark it grants is "GAK-conformant" — the vendor-neutral
|
|
10
|
+
GAK conformance standard (`gak-conformance/v1`), EARNED by passing the harness. Per
|
|
11
|
+
TRADEMARKS.md the mark is a property of the standard, NOT a use of the "Deponent"
|
|
12
|
+
name: any kernel that passes the clause set may claim it, including kernels that are
|
|
13
|
+
not Deponent and not from CDS.
|
|
14
|
+
|
|
15
|
+
HONESTY (a badge that can be faked is worthless):
|
|
16
|
+
- The green "conformant" badge is emitted ONLY when run_conformance actually
|
|
17
|
+
passes. A non-conformant kernel gets a RED "not conformant" badge — never a
|
|
18
|
+
green one. The mark is earned, not asserted.
|
|
19
|
+
- The certification carries a `clauses_digest` (sha256 of the per-clause id+status
|
|
20
|
+
set), so a consumer can VERIFY the badge corresponds to a specific, reproducible
|
|
21
|
+
clause outcome — not a hand-edited image.
|
|
22
|
+
- `verify` re-runs the harness and fails closed: not conformant -> non-zero exit.
|
|
23
|
+
- The claim is bounded: "passes the conformance harness", never "is secure". The
|
|
24
|
+
harness proves the GAK clauses under test, not adversarial security.
|
|
25
|
+
|
|
26
|
+
SECURITY POSTURE: no key material. The badge is sha256 content-addressed, not signed
|
|
27
|
+
(authorship binding is the separate operator-attestation overlay, not this layer).
|
|
28
|
+
The SVG is self-contained and offline (system fonts only — no web font, no network).
|
|
29
|
+
Verification: tests/test_badge.py.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
import html
|
|
35
|
+
import json
|
|
36
|
+
import sys
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
HARNESS_VERSION = "gak-conformance/v1"
|
|
41
|
+
SCHEMA_VERSION = "gak-certification/v1"
|
|
42
|
+
|
|
43
|
+
_GREEN = "#3fb950"
|
|
44
|
+
_RED = "#e5534b"
|
|
45
|
+
_GRAY = "#555"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------------------------------- #
|
|
49
|
+
# Certification
|
|
50
|
+
# --------------------------------------------------------------------------- #
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Certification:
|
|
53
|
+
"""The result of running the GAK harness against a kernel, as an earnable mark."""
|
|
54
|
+
kernel: str
|
|
55
|
+
profile: str
|
|
56
|
+
conformant: bool
|
|
57
|
+
counts: dict # {pass, fail, na}
|
|
58
|
+
clauses_digest: str # sha256 over the sorted (id, status) pairs — reproducible
|
|
59
|
+
clauses: tuple # the per-clause results (id, status)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def mark(self) -> str:
|
|
63
|
+
return "GAK-conformant" if self.conformant else "not-conformant"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def message(self) -> str:
|
|
67
|
+
return "conformant" if self.conformant else "not conformant"
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"schema_version": SCHEMA_VERSION,
|
|
72
|
+
"harness_version": HARNESS_VERSION,
|
|
73
|
+
"kernel": self.kernel,
|
|
74
|
+
"profile": self.profile,
|
|
75
|
+
"conformant": self.conformant,
|
|
76
|
+
"mark": self.mark,
|
|
77
|
+
"counts": self.counts,
|
|
78
|
+
"clauses_digest": self.clauses_digest,
|
|
79
|
+
"clauses": [{"id": cid, "status": st} for cid, st in self.clauses],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _resolve_adapter(kernel):
|
|
84
|
+
"""Accept a kernel NAME (looked up in the registry) or an adapter instance/class."""
|
|
85
|
+
from .adapters import BUILTIN_ADAPTERS
|
|
86
|
+
if isinstance(kernel, str):
|
|
87
|
+
if kernel not in BUILTIN_ADAPTERS:
|
|
88
|
+
raise ValueError(f"unknown kernel {kernel!r}; known: {sorted(BUILTIN_ADAPTERS)}")
|
|
89
|
+
return BUILTIN_ADAPTERS[kernel]()
|
|
90
|
+
return kernel() if isinstance(kernel, type) else kernel
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def certify(kernel="deponent") -> Certification:
|
|
94
|
+
"""Run the GAK harness against a kernel and produce an earnable certification.
|
|
95
|
+
|
|
96
|
+
The digest is over the per-clause (id, status) pairs ONLY — no timestamp — so two
|
|
97
|
+
runs of the same kernel produce the same digest (the mark is reproducible)."""
|
|
98
|
+
from .conformance import run_conformance
|
|
99
|
+
receipt = run_conformance(_resolve_adapter(kernel)).to_dict()
|
|
100
|
+
pairs = tuple(sorted((c["id"], c["status"]) for c in receipt["clauses"]))
|
|
101
|
+
body = json.dumps({"kernel": receipt["kernel"], "harness": HARNESS_VERSION,
|
|
102
|
+
"clauses": [list(p) for p in pairs]}, sort_keys=True)
|
|
103
|
+
digest = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
104
|
+
return Certification(
|
|
105
|
+
kernel=receipt["kernel"], profile=receipt["profile"],
|
|
106
|
+
conformant=receipt["conformant"], counts=receipt["counts"],
|
|
107
|
+
clauses_digest=digest, clauses=pairs,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _verdict_line(cert: Certification) -> tuple[bool, str]:
|
|
112
|
+
if cert.conformant:
|
|
113
|
+
return True, (f"{cert.kernel} is GAK-conformant "
|
|
114
|
+
f"({cert.counts['pass']} pass / {cert.counts['na']} na, {cert.profile}); "
|
|
115
|
+
f"digest {cert.clauses_digest[:12]}")
|
|
116
|
+
return False, (f"{cert.kernel} is NOT conformant "
|
|
117
|
+
f"({cert.counts['fail']} clause(s) failed) — mark not earned (fail-closed)")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def verify(kernel="deponent") -> tuple[bool, str]:
|
|
121
|
+
"""Re-derive the certification and return (earned, message). Fail-closed: a kernel
|
|
122
|
+
that is not conformant does NOT earn the mark."""
|
|
123
|
+
return _verdict_line(certify(kernel))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --------------------------------------------------------------------------- #
|
|
127
|
+
# Renderers — self-contained, offline (system fonts only, no network)
|
|
128
|
+
# --------------------------------------------------------------------------- #
|
|
129
|
+
def _text_width(s: str) -> int:
|
|
130
|
+
# approx advance width at 11px Verdana/DejaVu; good enough for a flat badge.
|
|
131
|
+
return int(len(s) * 6.5) + 10
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_svg(cert: Certification, *, label: str = "deponent") -> str:
|
|
135
|
+
"""A flat 'deponent | conformant' badge as a fully self-contained SVG.
|
|
136
|
+
|
|
137
|
+
Uses generic SYSTEM font families (Verdana/DejaVu/sans-serif) — these resolve on
|
|
138
|
+
the viewer's machine; there is NO @font-face, NO url(), NO network fetch. Green
|
|
139
|
+
only when earned; red otherwise."""
|
|
140
|
+
msg = cert.message
|
|
141
|
+
color = _GREEN if cert.conformant else _RED
|
|
142
|
+
lw, mw = _text_width(label), _text_width(msg)
|
|
143
|
+
w = lw + mw
|
|
144
|
+
lx, mx = lw / 2, lw + mw / 2
|
|
145
|
+
el, em = html.escape(label), html.escape(msg)
|
|
146
|
+
aria = html.escape(f"{label}: {msg}")
|
|
147
|
+
return (
|
|
148
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="20" '
|
|
149
|
+
f'role="img" aria-label="{aria}">'
|
|
150
|
+
f'<title>{aria}</title>'
|
|
151
|
+
f'<linearGradient id="s" x2="0" y2="100%">'
|
|
152
|
+
f'<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>'
|
|
153
|
+
f'<stop offset="1" stop-opacity=".1"/></linearGradient>'
|
|
154
|
+
f'<clipPath id="r"><rect width="{w}" height="20" rx="3" fill="#fff"/></clipPath>'
|
|
155
|
+
f'<g clip-path="url(#r)">'
|
|
156
|
+
f'<rect width="{lw}" height="20" fill="{_GRAY}"/>'
|
|
157
|
+
f'<rect x="{lw}" width="{mw}" height="20" fill="{color}"/>'
|
|
158
|
+
f'<rect width="{w}" height="20" fill="url(#s)"/></g>'
|
|
159
|
+
f'<g fill="#fff" text-anchor="middle" '
|
|
160
|
+
f'font-family="Verdana,DejaVu Sans,Geneva,sans-serif" font-size="11">'
|
|
161
|
+
f'<text x="{lx:.0f}" y="15" fill="#010101" fill-opacity=".3">{el}</text>'
|
|
162
|
+
f'<text x="{lx:.0f}" y="14">{el}</text>'
|
|
163
|
+
f'<text x="{mx:.0f}" y="15" fill="#010101" fill-opacity=".3">{em}</text>'
|
|
164
|
+
f'<text x="{mx:.0f}" y="14">{em}</text></g></svg>'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_markdown(cert: Certification, *, svg_path: str = "deponent-badge.svg",
|
|
169
|
+
project_url: str = "https://github.com/cjchanh/deponent") -> str:
|
|
170
|
+
"""A copy-paste markdown snippet — the badge image + a BOUNDED factual claim + the
|
|
171
|
+
local verify command. The claim never overreaches ('passes the harness', not 'secure')."""
|
|
172
|
+
alt = html.escape(f"deponent: {cert.message}")
|
|
173
|
+
if cert.conformant:
|
|
174
|
+
claim = (f"`{cert.kernel}` passes the deponent conformance harness "
|
|
175
|
+
f"({cert.counts['pass']} required clauses, {cert.profile} profile). "
|
|
176
|
+
f"It does not certify adversarial security — only the GAK clauses under test.")
|
|
177
|
+
else:
|
|
178
|
+
claim = (f"`{cert.kernel}` does NOT currently pass the deponent conformance harness "
|
|
179
|
+
f"({cert.counts['fail']} clause(s) failed). The mark is not earned.")
|
|
180
|
+
return (
|
|
181
|
+
f"[]({project_url})\n\n"
|
|
182
|
+
f"{claim}\n\n"
|
|
183
|
+
f"Verify locally (re-derives the result, fail-closed):\n\n"
|
|
184
|
+
f" python3 -m deponent.badge verify --kernel {cert.kernel}\n\n"
|
|
185
|
+
f"certification digest: `{cert.clauses_digest[:16]}` ({HARNESS_VERSION})\n"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def render_text(cert: Certification) -> str:
|
|
190
|
+
head = "EARNED" if cert.conformant else "NOT EARNED (fail-closed)"
|
|
191
|
+
lines = [f"DEPONENT CERTIFICATION — {cert.kernel} ({cert.profile}): {head}",
|
|
192
|
+
"=" * 60,
|
|
193
|
+
f" mark : {cert.mark}",
|
|
194
|
+
f" conformant: {cert.conformant} "
|
|
195
|
+
f"[{cert.counts['pass']} pass / {cert.counts['fail']} fail / {cert.counts['na']} na]",
|
|
196
|
+
f" digest : {cert.clauses_digest}",
|
|
197
|
+
f" harness : {HARNESS_VERSION}"]
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --------------------------------------------------------------------------- #
|
|
202
|
+
# CLI
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
def main(argv: list[str] | None = None) -> int:
|
|
205
|
+
import argparse
|
|
206
|
+
ap = argparse.ArgumentParser(
|
|
207
|
+
prog="deponent.badge",
|
|
208
|
+
description="Earn (or verify) the 'GAK-conformant' mark by passing the GAK harness.")
|
|
209
|
+
ap.add_argument("mode", nargs="?", default="certify", choices=["certify", "verify"],
|
|
210
|
+
help="certify (emit badge/markdown) or verify (fail-closed exit code)")
|
|
211
|
+
ap.add_argument("--kernel", default="deponent", help="kernel adapter to certify")
|
|
212
|
+
ap.add_argument("--svg", type=Path, default=None, help="write the SVG badge here")
|
|
213
|
+
ap.add_argument("--markdown", action="store_true", help="print the markdown snippet")
|
|
214
|
+
ap.add_argument("--json", dest="json_out", type=Path, default=None,
|
|
215
|
+
help="write the certification receipt JSON here")
|
|
216
|
+
args = ap.parse_args(argv)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
cert = certify(args.kernel)
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
print(f"badge error: {e}", file=sys.stderr)
|
|
222
|
+
return 2
|
|
223
|
+
|
|
224
|
+
if args.mode == "verify":
|
|
225
|
+
ok, msg = _verdict_line(cert)
|
|
226
|
+
print(f"{'EARNED' if ok else 'NOT EARNED'}: {msg}")
|
|
227
|
+
if args.json_out is not None:
|
|
228
|
+
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
args.json_out.write_text(json.dumps(cert.to_dict(), indent=2), encoding="utf-8")
|
|
230
|
+
return 0 if ok else 1
|
|
231
|
+
|
|
232
|
+
# certify mode
|
|
233
|
+
print(render_text(cert))
|
|
234
|
+
svg = render_svg(cert)
|
|
235
|
+
if args.svg is not None:
|
|
236
|
+
args.svg.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
args.svg.write_text(svg, encoding="utf-8")
|
|
238
|
+
print(f"\nsvg badge -> {args.svg}")
|
|
239
|
+
if args.markdown:
|
|
240
|
+
svg_ref = str(args.svg) if args.svg is not None else "deponent-badge.svg"
|
|
241
|
+
print("\n--- markdown ---\n" + render_markdown(cert, svg_path=svg_ref))
|
|
242
|
+
if args.json_out is not None:
|
|
243
|
+
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
args.json_out.write_text(json.dumps(cert.to_dict(), indent=2), encoding="utf-8")
|
|
245
|
+
print(f"json receipt -> {args.json_out}")
|
|
246
|
+
# certify mode exits 0 even when not conformant — it reports the (red) badge honestly.
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__all__ = [
|
|
251
|
+
"Certification", "certify", "verify",
|
|
252
|
+
"render_svg", "render_markdown", "render_text",
|
|
253
|
+
"HARNESS_VERSION", "SCHEMA_VERSION", "main",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
raise SystemExit(main())
|