diff --git a/README.md b/README.md index 7a13d16..255e502 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,19 @@ -# JEP CLI v0.6 +# JEP CLI — JEP Core 0.7 -Command-line tool for creating and verifying JEP v0.6 events through the JEP API. +Command-line client for the current JEP Core 0.7 reference API. -This CLI targets: +Default commands use: ```text -POST /events/create -POST /events/verify +POST /v0.7/events/create +POST /v0.7/events/verify GET /health ``` -It is aligned with: +Historical pre-0.7 verification is explicit through `jep verify-legacy`. +The CLI never retries a failed 0.7 event as 0.6. -- `draft-wang-jep-judgment-event-protocol-06` -- `draft-wang-jep-profiles-00` -- `draft-wang-jep-conformance-00` -- `hjs-spec/jep-api` - -## Status - -Experimental implementation seed. - -This CLI does not define new JEP-Core semantics and does not determine legal liability, factual truth, regulatory compliance, or complete-log availability. - -## Installation +## Install ```bash pip install -e ".[dev]" @@ -31,90 +21,63 @@ pip install -e ".[dev]" ## Configuration -Use flags: - -```bash -jep --base-url http://127.0.0.1:8000 health -``` - -Or environment variables: - ```bash export JEP_API_URL=http://127.0.0.1:8000 export JEP_API_KEY= ``` -## Commands - -### Health - -```bash -jep health -``` - -### Create a Judgment event +## Create a J event ```bash jep create \ --verb J \ --who did:example:agent-789 \ - --what '{"claim":"approve","subject":"demo"}' \ - --aud https://api.example.org + --what '{"claim":"approve","subject":"demo"}' ``` -### Create with extensions +For D/T/V, provide the Core 0.7 minimum structures: +- D: `what.delegatee` + `what.scope` +- T: `--ref` + `what.termination_scope` +- V: `--ref` + `what.verification_scope` + `what.result` -```bash -jep create \ - --verb J \ - --who did:example:agent-789 \ - --what '{"claim":"approve"}' \ - --ext 'https://example.org/profile={"name":"demo"}' \ - --ext-crit https://example.org/profile -``` - -### Verify an event from a file +## Verify ```bash jep verify event.json --mode archival ``` -### Pipe create response into verify +Current validation output uses: +- `status = valid | invalid | indeterminate` +- independent `checks` +- `event_identity` +- `event_hash` +- optional `acceptance` + +## Explicit legacy verification ```bash -jep create --verb J --who did:example:agent --what '{"claim":"approve"}' > response.json -jep extract-event response.json > event.json -jep verify event.json +jep verify-legacy historical-event.json --mode archival ``` -## Input Rules +Use this only when the artifact is already known to be pre-0.7. -`--what` and event arguments may be: +## Boundaries -- inline JSON; -- a plain string; -- a file path; -- `-` for stdin. +JEP Core validity does not establish substantive truth, legal effect, +authorization validity, causality, policy outcome, or complete-log status. -## Related Repositories +## Related repositories -- JEP v0.6: https://github.com/hjs-spec/jep-v06 -- JEP API v0.6: https://github.com/hjs-spec/jep-api -- JEP Python SDK v0.6: https://github.com/hjs-spec/jep-sdk-py -- JEP Go SDK v0.6: https://github.com/hjs-spec/jep-sdk-go -- HJS v0.5: https://github.com/hjs-spec/hjs-05 -- JAC v0.5: https://github.com/hjs-spec/jac-agent-02 +- JEP Core: https://github.com/hjs-spec/jep-core +- JEP API: https://github.com/hjs-spec/jep-api +- Python SDK: https://github.com/hjs-spec/sdk-py +- JavaScript SDK: https://github.com/hjs-spec/sdk-js +- Go SDK: https://github.com/hjs-spec/sdk-go -## Public Drafts +## Public draft -- JEP-Core: https://datatracker.ietf.org/doc/draft-wang-jep-judgment-event-protocol/ -- JEP-Profiles: https://datatracker.ietf.org/doc/draft-wang-jep-profiles/ -- JEP-Conformance: https://datatracker.ietf.org/doc/draft-wang-jep-conformance/ +https://datatracker.ietf.org/doc/draft-wang-jep-judgment-event-protocol/ ## License MIT - -## Runtime and verification notes - -See [HARDENING.md](HARDENING.md) for supported behavior, regression checks, and compatibility boundaries. diff --git a/VERSION b/VERSION index ee6cdce..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.7.0 diff --git a/jep_cli/client.py b/jep_cli/client.py index f2dd60d..029d1d7 100644 --- a/jep_cli/client.py +++ b/jep_cli/client.py @@ -1,4 +1,7 @@ -"""Small HTTP client for JEP CLI v0.6.""" +"""HTTP client for JEP Core 0.7 CLI. + +Current endpoints are versioned. Historical pre-0.7 verification is explicit. +""" from __future__ import annotations @@ -26,10 +29,13 @@ def __init__(self, base_url: str = "http://127.0.0.1:8000", api_key: str = "", t self.timeout = timeout def create_event(self, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._json("POST", "/events/create", payload) + return self._json("POST", "/v0.7/events/create", payload) def verify_event(self, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._json("POST", "/events/verify", payload) + return self._json("POST", "/v0.7/events/verify", payload) + + def verify_event_legacy(self, payload: Dict[str, Any]) -> Dict[str, Any]: + return self._json("POST", "/events/verify-legacy", payload) def health(self) -> Dict[str, Any]: return self._json("GET", "/health", None) @@ -38,7 +44,7 @@ def _json(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Di data = None headers = { "content-type": "application/json", - "user-agent": "JEP-CLI/0.6.0", + "user-agent": "JEP-CLI/0.7.0", } if self.api_key: headers["authorization"] = f"Bearer {self.api_key}" @@ -57,5 +63,5 @@ def _json(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Di parsed = json.loads(body) except Exception: parsed = {"message": body} - msg = parsed.get("message") or parsed.get("error") or body + msg = parsed.get("detail") or parsed.get("message") or parsed.get("error") or body raise JEPAPIError(exc.code, msg, parsed) from exc diff --git a/jep_cli/main.py b/jep_cli/main.py index ddd1347..82a6465 100644 --- a/jep_cli/main.py +++ b/jep_cli/main.py @@ -1,4 +1,4 @@ -"""JEP CLI v0.6 command line interface.""" +"""JEP Core 0.7 command line interface.""" from __future__ import annotations @@ -84,14 +84,22 @@ def cmd_verify(args: argparse.Namespace) -> int: payload = { "event": event, "mode": args.mode, - "consume_nonce": args.consume_nonce, } + if args.max_age_seconds is not None: + payload["max_age_seconds"] = args.max_age_seconds if args.expected_audience: payload["expected_audience"] = args.expected_audience - if args.mode == "acceptance" and not args.expected_audience: - raise ValueError("acceptance mode requires --expected-audience") result = build_client(args).verify_event(payload) print_json(result) + return 0 if result.get("status") == "valid" else 2 + + +def cmd_verify_legacy(args: argparse.Namespace) -> int: + event = parse_json_value(args.event) + if not isinstance(event, dict): + raise ValueError("event must be JSON object or path to JSON event file") + result = build_client(args).verify_event_legacy({"event": event, "mode": args.mode}) + print_json(result) return 0 if result.get("valid") is True else 2 @@ -112,7 +120,7 @@ def cmd_extract_event(args: argparse.Namespace) -> int: def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="jep", - description="JEP CLI v0.6 for creating and verifying JEP-Core events through the JEP API.", + description="JEP CLI for JEP Core 0.7. Legacy pre-0.7 verification is explicit.", ) parser.add_argument("--base-url", default="", help="JEP API base URL. Default: JEP_API_URL or http://127.0.0.1:8000") parser.add_argument("--api-key", default="", help="API key. Default: JEP_API_KEY") @@ -125,7 +133,7 @@ def build_parser() -> argparse.ArgumentParser: create.add_argument("--who", default="", help="Actor identifier") create.add_argument("--what", required=True, help="JSON value, string, file path, or '-' for stdin") create.add_argument("--aud", default="", help="Audience / validation context") - create.add_argument("--ref", default="", help="Reference event hash") + create.add_argument("--ref", default="", help="Typed reference JSON, digest, or file path") create.add_argument("--ttl-minutes", type=int, default=None, help="Optional TTL extension in minutes") create.add_argument("--digest-only-who", action="store_true", help="Request digest-only privacy extension") create.add_argument("--ext", action="append", default=[], help="Extension key=json_or_string. May be repeated") @@ -135,10 +143,15 @@ def build_parser() -> argparse.ArgumentParser: verify = sub.add_parser("verify", help="Verify a JEP event") verify.add_argument("event", help="Event JSON object, event JSON file path, or '-' for stdin") verify.add_argument("--mode", default="archival", choices=["archival", "acceptance"], help="Validation mode") - verify.add_argument("--expected-audience", default="", help="Required receiving audience for acceptance") - verify.add_argument("--consume-nonce", action="store_true", help="Ask verifier to consume nonce") + verify.add_argument("--expected-audience", default="", help="Expected audience when the selected profile requires it") + verify.add_argument("--max-age-seconds", type=int, default=None, help="Optional profile freshness window") verify.set_defaults(func=cmd_verify) + legacy = sub.add_parser("verify-legacy", help="Explicitly verify a historical pre-0.7 event") + legacy.add_argument("event", help="Historical event JSON object, file path, or '-' for stdin") + legacy.add_argument("--mode", default="archival", choices=["archival", "acceptance"]) + legacy.set_defaults(func=cmd_verify_legacy) + health = sub.add_parser("health", help="Check JEP API health") health.set_defaults(func=cmd_health) diff --git a/pyproject.toml b/pyproject.toml index a9157e7..a398701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "jep-cli" -version = "0.6.1" -description = "Command-line tool for creating and verifying JEP v0.6 events through the JEP API" +version = "0.7.0" +description = "Command-line tool for creating and verifying JEP Core 0.7 events through the JEP API" readme = "README.md" license = { text = "MIT" } authors = [ diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index ecdfa95..c09f0a9 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -1,16 +1,24 @@ -import json from jep_cli import main as module -def test_acceptance_requires_expected_audience(capsys): - assert module.main(["verify", '{"jep":"1"}', "--mode", "acceptance"]) == 1 - assert "expected-audience" in capsys.readouterr().err - - -def test_verification_failure_returns_nonzero(monkeypatch): - class Client: +def test_acceptance_does_not_require_core_audience(monkeypatch, capsys): + class FakeClient: def verify_event(self, payload): - assert payload["expected_audience"] == "receiver" - return {"valid": False} - monkeypatch.setattr(module, "build_client", lambda args: Client()) - assert module.main(["verify", '{}', "--mode", "acceptance", "--expected-audience", "receiver"]) == 2 + assert payload["mode"] == "acceptance" + assert "expected_audience" not in payload + return { + "status": "valid", + "mode": "acceptance", + "profile": "jep-core-0.7", + "checks": {"syntax": "pass", "cryptographic": "pass", "event_identity": "pass"}, + "acceptance": {"outcome": "accepted", "effect_applied": True}, + } + + monkeypatch.setattr(module, "build_client", lambda args: FakeClient()) + code = module.main([ + "verify", + '{"jep":"1","id":"urn:uuid:x","verb":"J","who":"a","when":1,"what":{"claim":"x"},"sig":"s"}', + "--mode", "acceptance", + ]) + assert code == 0 + assert '"status": "valid"' in capsys.readouterr().out diff --git a/tests/test_cli.py b/tests/test_cli.py index dec2d98..84634fa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -"""Tests for JEP CLI v0.6.""" +"""Tests for JEP CLI against JEP Core 0.7.""" import json import threading @@ -20,52 +20,56 @@ def _json(self, status, payload): def do_GET(self): if self.path == "/health": - self._json(200, {"ok": True, "profile": "jep-core-0.6"}) + self._json(200, {"ok": True, "profile": "jep-core-0.7"}) else: self._json(404, {"message": "not found"}) def do_POST(self): length = int(self.headers.get("content-length", "0")) payload = json.loads(self.rfile.read(length) or b"{}") - if self.path == "/events/create": + if self.path == "/v0.7/events/create": event = { "jep": "1", + "id": payload.get("id", "urn:uuid:00000000-0000-7000-8000-000000000001"), "verb": payload["verb"], "who": payload.get("who", "did:example:agent"), "when": 1234567890, - "what": payload.get("what"), - "nonce": "nonce-1", - "aud": payload.get("aud"), - "ref": payload.get("ref"), - "ext": payload.get("ext"), - "ext_crit": payload.get("ext_crit"), + "what": payload["what"], "sig": "header..sig", } + for key in ("aud", "ref", "ext", "ext_crit"): + if payload.get(key) is not None: + event[key] = payload[key] self._json(200, { "event": event, "event_hash": "sha256:abc", "validation": { - "valid": True, - "level": 1, + "status": "valid", "mode": "archival", - "profile": "jep-core-0.6", + "profile": "jep-core-0.7", + "event_identity": {"who": event["who"], "id": event["id"]}, + "checks": {"syntax": "pass", "cryptographic": "pass", "event_identity": "pass"}, "event_hash": "sha256:abc", "warnings": [], "errors": [], } }) return - if self.path == "/events/verify": + if self.path == "/v0.7/events/verify": self._json(200, { - "valid": True, - "level": 1, + "status": "valid", "mode": payload.get("mode", "archival"), - "profile": "jep-core-0.6", + "profile": "jep-core-0.7", + "event_identity": {"who": payload["event"]["who"], "id": payload["event"]["id"]}, + "checks": {"syntax": "pass", "cryptographic": "pass", "event_identity": "pass"}, "event_hash": "sha256:def", "warnings": [], "errors": [], }) return + if self.path == "/events/verify-legacy": + self._json(200, {"valid": True, "level": 1, "profile": "jep-core-0.6"}) + return self._json(404, {"message": "not found"}) def log_message(self, *args): @@ -100,36 +104,32 @@ def test_parse_ext_items(): def test_health(api_server, capsys): code = main(["--base-url", api_server, "health"]) - out = capsys.readouterr().out + out = json.loads(capsys.readouterr().out) assert code == 0 - assert '"ok": true' in out + assert out["profile"] == "jep-core-0.7" -def test_create(api_server, capsys): +def test_create_v07(api_server, capsys): code = main([ - "--base-url", api_server, - "create", + "--base-url", api_server, "create", "--verb", "J", "--who", "did:example:agent", "--what", '{"claim":"approve"}', - "--ext", 'https://example.org/profile={"name":"demo"}', - "--ext-crit", "https://example.org/profile", ]) out = json.loads(capsys.readouterr().out) assert code == 0 - assert out["event_hash"] == "sha256:abc" - assert out["event"]["verb"] == "J" - assert out["event"]["ext"]["https://example.org/profile"]["name"] == "demo" + assert out["event"]["id"] + assert out["validation"]["status"] == "valid" -def test_verify(api_server, capsys, tmp_path): +def test_verify_v07(api_server, capsys, tmp_path): event = { "jep": "1", + "id": "urn:uuid:00000000-0000-7000-8000-000000000001", "verb": "J", "who": "did:example:agent", "when": 123, - "what": "sha256:abc", - "nonce": "nonce-1", + "what": {"claim": "approve"}, "sig": "header..sig", } p = tmp_path / "event.json" @@ -137,12 +137,22 @@ def test_verify(api_server, capsys, tmp_path): code = main(["--base-url", api_server, "verify", str(p)]) out = json.loads(capsys.readouterr().out) assert code == 0 - assert out["valid"] is True + assert out["status"] == "valid" + assert out["checks"]["event_identity"] == "pass" + + +def test_explicit_legacy_verify(api_server, capsys, tmp_path): + p = tmp_path / "legacy.json" + p.write_text(json.dumps({"jep": "1", "nonce": "legacy"})) + code = main(["--base-url", api_server, "verify-legacy", str(p)]) + out = json.loads(capsys.readouterr().out) + assert code == 0 + assert out["profile"] == "jep-core-0.6" def test_extract_event(capsys, tmp_path): p = tmp_path / "response.json" - p.write_text(json.dumps({"event": {"jep": "1", "verb": "J"}})) + p.write_text(json.dumps({"event": {"jep": "1", "id": "urn:uuid:x", "verb": "J"}})) code = main(["extract-event", str(p)]) out = json.loads(capsys.readouterr().out) assert code == 0