jep-authority-runtime 0.1.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.
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: jep-authority-runtime
3
+ Version: 0.1.1
4
+ Summary: Reference runtime for JEP-compatible authority scope semantics.
5
+ Author: JEP Authority Runtime contributors
6
+ License: MIT
7
+ Keywords: jep,authority,delegation,attenuation,replay
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # jep-authority-runtime
14
+
15
+ Reference runtime for JEP authority scope, delegation boundaries, attenuation, revocation, and replay verification.
16
+
17
+ This project is intentionally **not** real IAM and does not replace OAuth, X.509, DID, or any production authorization protocol. It is a small JEP-compatible reference runtime for making delegation semantics replayable and verifiable.
18
+
19
+ ## AuthorityScope
20
+
21
+ `AuthorityScope` models the authority passed along a delegation chain:
22
+
23
+ - `actor`: principal that issued the scope.
24
+ - `subject`: principal receiving the scope.
25
+ - `allowed_actions`: actions the subject may perform.
26
+ - `denied_actions`: actions explicitly forbidden and inherited by descendants.
27
+ - `resource_scope`: resource prefix the scope applies to (`*` means every resource).
28
+ - `expires_at`: optional UTC expiration timestamp.
29
+ - `parent_scope`: optional parent scope id for delegated or attenuated authority.
30
+ - `attenuation_rules`: reference metadata describing how authority was narrowed.
31
+
32
+ The runtime also stores `scope_id` and `revoked_at` so archives can refer to scopes deterministically and model revocation.
33
+
34
+ ## DelegationRuntime
35
+
36
+ `DelegationRuntime` provides:
37
+
38
+ - `create_scope()` for root authority grants.
39
+ - `delegate_scope()` for child scopes that must be narrower than their parent.
40
+ - `attenuate_scope()` as a convenience wrapper for narrower delegation.
41
+ - `revoke_scope()` to invalidate a scope and its descendants for later checks.
42
+ - `verify_scope()` to check action, resource, expiration, revocation, and chain validity.
43
+ - `verify_delegation_path()` to check parent continuity and attenuation constraints.
44
+
45
+ A child scope is valid only when it does not add actions, omit inherited denials, widen resources, outlive its parent, or rely on a missing/revoked/expired parent.
46
+
47
+ ## Replay archive format
48
+
49
+ Archives are JSON Lines (`archive.jsonl`). Each line is an event with an `event` field:
50
+
51
+ ```json
52
+ {"event":"create_scope","scope_id":"human-search","actor":"human:alice","subject":"agent:searcher","allowed_actions":["search"],"denied_actions":["payment"],"resource_scope":"web/search","expires_at":"2030-01-01T00:00:00Z"}
53
+ {"event":"delegate_scope","scope_id":"agent-news","parent_scope":"human-search","actor":"agent:searcher","subject":"agent:sub-searcher","allowed_actions":["search"],"resource_scope":"web/search/news"}
54
+ {"event":"action","scope_id":"agent-news","action":"search","resource":"web/search/news/politics","at":"2028-01-01T00:00:00Z"}
55
+ {"event":"revoke_scope","scope_id":"human-search","revoked_at":"2028-06-01T00:00:00Z"}
56
+ ```
57
+
58
+ Replay checks whether propagation or actions are invalid because authority is exceeded, a scope is expired, a scope or ancestor is revoked, a child exceeds the parent scope, or the delegation chain is broken.
59
+
60
+ ## CLI
61
+
62
+ Install in editable mode:
63
+
64
+ ```sh
65
+ python -m pip install -e .
66
+ ```
67
+
68
+ Replay an archive:
69
+
70
+ ```sh
71
+ jep-authority replay examples/archive.jsonl
72
+ ```
73
+
74
+ Verify an archive and emit JSON:
75
+
76
+ ```sh
77
+ jep-authority verify examples/archive.jsonl --json
78
+ ```
79
+
80
+ Both commands exit with status `0` when the report has no violations and `1` when violations are found.
81
+
82
+ ## Example scenario
83
+
84
+ `examples/archive.jsonl` demonstrates:
85
+
86
+ 1. A human delegates limited `search` permission to an agent.
87
+ 2. The agent delegates narrower `web/search/news` permission to a sub-agent.
88
+ 3. The sub-agent performs an allowed search.
89
+ 4. The sub-agent attempts a forbidden `payment` action.
90
+ 5. Revocation of the parent invalidates a later delegated action.
@@ -0,0 +1,78 @@
1
+ # jep-authority-runtime
2
+
3
+ Reference runtime for JEP authority scope, delegation boundaries, attenuation, revocation, and replay verification.
4
+
5
+ This project is intentionally **not** real IAM and does not replace OAuth, X.509, DID, or any production authorization protocol. It is a small JEP-compatible reference runtime for making delegation semantics replayable and verifiable.
6
+
7
+ ## AuthorityScope
8
+
9
+ `AuthorityScope` models the authority passed along a delegation chain:
10
+
11
+ - `actor`: principal that issued the scope.
12
+ - `subject`: principal receiving the scope.
13
+ - `allowed_actions`: actions the subject may perform.
14
+ - `denied_actions`: actions explicitly forbidden and inherited by descendants.
15
+ - `resource_scope`: resource prefix the scope applies to (`*` means every resource).
16
+ - `expires_at`: optional UTC expiration timestamp.
17
+ - `parent_scope`: optional parent scope id for delegated or attenuated authority.
18
+ - `attenuation_rules`: reference metadata describing how authority was narrowed.
19
+
20
+ The runtime also stores `scope_id` and `revoked_at` so archives can refer to scopes deterministically and model revocation.
21
+
22
+ ## DelegationRuntime
23
+
24
+ `DelegationRuntime` provides:
25
+
26
+ - `create_scope()` for root authority grants.
27
+ - `delegate_scope()` for child scopes that must be narrower than their parent.
28
+ - `attenuate_scope()` as a convenience wrapper for narrower delegation.
29
+ - `revoke_scope()` to invalidate a scope and its descendants for later checks.
30
+ - `verify_scope()` to check action, resource, expiration, revocation, and chain validity.
31
+ - `verify_delegation_path()` to check parent continuity and attenuation constraints.
32
+
33
+ A child scope is valid only when it does not add actions, omit inherited denials, widen resources, outlive its parent, or rely on a missing/revoked/expired parent.
34
+
35
+ ## Replay archive format
36
+
37
+ Archives are JSON Lines (`archive.jsonl`). Each line is an event with an `event` field:
38
+
39
+ ```json
40
+ {"event":"create_scope","scope_id":"human-search","actor":"human:alice","subject":"agent:searcher","allowed_actions":["search"],"denied_actions":["payment"],"resource_scope":"web/search","expires_at":"2030-01-01T00:00:00Z"}
41
+ {"event":"delegate_scope","scope_id":"agent-news","parent_scope":"human-search","actor":"agent:searcher","subject":"agent:sub-searcher","allowed_actions":["search"],"resource_scope":"web/search/news"}
42
+ {"event":"action","scope_id":"agent-news","action":"search","resource":"web/search/news/politics","at":"2028-01-01T00:00:00Z"}
43
+ {"event":"revoke_scope","scope_id":"human-search","revoked_at":"2028-06-01T00:00:00Z"}
44
+ ```
45
+
46
+ Replay checks whether propagation or actions are invalid because authority is exceeded, a scope is expired, a scope or ancestor is revoked, a child exceeds the parent scope, or the delegation chain is broken.
47
+
48
+ ## CLI
49
+
50
+ Install in editable mode:
51
+
52
+ ```sh
53
+ python -m pip install -e .
54
+ ```
55
+
56
+ Replay an archive:
57
+
58
+ ```sh
59
+ jep-authority replay examples/archive.jsonl
60
+ ```
61
+
62
+ Verify an archive and emit JSON:
63
+
64
+ ```sh
65
+ jep-authority verify examples/archive.jsonl --json
66
+ ```
67
+
68
+ Both commands exit with status `0` when the report has no violations and `1` when violations are found.
69
+
70
+ ## Example scenario
71
+
72
+ `examples/archive.jsonl` demonstrates:
73
+
74
+ 1. A human delegates limited `search` permission to an agent.
75
+ 2. The agent delegates narrower `web/search/news` permission to a sub-agent.
76
+ 3. The sub-agent performs an allowed search.
77
+ 4. The sub-agent attempts a forbidden `payment` action.
78
+ 5. Revocation of the parent invalidates a later delegated action.
@@ -0,0 +1,19 @@
1
+ """JEP-compatible authority semantics reference runtime."""
2
+
3
+ from .runtime import (
4
+ AuthorityScope,
5
+ DelegationRuntime,
6
+ ReplayEvent,
7
+ ReplayReport,
8
+ ReplayViolation,
9
+ replay_archive,
10
+ )
11
+
12
+ __all__ = [
13
+ "AuthorityScope",
14
+ "DelegationRuntime",
15
+ "ReplayEvent",
16
+ "ReplayReport",
17
+ "ReplayViolation",
18
+ "replay_archive",
19
+ ]
@@ -0,0 +1,43 @@
1
+ """Command-line interface for the JEP authority reference runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+ from .runtime import replay_archive
10
+
11
+
12
+ def build_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(
14
+ prog="jep-authority",
15
+ description="Replay and verify JEP-compatible authority semantics archives.",
16
+ )
17
+ subcommands = parser.add_subparsers(dest="command", required=True)
18
+ for command in ("replay", "verify"):
19
+ subparser = subcommands.add_parser(command, help=f"{command} archive.jsonl")
20
+ subparser.add_argument("archive", help="Path to a JSON Lines authority archive")
21
+ subparser.add_argument(
22
+ "--json",
23
+ action="store_true",
24
+ help="Emit a machine-readable JSON report",
25
+ )
26
+ return parser
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ args = build_parser().parse_args(argv)
31
+ report = replay_archive(args.archive, verify_only=args.command == "verify")
32
+ if args.json:
33
+ print(json.dumps(report.to_dict(), indent=2, sort_keys=True))
34
+ else:
35
+ status = "ok" if report.ok else "violations"
36
+ print(f"{args.command}: {status} ({report.events} events)")
37
+ for violation in report.violations:
38
+ print(f"line {violation.line}: {violation.event}: {violation.reason}")
39
+ return 0 if report.ok else 1
40
+
41
+
42
+ if __name__ == "__main__":
43
+ sys.exit(main())
@@ -0,0 +1,420 @@
1
+ """Reference authority semantics for JEP delegation archives.
2
+
3
+ This module intentionally does not implement real IAM and does not replace
4
+ OAuth, X.509, DID, or any production identity/security protocol. It provides a
5
+ small deterministic runtime for checking JEP-compatible authority propagation:
6
+ allowed/denied actions, resource attenuation, expiration, revocation, and
7
+ parent-chain continuity.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime, timezone
14
+ import json
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Any, Iterable, Mapping
18
+ from uuid import uuid4
19
+
20
+ ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
21
+
22
+
23
+ class AuthorityError(ValueError):
24
+ """Raised when an authority operation violates scope semantics."""
25
+
26
+
27
+ def _utc_now() -> datetime:
28
+ return datetime.now(timezone.utc).replace(microsecond=0)
29
+
30
+
31
+ def parse_time(value: str | datetime | None) -> datetime | None:
32
+ """Parse UTC-ish timestamps accepted by archive events."""
33
+ if value is None or isinstance(value, datetime):
34
+ return value
35
+ normalized = value.strip()
36
+ if normalized.endswith("Z"):
37
+ normalized = normalized[:-1] + "+00:00"
38
+ parsed = datetime.fromisoformat(normalized)
39
+ if parsed.tzinfo is None:
40
+ parsed = parsed.replace(tzinfo=timezone.utc)
41
+ return parsed.astimezone(timezone.utc).replace(microsecond=0)
42
+
43
+
44
+ def format_time(value: datetime | None) -> str | None:
45
+ """Format timestamps as stable UTC strings."""
46
+ if value is None:
47
+ return None
48
+ return value.astimezone(timezone.utc).strftime(ISO_FORMAT)
49
+
50
+
51
+ def _as_set(values: Iterable[str] | None) -> set[str]:
52
+ return set(values or [])
53
+
54
+
55
+ def _resource_within(child: str, parent: str) -> bool:
56
+ """Return whether child resource scope is equal to or narrower than parent.
57
+
58
+ The reference runtime uses simple prefix resource semantics. The wildcard
59
+ "*" grants every resource; otherwise a child resource is in scope when it is
60
+ equal to the parent resource or is a slash-delimited descendant of it.
61
+ """
62
+ if any(not isinstance(value, str) or not value or "\\" in value or any(part in {".", ".."} for part in value.split("/")) or re.search(r"%(?:2e|2f|5c|25)", value, re.I) for value in (child, parent)):
63
+ return False
64
+ if parent == "*":
65
+ return True
66
+ if child == parent:
67
+ return True
68
+ normalized_parent = parent.rstrip("/") + "/"
69
+ return child.startswith(normalized_parent)
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class AuthorityScope:
74
+ """Delegable authority scope used by the reference runtime."""
75
+
76
+ actor: str
77
+ subject: str
78
+ allowed_actions: set[str] = field(default_factory=set)
79
+ denied_actions: set[str] = field(default_factory=set)
80
+ resource_scope: str = "*"
81
+ expires_at: datetime | None = None
82
+ parent_scope: str | None = None
83
+ attenuation_rules: dict[str, Any] = field(default_factory=dict)
84
+ scope_id: str = field(default_factory=lambda: f"scope-{uuid4().hex}")
85
+ revoked_at: datetime | None = None
86
+
87
+ def __post_init__(self) -> None:
88
+ self.allowed_actions = _as_set(self.allowed_actions)
89
+ self.denied_actions = _as_set(self.denied_actions)
90
+ self.expires_at = parse_time(self.expires_at)
91
+ self.revoked_at = parse_time(self.revoked_at)
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ return {
95
+ "scope_id": self.scope_id,
96
+ "actor": self.actor,
97
+ "subject": self.subject,
98
+ "allowed_actions": sorted(self.allowed_actions),
99
+ "denied_actions": sorted(self.denied_actions),
100
+ "resource_scope": self.resource_scope,
101
+ "expires_at": format_time(self.expires_at),
102
+ "parent_scope": self.parent_scope,
103
+ "attenuation_rules": self.attenuation_rules,
104
+ "revoked_at": format_time(self.revoked_at),
105
+ }
106
+
107
+ @classmethod
108
+ def from_dict(cls, payload: Mapping[str, Any]) -> "AuthorityScope":
109
+ return cls(
110
+ scope_id=str(payload.get("scope_id") or f"scope-{uuid4().hex}"),
111
+ actor=str(payload["actor"]),
112
+ subject=str(payload["subject"]),
113
+ allowed_actions=set(payload.get("allowed_actions") or []),
114
+ denied_actions=set(payload.get("denied_actions") or []),
115
+ resource_scope=str(payload.get("resource_scope") or "*"),
116
+ expires_at=parse_time(payload.get("expires_at")),
117
+ parent_scope=payload.get("parent_scope"),
118
+ attenuation_rules=dict(payload.get("attenuation_rules") or {}),
119
+ revoked_at=parse_time(payload.get("revoked_at")),
120
+ )
121
+
122
+ def permits(self, action: str, resource: str, at: datetime | None = None) -> tuple[bool, str]:
123
+ checked_at = at or _utc_now()
124
+ if self.revoked_at is not None and checked_at >= self.revoked_at:
125
+ return False, "scope revoked"
126
+ if self.expires_at is not None and checked_at >= self.expires_at:
127
+ return False, "scope expired"
128
+ if action in self.denied_actions:
129
+ return False, "action denied"
130
+ if action not in self.allowed_actions:
131
+ return False, "action not allowed"
132
+ if not _resource_within(resource, self.resource_scope):
133
+ return False, "resource out of scope"
134
+ return True, "ok"
135
+
136
+
137
+ @dataclass(slots=True)
138
+ class VerificationResult:
139
+ ok: bool
140
+ reason: str = "ok"
141
+
142
+
143
+ class DelegationRuntime:
144
+ """In-memory authority runtime for scopes, attenuation, and revocation."""
145
+
146
+ def __init__(self) -> None:
147
+ self.scopes: dict[str, AuthorityScope] = {}
148
+
149
+ def create_scope(
150
+ self,
151
+ *,
152
+ actor: str,
153
+ subject: str,
154
+ allowed_actions: Iterable[str],
155
+ denied_actions: Iterable[str] | None = None,
156
+ resource_scope: str = "*",
157
+ expires_at: str | datetime | None = None,
158
+ attenuation_rules: Mapping[str, Any] | None = None,
159
+ scope_id: str | None = None,
160
+ ) -> AuthorityScope:
161
+ scope = AuthorityScope(
162
+ scope_id=scope_id or f"scope-{uuid4().hex}",
163
+ actor=actor,
164
+ subject=subject,
165
+ allowed_actions=set(allowed_actions),
166
+ denied_actions=set(denied_actions or []),
167
+ resource_scope=resource_scope,
168
+ expires_at=parse_time(expires_at),
169
+ parent_scope=None,
170
+ attenuation_rules=dict(attenuation_rules or {}),
171
+ )
172
+ if scope.scope_id in self.scopes:
173
+ raise AuthorityError("scope_id already exists")
174
+ self.scopes[scope.scope_id] = scope
175
+ return scope
176
+
177
+ def delegate_scope(
178
+ self,
179
+ *,
180
+ parent_scope: str,
181
+ actor: str,
182
+ subject: str,
183
+ allowed_actions: Iterable[str] | None = None,
184
+ denied_actions: Iterable[str] | None = None,
185
+ resource_scope: str | None = None,
186
+ expires_at: str | datetime | None = None,
187
+ attenuation_rules: Mapping[str, Any] | None = None,
188
+ scope_id: str | None = None,
189
+ at: str | datetime | None = None,
190
+ ) -> AuthorityScope:
191
+ parent = self._require_scope(parent_scope)
192
+ allowed = set(allowed_actions) if allowed_actions is not None else set(parent.allowed_actions)
193
+ denied = parent.denied_actions | set(denied_actions or [])
194
+ child_resource = resource_scope or parent.resource_scope
195
+ child_expires = parse_time(expires_at) or parent.expires_at
196
+ child = AuthorityScope(
197
+ scope_id=scope_id or f"scope-{uuid4().hex}",
198
+ actor=actor,
199
+ subject=subject,
200
+ allowed_actions=allowed,
201
+ denied_actions=denied,
202
+ resource_scope=child_resource,
203
+ expires_at=child_expires,
204
+ parent_scope=parent.scope_id,
205
+ attenuation_rules={**parent.attenuation_rules, **dict(attenuation_rules or {})},
206
+ )
207
+ result = self.verify_delegation_path(child, at=at)
208
+ if not result.ok:
209
+ raise AuthorityError(result.reason)
210
+ if child.scope_id in self.scopes:
211
+ raise AuthorityError("scope_id already exists")
212
+ self.scopes[child.scope_id] = child
213
+ return child
214
+
215
+ def attenuate_scope(self, scope_id: str, **updates: Any) -> AuthorityScope:
216
+ existing = self._require_scope(scope_id)
217
+ return self.delegate_scope(
218
+ parent_scope=scope_id,
219
+ actor=updates.get("actor", existing.subject),
220
+ subject=updates["subject"],
221
+ allowed_actions=updates.get("allowed_actions", existing.allowed_actions),
222
+ denied_actions=updates.get("denied_actions", set()),
223
+ resource_scope=updates.get("resource_scope", existing.resource_scope),
224
+ expires_at=updates.get("expires_at", existing.expires_at),
225
+ attenuation_rules=updates.get("attenuation_rules", {}),
226
+ scope_id=updates.get("scope_id"),
227
+ )
228
+
229
+ def revoke_scope(self, scope_id: str, revoked_at: str | datetime | None = None) -> AuthorityScope:
230
+ scope = self._require_scope(scope_id)
231
+ revoked = parse_time(revoked_at) or _utc_now()
232
+ scope.revoked_at = min(scope.revoked_at, revoked) if scope.revoked_at else revoked
233
+ return scope
234
+
235
+ def verify_scope(
236
+ self,
237
+ scope_id: str,
238
+ *,
239
+ action: str,
240
+ resource: str,
241
+ at: str | datetime | None = None,
242
+ ) -> VerificationResult:
243
+ scope = self._require_scope(scope_id)
244
+ ok, reason = scope.permits(action, resource, parse_time(at))
245
+ if not ok:
246
+ return VerificationResult(False, reason)
247
+ path = self.verify_delegation_path(scope, at=parse_time(at))
248
+ if not path.ok:
249
+ return path
250
+ return VerificationResult(True)
251
+
252
+ def verify_delegation_path(
253
+ self,
254
+ scope_or_id: AuthorityScope | str,
255
+ *,
256
+ at: str | datetime | None = None,
257
+ ) -> VerificationResult:
258
+ scope = self._require_scope(scope_or_id) if isinstance(scope_or_id, str) else scope_or_id
259
+ checked_at = parse_time(at) or _utc_now()
260
+ if scope.revoked_at is not None and checked_at >= scope.revoked_at:
261
+ return VerificationResult(False, "scope revoked")
262
+ if scope.expires_at is not None and checked_at >= scope.expires_at:
263
+ return VerificationResult(False, "scope expired")
264
+ seen: set[str] = set()
265
+ child = scope
266
+ while child.parent_scope is not None:
267
+ if child.scope_id in seen:
268
+ return VerificationResult(False, "delegation chain cycle")
269
+ seen.add(child.scope_id)
270
+ parent = self.scopes.get(child.parent_scope)
271
+ if parent is None:
272
+ return VerificationResult(False, "delegation chain broken")
273
+ if child.actor != parent.subject:
274
+ return VerificationResult(False, "delegating actor is not parent subject")
275
+ if parent.revoked_at is not None and checked_at >= parent.revoked_at:
276
+ return VerificationResult(False, "parent scope revoked")
277
+ if parent.expires_at is not None and checked_at >= parent.expires_at:
278
+ return VerificationResult(False, "parent scope expired")
279
+ if not child.allowed_actions <= parent.allowed_actions:
280
+ return VerificationResult(False, "child allows actions outside parent scope")
281
+ if not parent.denied_actions <= child.denied_actions:
282
+ return VerificationResult(False, "child omits parent denied actions")
283
+ if child.allowed_actions & parent.denied_actions:
284
+ return VerificationResult(False, "child allows parent-denied action")
285
+ if not _resource_within(child.resource_scope, parent.resource_scope):
286
+ return VerificationResult(False, "child resource outside parent scope")
287
+ if parent.expires_at is not None and (child.expires_at is None or child.expires_at > parent.expires_at):
288
+ return VerificationResult(False, "child expires after parent")
289
+ child = parent
290
+ return VerificationResult(True)
291
+
292
+ def _require_scope(self, scope_id: str) -> AuthorityScope:
293
+ try:
294
+ return self.scopes[scope_id]
295
+ except KeyError as exc:
296
+ raise AuthorityError(f"unknown scope: {scope_id}") from exc
297
+
298
+
299
+ @dataclass(slots=True)
300
+ class ReplayEvent:
301
+ line: int
302
+ event: str
303
+ payload: dict[str, Any]
304
+
305
+
306
+ @dataclass(slots=True)
307
+ class ReplayViolation:
308
+ line: int
309
+ event: str
310
+ reason: str
311
+ payload: dict[str, Any]
312
+
313
+
314
+ @dataclass(slots=True)
315
+ class ReplayReport:
316
+ ok: bool
317
+ events: int
318
+ violations: list[ReplayViolation]
319
+
320
+ def to_dict(self) -> dict[str, Any]:
321
+ return {
322
+ "ok": self.ok,
323
+ "events": self.events,
324
+ "violations": [
325
+ {
326
+ "line": violation.line,
327
+ "event": violation.event,
328
+ "reason": violation.reason,
329
+ "payload": violation.payload,
330
+ }
331
+ for violation in self.violations
332
+ ],
333
+ }
334
+
335
+
336
+ def load_archive(path: str | Path) -> list[ReplayEvent]:
337
+ events: list[ReplayEvent] = []
338
+ with Path(path).open("r", encoding="utf-8") as handle:
339
+ for line_number, raw in enumerate(handle, 1):
340
+ stripped = raw.strip()
341
+ if not stripped:
342
+ continue
343
+ payload = json.loads(stripped)
344
+ event = str(payload.get("event") or payload.get("type") or "")
345
+ events.append(ReplayEvent(line_number, event, payload))
346
+ return events
347
+
348
+
349
+ def replay_archive(path: str | Path, *, verify_only: bool = False) -> ReplayReport:
350
+ """Replay an archive.jsonl and report authority propagation violations."""
351
+ runtime = DelegationRuntime()
352
+ violations: list[ReplayViolation] = []
353
+ events = load_archive(path)
354
+
355
+ for item in events:
356
+ try:
357
+ _apply_event(runtime, item, verify_only=verify_only)
358
+ except (AuthorityError, KeyError, TypeError, ValueError) as exc:
359
+ violations.append(ReplayViolation(item.line, item.event, str(exc), item.payload))
360
+
361
+ return ReplayReport(ok=not violations, events=len(events), violations=violations)
362
+
363
+
364
+ def _apply_event(runtime: DelegationRuntime, item: ReplayEvent, *, verify_only: bool) -> None:
365
+ payload = item.payload
366
+ event = item.event
367
+ if event in {"delegate_scope", "attenuate_scope", "action", "verify_scope", "verify_delegation_path"} and not payload.get("at"):
368
+ raise AuthorityError("archive event requires recorded at timestamp")
369
+ if event == "revoke_scope" and not (payload.get("revoked_at") or payload.get("at")):
370
+ raise AuthorityError("archive revocation requires recorded timestamp")
371
+ if event == "create_scope":
372
+ runtime.create_scope(
373
+ scope_id=payload.get("scope_id"),
374
+ actor=payload["actor"],
375
+ subject=payload["subject"],
376
+ allowed_actions=payload.get("allowed_actions") or [],
377
+ denied_actions=payload.get("denied_actions") or [],
378
+ resource_scope=payload.get("resource_scope") or "*",
379
+ expires_at=payload.get("expires_at"),
380
+ attenuation_rules=payload.get("attenuation_rules") or {},
381
+ )
382
+ return
383
+ if event in {"delegate_scope", "attenuate_scope"}:
384
+ runtime.delegate_scope(
385
+ scope_id=payload.get("scope_id"),
386
+ at=payload["at"],
387
+ parent_scope=payload["parent_scope"],
388
+ actor=payload["actor"],
389
+ subject=payload["subject"],
390
+ allowed_actions=payload.get("allowed_actions"),
391
+ denied_actions=payload.get("denied_actions") or [],
392
+ resource_scope=payload.get("resource_scope"),
393
+ expires_at=payload.get("expires_at"),
394
+ attenuation_rules=payload.get("attenuation_rules") or {},
395
+ )
396
+ return
397
+ if event == "revoke_scope":
398
+ runtime.revoke_scope(payload["scope_id"], payload.get("revoked_at") or payload.get("at"))
399
+ return
400
+ if event in {"action", "verify_scope"}:
401
+ result = runtime.verify_scope(
402
+ payload["scope_id"],
403
+ action=payload["action"],
404
+ resource=payload["resource"],
405
+ at=payload.get("at"),
406
+ )
407
+ expected = payload.get("expect")
408
+ if expected in {"deny", "denied", False}:
409
+ if result.ok:
410
+ raise AuthorityError("expected denial but action was allowed")
411
+ return
412
+ if not result.ok:
413
+ raise AuthorityError(result.reason)
414
+ return
415
+ if event == "verify_delegation_path":
416
+ result = runtime.verify_delegation_path(payload["scope_id"], at=payload.get("at"))
417
+ if not result.ok:
418
+ raise AuthorityError(result.reason)
419
+ return
420
+ raise AuthorityError(f"unknown event: {event}")
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: jep-authority-runtime
3
+ Version: 0.1.1
4
+ Summary: Reference runtime for JEP-compatible authority scope semantics.
5
+ Author: JEP Authority Runtime contributors
6
+ License: MIT
7
+ Keywords: jep,authority,delegation,attenuation,replay
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # jep-authority-runtime
14
+
15
+ Reference runtime for JEP authority scope, delegation boundaries, attenuation, revocation, and replay verification.
16
+
17
+ This project is intentionally **not** real IAM and does not replace OAuth, X.509, DID, or any production authorization protocol. It is a small JEP-compatible reference runtime for making delegation semantics replayable and verifiable.
18
+
19
+ ## AuthorityScope
20
+
21
+ `AuthorityScope` models the authority passed along a delegation chain:
22
+
23
+ - `actor`: principal that issued the scope.
24
+ - `subject`: principal receiving the scope.
25
+ - `allowed_actions`: actions the subject may perform.
26
+ - `denied_actions`: actions explicitly forbidden and inherited by descendants.
27
+ - `resource_scope`: resource prefix the scope applies to (`*` means every resource).
28
+ - `expires_at`: optional UTC expiration timestamp.
29
+ - `parent_scope`: optional parent scope id for delegated or attenuated authority.
30
+ - `attenuation_rules`: reference metadata describing how authority was narrowed.
31
+
32
+ The runtime also stores `scope_id` and `revoked_at` so archives can refer to scopes deterministically and model revocation.
33
+
34
+ ## DelegationRuntime
35
+
36
+ `DelegationRuntime` provides:
37
+
38
+ - `create_scope()` for root authority grants.
39
+ - `delegate_scope()` for child scopes that must be narrower than their parent.
40
+ - `attenuate_scope()` as a convenience wrapper for narrower delegation.
41
+ - `revoke_scope()` to invalidate a scope and its descendants for later checks.
42
+ - `verify_scope()` to check action, resource, expiration, revocation, and chain validity.
43
+ - `verify_delegation_path()` to check parent continuity and attenuation constraints.
44
+
45
+ A child scope is valid only when it does not add actions, omit inherited denials, widen resources, outlive its parent, or rely on a missing/revoked/expired parent.
46
+
47
+ ## Replay archive format
48
+
49
+ Archives are JSON Lines (`archive.jsonl`). Each line is an event with an `event` field:
50
+
51
+ ```json
52
+ {"event":"create_scope","scope_id":"human-search","actor":"human:alice","subject":"agent:searcher","allowed_actions":["search"],"denied_actions":["payment"],"resource_scope":"web/search","expires_at":"2030-01-01T00:00:00Z"}
53
+ {"event":"delegate_scope","scope_id":"agent-news","parent_scope":"human-search","actor":"agent:searcher","subject":"agent:sub-searcher","allowed_actions":["search"],"resource_scope":"web/search/news"}
54
+ {"event":"action","scope_id":"agent-news","action":"search","resource":"web/search/news/politics","at":"2028-01-01T00:00:00Z"}
55
+ {"event":"revoke_scope","scope_id":"human-search","revoked_at":"2028-06-01T00:00:00Z"}
56
+ ```
57
+
58
+ Replay checks whether propagation or actions are invalid because authority is exceeded, a scope is expired, a scope or ancestor is revoked, a child exceeds the parent scope, or the delegation chain is broken.
59
+
60
+ ## CLI
61
+
62
+ Install in editable mode:
63
+
64
+ ```sh
65
+ python -m pip install -e .
66
+ ```
67
+
68
+ Replay an archive:
69
+
70
+ ```sh
71
+ jep-authority replay examples/archive.jsonl
72
+ ```
73
+
74
+ Verify an archive and emit JSON:
75
+
76
+ ```sh
77
+ jep-authority verify examples/archive.jsonl --json
78
+ ```
79
+
80
+ Both commands exit with status `0` when the report has no violations and `1` when violations are found.
81
+
82
+ ## Example scenario
83
+
84
+ `examples/archive.jsonl` demonstrates:
85
+
86
+ 1. A human delegates limited `search` permission to an agent.
87
+ 2. The agent delegates narrower `web/search/news` permission to a sub-agent.
88
+ 3. The sub-agent performs an allowed search.
89
+ 4. The sub-agent attempts a forbidden `payment` action.
90
+ 5. Revocation of the parent invalidates a later delegated action.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ jep_authority/__init__.py
4
+ jep_authority/cli.py
5
+ jep_authority/runtime.py
6
+ jep_authority_runtime.egg-info/PKG-INFO
7
+ jep_authority_runtime.egg-info/SOURCES.txt
8
+ jep_authority_runtime.egg-info/dependency_links.txt
9
+ jep_authority_runtime.egg-info/entry_points.txt
10
+ jep_authority_runtime.egg-info/top_level.txt
11
+ tests/test_hardening.py
12
+ tests/test_resource_bounds.py
13
+ tests/test_runtime.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jep-authority = jep_authority.cli:main
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jep-authority-runtime"
7
+ version = "0.1.1"
8
+ description = "Reference runtime for JEP-compatible authority scope semantics."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "JEP Authority Runtime contributors"}]
13
+ keywords = ["jep", "authority", "delegation", "attenuation", "replay"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ ]
18
+
19
+ [project.scripts]
20
+ jep-authority = "jep_authority.cli:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ include = ["jep_authority*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ import json
2
+ import pytest
3
+ from jep_authority.runtime import AuthorityError, DelegationRuntime, replay_archive
4
+
5
+
6
+ def test_actor_binding_and_scope_ids_cannot_be_replaced():
7
+ runtime = DelegationRuntime()
8
+ runtime.create_scope(scope_id="root", actor="owner", subject="worker", allowed_actions=["read"])
9
+ with pytest.raises(AuthorityError, match="parent subject"):
10
+ runtime.delegate_scope(parent_scope="root", actor="stranger", subject="child")
11
+ runtime.revoke_scope("root", "2020-01-01T00:00:00Z")
12
+ with pytest.raises(AuthorityError, match="already exists"):
13
+ runtime.create_scope(scope_id="root", actor="owner", subject="worker", allowed_actions=["read"])
14
+
15
+
16
+ def test_historical_delegation_uses_recorded_time(tmp_path):
17
+ rows = [
18
+ {"event": "create_scope", "scope_id": "root", "actor": "owner", "subject": "worker", "allowed_actions": ["read"], "expires_at": "2021-01-01T00:00:00Z"},
19
+ {"event": "delegate_scope", "scope_id": "child", "parent_scope": "root", "actor": "worker", "subject": "child", "at": "2020-01-01T00:00:00Z"},
20
+ {"event": "action", "scope_id": "child", "action": "read", "resource": "file", "at": "2020-02-01T00:00:00Z"},
21
+ ]
22
+ path = tmp_path / "archive.jsonl"
23
+ path.write_text("\n".join(map(json.dumps, rows)))
24
+ assert replay_archive(path).ok
25
+ rows[-1]["expect"] = "deny"
26
+ path.write_text("\n".join(map(json.dumps, rows)))
27
+ assert not replay_archive(path, verify_only=True).ok
@@ -0,0 +1,15 @@
1
+ from datetime import datetime, timezone
2
+ from jep_authority.runtime import AuthorityScope
3
+
4
+
5
+ def test_resources_do_not_escape_by_path_traversal():
6
+ scope = AuthorityScope(actor="a", subject="b", allowed_actions={"read"}, resource_scope="repo/private")
7
+ for resource in ["repo/private/../public", "repo/private/%2e%2e/public", "repo/private/..\\public", "repo/private2"]:
8
+ assert not scope.permits("read", resource)[0]
9
+ assert scope.permits("read", "repo/private/file")[0]
10
+
11
+
12
+ def test_expiry_is_exclusive():
13
+ end = datetime(2026, 1, 1, tzinfo=timezone.utc)
14
+ scope = AuthorityScope(actor="a", subject="b", allowed_actions={"read"}, expires_at=end)
15
+ assert not scope.permits("read", "anything", end)[0]
@@ -0,0 +1,92 @@
1
+ from pathlib import Path
2
+ import tempfile
3
+ import unittest
4
+
5
+ from jep_authority.runtime import AuthorityError, DelegationRuntime, replay_archive
6
+
7
+
8
+ class AuthorityRuntimeTests(unittest.TestCase):
9
+ def test_delegation_and_verification(self):
10
+ runtime = DelegationRuntime()
11
+ parent = runtime.create_scope(
12
+ scope_id="human-search",
13
+ actor="human:alice",
14
+ subject="agent:searcher",
15
+ allowed_actions=["search"],
16
+ denied_actions=["payment"],
17
+ resource_scope="web/search",
18
+ expires_at="2030-01-01T00:00:00Z",
19
+ )
20
+ child = runtime.delegate_scope(
21
+ scope_id="agent-news",
22
+ parent_scope=parent.scope_id,
23
+ actor="agent:searcher",
24
+ subject="agent:sub-searcher",
25
+ allowed_actions=["search"],
26
+ resource_scope="web/search/news",
27
+ expires_at="2029-01-01T00:00:00Z",
28
+ )
29
+
30
+ self.assertTrue(
31
+ runtime.verify_scope(
32
+ child.scope_id,
33
+ action="search",
34
+ resource="web/search/news/politics",
35
+ at="2028-01-01T00:00:00Z",
36
+ ).ok
37
+ )
38
+ payment = runtime.verify_scope(
39
+ child.scope_id,
40
+ action="payment",
41
+ resource="payments/card",
42
+ at="2028-01-01T00:00:00Z",
43
+ )
44
+ self.assertFalse(payment.ok)
45
+ self.assertEqual(payment.reason, "action denied")
46
+
47
+ def test_delegate_cannot_exceed_parent_scope(self):
48
+ runtime = DelegationRuntime()
49
+ runtime.create_scope(
50
+ scope_id="parent",
51
+ actor="human:alice",
52
+ subject="agent:a",
53
+ allowed_actions=["search"],
54
+ resource_scope="web/search",
55
+ )
56
+ with self.assertRaisesRegex(AuthorityError, "outside parent"):
57
+ runtime.delegate_scope(
58
+ scope_id="child",
59
+ parent_scope="parent",
60
+ actor="agent:a",
61
+ subject="agent:b",
62
+ allowed_actions=["payment"],
63
+ resource_scope="web/search",
64
+ )
65
+
66
+ def test_replay_reports_revocation_violation(self):
67
+ archive = "\n".join(
68
+ [
69
+ '{"event":"create_scope","scope_id":"s1","actor":"human","subject":"agent","allowed_actions":["search"],"resource_scope":"web/search"}',
70
+ '{"event":"revoke_scope","scope_id":"s1","revoked_at":"2028-01-01T00:00:00Z"}',
71
+ '{"event":"action","scope_id":"s1","action":"search","resource":"web/search/news","at":"2028-01-02T00:00:00Z"}',
72
+ ]
73
+ )
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+ path = Path(tmp) / "archive.jsonl"
76
+ path.write_text(archive, encoding="utf-8")
77
+ report = replay_archive(path)
78
+ self.assertFalse(report.ok)
79
+ self.assertEqual(report.violations[0].reason, "scope revoked")
80
+
81
+ def test_replay_reports_broken_chain(self):
82
+ archive = '{"event":"delegate_scope","at":"2025-01-01T00:00:00Z","scope_id":"orphan","parent_scope":"missing","actor":"a","subject":"b","allowed_actions":["search"],"resource_scope":"web/search"}\n'
83
+ with tempfile.TemporaryDirectory() as tmp:
84
+ path = Path(tmp) / "archive.jsonl"
85
+ path.write_text(archive, encoding="utf-8")
86
+ report = replay_archive(path)
87
+ self.assertFalse(report.ok)
88
+ self.assertIn("unknown scope", report.violations[0].reason)
89
+
90
+
91
+ if __name__ == "__main__":
92
+ unittest.main()