jep-cli 0.6.1__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.
- jep_cli-0.6.1/LICENSE +21 -0
- jep_cli-0.6.1/NOTICE.md +7 -0
- jep_cli-0.6.1/PKG-INFO +141 -0
- jep_cli-0.6.1/README.md +120 -0
- jep_cli-0.6.1/jep_cli/__init__.py +3 -0
- jep_cli-0.6.1/jep_cli/client.py +61 -0
- jep_cli-0.6.1/jep_cli/main.py +168 -0
- jep_cli-0.6.1/jep_cli.egg-info/PKG-INFO +141 -0
- jep_cli-0.6.1/jep_cli.egg-info/SOURCES.txt +15 -0
- jep_cli-0.6.1/jep_cli.egg-info/dependency_links.txt +1 -0
- jep_cli-0.6.1/jep_cli.egg-info/entry_points.txt +2 -0
- jep_cli-0.6.1/jep_cli.egg-info/requires.txt +3 -0
- jep_cli-0.6.1/jep_cli.egg-info/top_level.txt +1 -0
- jep_cli-0.6.1/pyproject.toml +37 -0
- jep_cli-0.6.1/setup.cfg +4 -0
- jep_cli-0.6.1/tests/test_acceptance.py +16 -0
- jep_cli-0.6.1/tests/test_cli.py +154 -0
jep_cli-0.6.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Human Judgment Systems Foundation Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jep_cli-0.6.1/NOTICE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Notice
|
|
2
|
+
|
|
3
|
+
This repository contains an experimental CLI seed for the JEP v0.6 API.
|
|
4
|
+
|
|
5
|
+
Internet-Draft text and excerpts are governed by the IETF Trust Legal Provisions and BCP 78 / BCP 79.
|
|
6
|
+
|
|
7
|
+
Implementation code, examples, and tests are provided under the repository license unless otherwise stated.
|
jep_cli-0.6.1/PKG-INFO
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jep-cli
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Command-line tool for creating and verifying JEP v0.6 events through the JEP API
|
|
5
|
+
Author-email: HJS Spec <signal@humanjudgment.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hjs-spec/cli
|
|
8
|
+
Project-URL: Repository, https://github.com/hjs-spec/cli
|
|
9
|
+
Project-URL: Issues, https://github.com/hjs-spec/cli/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: NOTICE.md
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# JEP CLI v0.6
|
|
23
|
+
|
|
24
|
+
Command-line tool for creating and verifying JEP v0.6 events through the JEP API.
|
|
25
|
+
|
|
26
|
+
This CLI targets:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
POST /events/create
|
|
30
|
+
POST /events/verify
|
|
31
|
+
GET /health
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It is aligned with:
|
|
35
|
+
|
|
36
|
+
- `draft-wang-jep-judgment-event-protocol-06`
|
|
37
|
+
- `draft-wang-jep-profiles-00`
|
|
38
|
+
- `draft-wang-jep-conformance-00`
|
|
39
|
+
- `hjs-spec/jep-api`
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
Experimental implementation seed.
|
|
44
|
+
|
|
45
|
+
This CLI does not define new JEP-Core semantics and does not determine legal liability, factual truth, regulatory compliance, or complete-log availability.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Use flags:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
jep --base-url http://127.0.0.1:8000 health
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or environment variables:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export JEP_API_URL=http://127.0.0.1:8000
|
|
65
|
+
export JEP_API_KEY=
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
### Health
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
jep health
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Create a Judgment event
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jep create \
|
|
80
|
+
--verb J \
|
|
81
|
+
--who did:example:agent-789 \
|
|
82
|
+
--what '{"claim":"approve","subject":"demo"}' \
|
|
83
|
+
--aud https://api.example.org
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Create with extensions
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
jep create \
|
|
90
|
+
--verb J \
|
|
91
|
+
--who did:example:agent-789 \
|
|
92
|
+
--what '{"claim":"approve"}' \
|
|
93
|
+
--ext 'https://example.org/profile={"name":"demo"}' \
|
|
94
|
+
--ext-crit https://example.org/profile
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Verify an event from a file
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
jep verify event.json --mode archival
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pipe create response into verify
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
jep create --verb J --who did:example:agent --what '{"claim":"approve"}' > response.json
|
|
107
|
+
jep extract-event response.json > event.json
|
|
108
|
+
jep verify event.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Input Rules
|
|
112
|
+
|
|
113
|
+
`--what` and event arguments may be:
|
|
114
|
+
|
|
115
|
+
- inline JSON;
|
|
116
|
+
- a plain string;
|
|
117
|
+
- a file path;
|
|
118
|
+
- `-` for stdin.
|
|
119
|
+
|
|
120
|
+
## Related Repositories
|
|
121
|
+
|
|
122
|
+
- JEP v0.6: https://github.com/hjs-spec/jep-v06
|
|
123
|
+
- JEP API v0.6: https://github.com/hjs-spec/jep-api
|
|
124
|
+
- JEP Python SDK v0.6: https://github.com/hjs-spec/jep-sdk-py
|
|
125
|
+
- JEP Go SDK v0.6: https://github.com/hjs-spec/jep-sdk-go
|
|
126
|
+
- HJS v0.5: https://github.com/hjs-spec/hjs-05
|
|
127
|
+
- JAC v0.5: https://github.com/hjs-spec/jac-agent-02
|
|
128
|
+
|
|
129
|
+
## Public Drafts
|
|
130
|
+
|
|
131
|
+
- JEP-Core: https://datatracker.ietf.org/doc/draft-wang-jep-judgment-event-protocol/
|
|
132
|
+
- JEP-Profiles: https://datatracker.ietf.org/doc/draft-wang-jep-profiles/
|
|
133
|
+
- JEP-Conformance: https://datatracker.ietf.org/doc/draft-wang-jep-conformance/
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
138
|
+
|
|
139
|
+
## Runtime and verification notes
|
|
140
|
+
|
|
141
|
+
See [HARDENING.md](HARDENING.md) for supported behavior, regression checks, and compatibility boundaries.
|
jep_cli-0.6.1/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# JEP CLI v0.6
|
|
2
|
+
|
|
3
|
+
Command-line tool for creating and verifying JEP v0.6 events through the JEP API.
|
|
4
|
+
|
|
5
|
+
This CLI targets:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
POST /events/create
|
|
9
|
+
POST /events/verify
|
|
10
|
+
GET /health
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is aligned with:
|
|
14
|
+
|
|
15
|
+
- `draft-wang-jep-judgment-event-protocol-06`
|
|
16
|
+
- `draft-wang-jep-profiles-00`
|
|
17
|
+
- `draft-wang-jep-conformance-00`
|
|
18
|
+
- `hjs-spec/jep-api`
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
|
|
22
|
+
Experimental implementation seed.
|
|
23
|
+
|
|
24
|
+
This CLI does not define new JEP-Core semantics and does not determine legal liability, factual truth, regulatory compliance, or complete-log availability.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Use flags:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
jep --base-url http://127.0.0.1:8000 health
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or environment variables:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export JEP_API_URL=http://127.0.0.1:8000
|
|
44
|
+
export JEP_API_KEY=
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### Health
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
jep health
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Create a Judgment event
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
jep create \
|
|
59
|
+
--verb J \
|
|
60
|
+
--who did:example:agent-789 \
|
|
61
|
+
--what '{"claim":"approve","subject":"demo"}' \
|
|
62
|
+
--aud https://api.example.org
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Create with extensions
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
jep create \
|
|
69
|
+
--verb J \
|
|
70
|
+
--who did:example:agent-789 \
|
|
71
|
+
--what '{"claim":"approve"}' \
|
|
72
|
+
--ext 'https://example.org/profile={"name":"demo"}' \
|
|
73
|
+
--ext-crit https://example.org/profile
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Verify an event from a file
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jep verify event.json --mode archival
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Pipe create response into verify
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
jep create --verb J --who did:example:agent --what '{"claim":"approve"}' > response.json
|
|
86
|
+
jep extract-event response.json > event.json
|
|
87
|
+
jep verify event.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Input Rules
|
|
91
|
+
|
|
92
|
+
`--what` and event arguments may be:
|
|
93
|
+
|
|
94
|
+
- inline JSON;
|
|
95
|
+
- a plain string;
|
|
96
|
+
- a file path;
|
|
97
|
+
- `-` for stdin.
|
|
98
|
+
|
|
99
|
+
## Related Repositories
|
|
100
|
+
|
|
101
|
+
- JEP v0.6: https://github.com/hjs-spec/jep-v06
|
|
102
|
+
- JEP API v0.6: https://github.com/hjs-spec/jep-api
|
|
103
|
+
- JEP Python SDK v0.6: https://github.com/hjs-spec/jep-sdk-py
|
|
104
|
+
- JEP Go SDK v0.6: https://github.com/hjs-spec/jep-sdk-go
|
|
105
|
+
- HJS v0.5: https://github.com/hjs-spec/hjs-05
|
|
106
|
+
- JAC v0.5: https://github.com/hjs-spec/jac-agent-02
|
|
107
|
+
|
|
108
|
+
## Public Drafts
|
|
109
|
+
|
|
110
|
+
- JEP-Core: https://datatracker.ietf.org/doc/draft-wang-jep-judgment-event-protocol/
|
|
111
|
+
- JEP-Profiles: https://datatracker.ietf.org/doc/draft-wang-jep-profiles/
|
|
112
|
+
- JEP-Conformance: https://datatracker.ietf.org/doc/draft-wang-jep-conformance/
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
117
|
+
|
|
118
|
+
## Runtime and verification notes
|
|
119
|
+
|
|
120
|
+
See [HARDENING.md](HARDENING.md) for supported behavior, regression checks, and compatibility boundaries.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Small HTTP client for JEP CLI v0.6."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from urllib import request, error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JEPCLIError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JEPAPIError(JEPCLIError):
|
|
15
|
+
def __init__(self, status: int, message: str, payload: Optional[Dict[str, Any]] = None):
|
|
16
|
+
super().__init__(f"JEP API error ({status}): {message}")
|
|
17
|
+
self.status = status
|
|
18
|
+
self.message = message
|
|
19
|
+
self.payload = payload or {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JEPClient:
|
|
23
|
+
def __init__(self, base_url: str = "http://127.0.0.1:8000", api_key: str = "", timeout: float = 30.0):
|
|
24
|
+
self.base_url = base_url.rstrip("/")
|
|
25
|
+
self.api_key = api_key
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
|
|
28
|
+
def create_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
29
|
+
return self._json("POST", "/events/create", payload)
|
|
30
|
+
|
|
31
|
+
def verify_event(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
32
|
+
return self._json("POST", "/events/verify", payload)
|
|
33
|
+
|
|
34
|
+
def health(self) -> Dict[str, Any]:
|
|
35
|
+
return self._json("GET", "/health", None)
|
|
36
|
+
|
|
37
|
+
def _json(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
38
|
+
data = None
|
|
39
|
+
headers = {
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
"user-agent": "JEP-CLI/0.6.0",
|
|
42
|
+
}
|
|
43
|
+
if self.api_key:
|
|
44
|
+
headers["authorization"] = f"Bearer {self.api_key}"
|
|
45
|
+
headers["x-api-key"] = self.api_key
|
|
46
|
+
if payload is not None:
|
|
47
|
+
data = json.dumps(payload).encode("utf-8")
|
|
48
|
+
|
|
49
|
+
req = request.Request(self.base_url + path, data=data, headers=headers, method=method)
|
|
50
|
+
try:
|
|
51
|
+
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
52
|
+
body = resp.read().decode("utf-8")
|
|
53
|
+
return json.loads(body) if body else {}
|
|
54
|
+
except error.HTTPError as exc:
|
|
55
|
+
body = exc.read().decode("utf-8")
|
|
56
|
+
try:
|
|
57
|
+
parsed = json.loads(body)
|
|
58
|
+
except Exception:
|
|
59
|
+
parsed = {"message": body}
|
|
60
|
+
msg = parsed.get("message") or parsed.get("error") or body
|
|
61
|
+
raise JEPAPIError(exc.code, msg, parsed) from exc
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""JEP CLI v0.6 command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from .client import JEPClient, JEPCLIError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VALID_VERBS = {"J", "D", "T", "V"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_json_value(value: str) -> Any:
|
|
19
|
+
if value == "-":
|
|
20
|
+
return json.load(sys.stdin)
|
|
21
|
+
|
|
22
|
+
path = Path(value)
|
|
23
|
+
if path.exists() and path.is_file():
|
|
24
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(value)
|
|
28
|
+
except json.JSONDecodeError:
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_ext_items(items: list[str]) -> Dict[str, Any]:
|
|
33
|
+
ext: Dict[str, Any] = {}
|
|
34
|
+
for item in items:
|
|
35
|
+
if "=" not in item:
|
|
36
|
+
raise ValueError(f"extension must be key=json_or_string, got: {item}")
|
|
37
|
+
key, raw = item.split("=", 1)
|
|
38
|
+
ext[key] = parse_json_value(raw)
|
|
39
|
+
return ext
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print_json(data: Any) -> None:
|
|
43
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_client(args: argparse.Namespace) -> JEPClient:
|
|
47
|
+
base_url = args.base_url or os.environ.get("JEP_API_URL", "http://127.0.0.1:8000")
|
|
48
|
+
api_key = args.api_key or os.environ.get("JEP_API_KEY", "")
|
|
49
|
+
return JEPClient(base_url=base_url, api_key=api_key, timeout=args.timeout)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_create(args: argparse.Namespace) -> int:
|
|
53
|
+
if args.verb not in VALID_VERBS:
|
|
54
|
+
raise ValueError("verb must be J, D, T, or V")
|
|
55
|
+
|
|
56
|
+
payload: Dict[str, Any] = {
|
|
57
|
+
"verb": args.verb,
|
|
58
|
+
"what": parse_json_value(args.what),
|
|
59
|
+
}
|
|
60
|
+
if args.who:
|
|
61
|
+
payload["who"] = args.who
|
|
62
|
+
if args.aud:
|
|
63
|
+
payload["aud"] = args.aud
|
|
64
|
+
if args.ref:
|
|
65
|
+
payload["ref"] = parse_json_value(args.ref)
|
|
66
|
+
if args.ttl_minutes is not None:
|
|
67
|
+
payload["ttl_minutes"] = args.ttl_minutes
|
|
68
|
+
if args.digest_only_who:
|
|
69
|
+
payload["digest_only_who"] = True
|
|
70
|
+
if args.ext:
|
|
71
|
+
payload["ext"] = parse_ext_items(args.ext)
|
|
72
|
+
if args.ext_crit:
|
|
73
|
+
payload["ext_crit"] = args.ext_crit
|
|
74
|
+
|
|
75
|
+
result = build_client(args).create_event(payload)
|
|
76
|
+
print_json(result)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_verify(args: argparse.Namespace) -> int:
|
|
81
|
+
event = parse_json_value(args.event)
|
|
82
|
+
if not isinstance(event, dict):
|
|
83
|
+
raise ValueError("event must be JSON object or path to JSON event file")
|
|
84
|
+
payload = {
|
|
85
|
+
"event": event,
|
|
86
|
+
"mode": args.mode,
|
|
87
|
+
"consume_nonce": args.consume_nonce,
|
|
88
|
+
}
|
|
89
|
+
if args.expected_audience:
|
|
90
|
+
payload["expected_audience"] = args.expected_audience
|
|
91
|
+
if args.mode == "acceptance" and not args.expected_audience:
|
|
92
|
+
raise ValueError("acceptance mode requires --expected-audience")
|
|
93
|
+
result = build_client(args).verify_event(payload)
|
|
94
|
+
print_json(result)
|
|
95
|
+
return 0 if result.get("valid") is True else 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_health(args: argparse.Namespace) -> int:
|
|
99
|
+
result = build_client(args).health()
|
|
100
|
+
print_json(result)
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_extract_event(args: argparse.Namespace) -> int:
|
|
105
|
+
data = parse_json_value(args.response)
|
|
106
|
+
if not isinstance(data, dict) or "event" not in data:
|
|
107
|
+
raise ValueError("response must contain an event field")
|
|
108
|
+
print_json(data["event"])
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
113
|
+
parser = argparse.ArgumentParser(
|
|
114
|
+
prog="jep",
|
|
115
|
+
description="JEP CLI v0.6 for creating and verifying JEP-Core events through the JEP API.",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument("--base-url", default="", help="JEP API base URL. Default: JEP_API_URL or http://127.0.0.1:8000")
|
|
118
|
+
parser.add_argument("--api-key", default="", help="API key. Default: JEP_API_KEY")
|
|
119
|
+
parser.add_argument("--timeout", type=float, default=30.0, help="HTTP timeout in seconds")
|
|
120
|
+
|
|
121
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
122
|
+
|
|
123
|
+
create = sub.add_parser("create", help="Create a JEP event")
|
|
124
|
+
create.add_argument("--verb", required=True, choices=sorted(VALID_VERBS), help="JEP verb: J, D, T, or V")
|
|
125
|
+
create.add_argument("--who", default="", help="Actor identifier")
|
|
126
|
+
create.add_argument("--what", required=True, help="JSON value, string, file path, or '-' for stdin")
|
|
127
|
+
create.add_argument("--aud", default="", help="Audience / validation context")
|
|
128
|
+
create.add_argument("--ref", default="", help="Reference event hash")
|
|
129
|
+
create.add_argument("--ttl-minutes", type=int, default=None, help="Optional TTL extension in minutes")
|
|
130
|
+
create.add_argument("--digest-only-who", action="store_true", help="Request digest-only privacy extension")
|
|
131
|
+
create.add_argument("--ext", action="append", default=[], help="Extension key=json_or_string. May be repeated")
|
|
132
|
+
create.add_argument("--ext-crit", action="append", default=[], help="Critical extension URI. May be repeated")
|
|
133
|
+
create.set_defaults(func=cmd_create)
|
|
134
|
+
|
|
135
|
+
verify = sub.add_parser("verify", help="Verify a JEP event")
|
|
136
|
+
verify.add_argument("event", help="Event JSON object, event JSON file path, or '-' for stdin")
|
|
137
|
+
verify.add_argument("--mode", default="archival", choices=["archival", "acceptance"], help="Validation mode")
|
|
138
|
+
verify.add_argument("--expected-audience", default="", help="Required receiving audience for acceptance")
|
|
139
|
+
verify.add_argument("--consume-nonce", action="store_true", help="Ask verifier to consume nonce")
|
|
140
|
+
verify.set_defaults(func=cmd_verify)
|
|
141
|
+
|
|
142
|
+
health = sub.add_parser("health", help="Check JEP API health")
|
|
143
|
+
health.set_defaults(func=cmd_health)
|
|
144
|
+
|
|
145
|
+
extract = sub.add_parser("extract-event", help="Extract event object from /events/create response")
|
|
146
|
+
extract.add_argument("response", help="Response JSON object, response JSON file path, or '-' for stdin")
|
|
147
|
+
extract.set_defaults(func=cmd_extract_event)
|
|
148
|
+
|
|
149
|
+
return parser
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv: list[str] | None = None) -> int:
|
|
153
|
+
parser = build_parser()
|
|
154
|
+
try:
|
|
155
|
+
args = parser.parse_args(argv)
|
|
156
|
+
return int(args.func(args) or 0)
|
|
157
|
+
except SystemExit as exc:
|
|
158
|
+
# Allows tests and embedded callers to receive an exit code instead of
|
|
159
|
+
# terminating the Python process. The console entry point still exits
|
|
160
|
+
# via the __main__ block below.
|
|
161
|
+
return int(exc.code or 0)
|
|
162
|
+
except (JEPCLIError, ValueError, OSError, json.JSONDecodeError) as exc:
|
|
163
|
+
print(f"jep: error: {exc}", file=sys.stderr)
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jep-cli
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Command-line tool for creating and verifying JEP v0.6 events through the JEP API
|
|
5
|
+
Author-email: HJS Spec <signal@humanjudgment.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hjs-spec/cli
|
|
8
|
+
Project-URL: Repository, https://github.com/hjs-spec/cli
|
|
9
|
+
Project-URL: Issues, https://github.com/hjs-spec/cli/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: NOTICE.md
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# JEP CLI v0.6
|
|
23
|
+
|
|
24
|
+
Command-line tool for creating and verifying JEP v0.6 events through the JEP API.
|
|
25
|
+
|
|
26
|
+
This CLI targets:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
POST /events/create
|
|
30
|
+
POST /events/verify
|
|
31
|
+
GET /health
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It is aligned with:
|
|
35
|
+
|
|
36
|
+
- `draft-wang-jep-judgment-event-protocol-06`
|
|
37
|
+
- `draft-wang-jep-profiles-00`
|
|
38
|
+
- `draft-wang-jep-conformance-00`
|
|
39
|
+
- `hjs-spec/jep-api`
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
Experimental implementation seed.
|
|
44
|
+
|
|
45
|
+
This CLI does not define new JEP-Core semantics and does not determine legal liability, factual truth, regulatory compliance, or complete-log availability.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Use flags:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
jep --base-url http://127.0.0.1:8000 health
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or environment variables:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export JEP_API_URL=http://127.0.0.1:8000
|
|
65
|
+
export JEP_API_KEY=
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
### Health
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
jep health
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Create a Judgment event
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jep create \
|
|
80
|
+
--verb J \
|
|
81
|
+
--who did:example:agent-789 \
|
|
82
|
+
--what '{"claim":"approve","subject":"demo"}' \
|
|
83
|
+
--aud https://api.example.org
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Create with extensions
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
jep create \
|
|
90
|
+
--verb J \
|
|
91
|
+
--who did:example:agent-789 \
|
|
92
|
+
--what '{"claim":"approve"}' \
|
|
93
|
+
--ext 'https://example.org/profile={"name":"demo"}' \
|
|
94
|
+
--ext-crit https://example.org/profile
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Verify an event from a file
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
jep verify event.json --mode archival
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pipe create response into verify
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
jep create --verb J --who did:example:agent --what '{"claim":"approve"}' > response.json
|
|
107
|
+
jep extract-event response.json > event.json
|
|
108
|
+
jep verify event.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Input Rules
|
|
112
|
+
|
|
113
|
+
`--what` and event arguments may be:
|
|
114
|
+
|
|
115
|
+
- inline JSON;
|
|
116
|
+
- a plain string;
|
|
117
|
+
- a file path;
|
|
118
|
+
- `-` for stdin.
|
|
119
|
+
|
|
120
|
+
## Related Repositories
|
|
121
|
+
|
|
122
|
+
- JEP v0.6: https://github.com/hjs-spec/jep-v06
|
|
123
|
+
- JEP API v0.6: https://github.com/hjs-spec/jep-api
|
|
124
|
+
- JEP Python SDK v0.6: https://github.com/hjs-spec/jep-sdk-py
|
|
125
|
+
- JEP Go SDK v0.6: https://github.com/hjs-spec/jep-sdk-go
|
|
126
|
+
- HJS v0.5: https://github.com/hjs-spec/hjs-05
|
|
127
|
+
- JAC v0.5: https://github.com/hjs-spec/jac-agent-02
|
|
128
|
+
|
|
129
|
+
## Public Drafts
|
|
130
|
+
|
|
131
|
+
- JEP-Core: https://datatracker.ietf.org/doc/draft-wang-jep-judgment-event-protocol/
|
|
132
|
+
- JEP-Profiles: https://datatracker.ietf.org/doc/draft-wang-jep-profiles/
|
|
133
|
+
- JEP-Conformance: https://datatracker.ietf.org/doc/draft-wang-jep-conformance/
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
138
|
+
|
|
139
|
+
## Runtime and verification notes
|
|
140
|
+
|
|
141
|
+
See [HARDENING.md](HARDENING.md) for supported behavior, regression checks, and compatibility boundaries.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
NOTICE.md
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
jep_cli/__init__.py
|
|
6
|
+
jep_cli/client.py
|
|
7
|
+
jep_cli/main.py
|
|
8
|
+
jep_cli.egg-info/PKG-INFO
|
|
9
|
+
jep_cli.egg-info/SOURCES.txt
|
|
10
|
+
jep_cli.egg-info/dependency_links.txt
|
|
11
|
+
jep_cli.egg-info/entry_points.txt
|
|
12
|
+
jep_cli.egg-info/requires.txt
|
|
13
|
+
jep_cli.egg-info/top_level.txt
|
|
14
|
+
tests/test_acceptance.py
|
|
15
|
+
tests/test_cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jep_cli
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jep-cli"
|
|
7
|
+
version = "0.6.1"
|
|
8
|
+
description = "Command-line tool for creating and verifying JEP v0.6 events through the JEP API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "HJS Spec", email = "signal@humanjudgment.org" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
dependencies = []
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
jep = "jep_cli.main:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/hjs-spec/cli"
|
|
33
|
+
Repository = "https://github.com/hjs-spec/cli"
|
|
34
|
+
Issues = "https://github.com/hjs-spec/cli/issues"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["jep_cli*"]
|
jep_cli-0.6.1/setup.cfg
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from jep_cli import main as module
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_acceptance_requires_expected_audience(capsys):
|
|
6
|
+
assert module.main(["verify", '{"jep":"1"}', "--mode", "acceptance"]) == 1
|
|
7
|
+
assert "expected-audience" in capsys.readouterr().err
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_verification_failure_returns_nonzero(monkeypatch):
|
|
11
|
+
class Client:
|
|
12
|
+
def verify_event(self, payload):
|
|
13
|
+
assert payload["expected_audience"] == "receiver"
|
|
14
|
+
return {"valid": False}
|
|
15
|
+
monkeypatch.setattr(module, "build_client", lambda args: Client())
|
|
16
|
+
assert module.main(["verify", '{}', "--mode", "acceptance", "--expected-audience", "receiver"]) == 2
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Tests for JEP CLI v0.6."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from jep_cli.main import main, parse_json_value, parse_ext_items
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Handler(BaseHTTPRequestHandler):
|
|
13
|
+
def _json(self, status, payload):
|
|
14
|
+
body = json.dumps(payload).encode()
|
|
15
|
+
self.send_response(status)
|
|
16
|
+
self.send_header("content-type", "application/json")
|
|
17
|
+
self.send_header("content-length", str(len(body)))
|
|
18
|
+
self.end_headers()
|
|
19
|
+
self.wfile.write(body)
|
|
20
|
+
|
|
21
|
+
def do_GET(self):
|
|
22
|
+
if self.path == "/health":
|
|
23
|
+
self._json(200, {"ok": True, "profile": "jep-core-0.6"})
|
|
24
|
+
else:
|
|
25
|
+
self._json(404, {"message": "not found"})
|
|
26
|
+
|
|
27
|
+
def do_POST(self):
|
|
28
|
+
length = int(self.headers.get("content-length", "0"))
|
|
29
|
+
payload = json.loads(self.rfile.read(length) or b"{}")
|
|
30
|
+
if self.path == "/events/create":
|
|
31
|
+
event = {
|
|
32
|
+
"jep": "1",
|
|
33
|
+
"verb": payload["verb"],
|
|
34
|
+
"who": payload.get("who", "did:example:agent"),
|
|
35
|
+
"when": 1234567890,
|
|
36
|
+
"what": payload.get("what"),
|
|
37
|
+
"nonce": "nonce-1",
|
|
38
|
+
"aud": payload.get("aud"),
|
|
39
|
+
"ref": payload.get("ref"),
|
|
40
|
+
"ext": payload.get("ext"),
|
|
41
|
+
"ext_crit": payload.get("ext_crit"),
|
|
42
|
+
"sig": "header..sig",
|
|
43
|
+
}
|
|
44
|
+
self._json(200, {
|
|
45
|
+
"event": event,
|
|
46
|
+
"event_hash": "sha256:abc",
|
|
47
|
+
"validation": {
|
|
48
|
+
"valid": True,
|
|
49
|
+
"level": 1,
|
|
50
|
+
"mode": "archival",
|
|
51
|
+
"profile": "jep-core-0.6",
|
|
52
|
+
"event_hash": "sha256:abc",
|
|
53
|
+
"warnings": [],
|
|
54
|
+
"errors": [],
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
return
|
|
58
|
+
if self.path == "/events/verify":
|
|
59
|
+
self._json(200, {
|
|
60
|
+
"valid": True,
|
|
61
|
+
"level": 1,
|
|
62
|
+
"mode": payload.get("mode", "archival"),
|
|
63
|
+
"profile": "jep-core-0.6",
|
|
64
|
+
"event_hash": "sha256:def",
|
|
65
|
+
"warnings": [],
|
|
66
|
+
"errors": [],
|
|
67
|
+
})
|
|
68
|
+
return
|
|
69
|
+
self._json(404, {"message": "not found"})
|
|
70
|
+
|
|
71
|
+
def log_message(self, *args):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture()
|
|
76
|
+
def api_server():
|
|
77
|
+
server = HTTPServer(("127.0.0.1", 0), Handler)
|
|
78
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
79
|
+
thread.start()
|
|
80
|
+
try:
|
|
81
|
+
yield f"http://127.0.0.1:{server.server_port}"
|
|
82
|
+
finally:
|
|
83
|
+
server.shutdown()
|
|
84
|
+
thread.join()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_parse_json_value_string_and_json(tmp_path):
|
|
88
|
+
assert parse_json_value('{"a":1}') == {"a": 1}
|
|
89
|
+
assert parse_json_value("plain") == "plain"
|
|
90
|
+
f = tmp_path / "data.json"
|
|
91
|
+
f.write_text('{"b":2}')
|
|
92
|
+
assert parse_json_value(str(f)) == {"b": 2}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_parse_ext_items():
|
|
96
|
+
ext = parse_ext_items(['https://example.org/x={"a":1}', 'https://example.org/y=plain'])
|
|
97
|
+
assert ext["https://example.org/x"] == {"a": 1}
|
|
98
|
+
assert ext["https://example.org/y"] == "plain"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_health(api_server, capsys):
|
|
102
|
+
code = main(["--base-url", api_server, "health"])
|
|
103
|
+
out = capsys.readouterr().out
|
|
104
|
+
assert code == 0
|
|
105
|
+
assert '"ok": true' in out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_create(api_server, capsys):
|
|
109
|
+
code = main([
|
|
110
|
+
"--base-url", api_server,
|
|
111
|
+
"create",
|
|
112
|
+
"--verb", "J",
|
|
113
|
+
"--who", "did:example:agent",
|
|
114
|
+
"--what", '{"claim":"approve"}',
|
|
115
|
+
"--ext", 'https://example.org/profile={"name":"demo"}',
|
|
116
|
+
"--ext-crit", "https://example.org/profile",
|
|
117
|
+
])
|
|
118
|
+
out = json.loads(capsys.readouterr().out)
|
|
119
|
+
assert code == 0
|
|
120
|
+
assert out["event_hash"] == "sha256:abc"
|
|
121
|
+
assert out["event"]["verb"] == "J"
|
|
122
|
+
assert out["event"]["ext"]["https://example.org/profile"]["name"] == "demo"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_verify(api_server, capsys, tmp_path):
|
|
126
|
+
event = {
|
|
127
|
+
"jep": "1",
|
|
128
|
+
"verb": "J",
|
|
129
|
+
"who": "did:example:agent",
|
|
130
|
+
"when": 123,
|
|
131
|
+
"what": "sha256:abc",
|
|
132
|
+
"nonce": "nonce-1",
|
|
133
|
+
"sig": "header..sig",
|
|
134
|
+
}
|
|
135
|
+
p = tmp_path / "event.json"
|
|
136
|
+
p.write_text(json.dumps(event))
|
|
137
|
+
code = main(["--base-url", api_server, "verify", str(p)])
|
|
138
|
+
out = json.loads(capsys.readouterr().out)
|
|
139
|
+
assert code == 0
|
|
140
|
+
assert out["valid"] is True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_extract_event(capsys, tmp_path):
|
|
144
|
+
p = tmp_path / "response.json"
|
|
145
|
+
p.write_text(json.dumps({"event": {"jep": "1", "verb": "J"}}))
|
|
146
|
+
code = main(["extract-event", str(p)])
|
|
147
|
+
out = json.loads(capsys.readouterr().out)
|
|
148
|
+
assert code == 0
|
|
149
|
+
assert out["verb"] == "J"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_invalid_verb(capsys):
|
|
153
|
+
code = main(["create", "--verb", "X", "--what", "x"])
|
|
154
|
+
assert code != 0
|