present-day 0.14.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.
Files changed (67) hide show
  1. present_day/__init__.py +4 -0
  2. present_day/agent.py +58 -0
  3. present_day/backends.py +87 -0
  4. present_day/canon.py +52 -0
  5. present_day/claims.py +148 -0
  6. present_day/cli.py +1045 -0
  7. present_day/client.py +122 -0
  8. present_day/cloud/__init__.py +1 -0
  9. present_day/cloud/app.py +1041 -0
  10. present_day/cloud/backup.py +199 -0
  11. present_day/cloud/db.py +168 -0
  12. present_day/cloud/mail.py +53 -0
  13. present_day/cloud/metrics.py +112 -0
  14. present_day/cloud/migrate.py +90 -0
  15. present_day/cloud/oidc.py +140 -0
  16. present_day/cloud/pgdb.py +117 -0
  17. present_day/cloud/plans.py +21 -0
  18. present_day/cloud/ratelimit.py +66 -0
  19. present_day/cloud/remote.py +220 -0
  20. present_day/cloud/rotate.py +64 -0
  21. present_day/cloud/saml.py +87 -0
  22. present_day/cloud/security.py +162 -0
  23. present_day/cloud/service.py +1858 -0
  24. present_day/cloud/settings.py +159 -0
  25. present_day/cloud/stripe.py +67 -0
  26. present_day/cloud/web/__init__.py +0 -0
  27. present_day/cloud/web/app.html +22 -0
  28. present_day/cloud/web/cabinet.css +136 -0
  29. present_day/cloud/web/cabinet.js +868 -0
  30. present_day/cloud/web/login.html +54 -0
  31. present_day/cloud/web/login.js +111 -0
  32. present_day/crypto.py +105 -0
  33. present_day/engine.py +328 -0
  34. present_day/evalkit.py +156 -0
  35. present_day/evidence.py +284 -0
  36. present_day/export.py +118 -0
  37. present_day/gateway.py +496 -0
  38. present_day/home.py +92 -0
  39. present_day/injection.py +355 -0
  40. present_day/integrations/__init__.py +119 -0
  41. present_day/integrations/langchain.py +19 -0
  42. present_day/integrations/llamaindex.py +19 -0
  43. present_day/integrations/openai_agents.py +20 -0
  44. present_day/ledger.py +370 -0
  45. present_day/llm.py +224 -0
  46. present_day/mcp_server.py +143 -0
  47. present_day/merkle.py +168 -0
  48. present_day/node.py +278 -0
  49. present_day/recorder.py +151 -0
  50. present_day/redteam.py +220 -0
  51. present_day/rules.py +256 -0
  52. present_day/soc.py +528 -0
  53. present_day/soc_demo.py +120 -0
  54. present_day/stores.py +310 -0
  55. present_day/sync.py +70 -0
  56. present_day/trust.py +379 -0
  57. present_day/watch.py +386 -0
  58. present_day/web/__init__.py +0 -0
  59. present_day/web/dashboard.css +22 -0
  60. present_day/web/dashboard.js +107 -0
  61. present_day/web/index.html +34 -0
  62. present_day-0.14.0.dist-info/METADATA +52 -0
  63. present_day-0.14.0.dist-info/RECORD +67 -0
  64. present_day-0.14.0.dist-info/WHEEL +5 -0
  65. present_day-0.14.0.dist-info/entry_points.txt +3 -0
  66. present_day-0.14.0.dist-info/licenses/LICENSE +177 -0
  67. present_day-0.14.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ """Present Day: tamper-evident signed memory and provenance for AI agents (protocol P7)."""
2
+
3
+ __version__ = "0.14.0"
4
+ PROTOCOL = "P7/1"
present_day/agent.py ADDED
@@ -0,0 +1,58 @@
1
+ """Agent home: the agent's own signing key plus the cabinet connection it should use.
2
+
3
+ The private key never leaves this directory; the cabinet only ever sees signed entries.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+
12
+ from .client import CloudClient
13
+ from .crypto import NAME_RE, Identity
14
+
15
+
16
+ class AgentError(Exception):
17
+ pass
18
+
19
+
20
+ def default_path() -> Path:
21
+ return Path(os.environ.get("P7_AGENT_HOME") or Path.home() / ".present-day-agent")
22
+
23
+
24
+ class AgentHome:
25
+ def __init__(self, path: str | Path | None = None):
26
+ self.path = Path(path) if path else default_path()
27
+ self.cfg_path = self.path / "agent.json"
28
+
29
+ def exists(self) -> bool:
30
+ return self.cfg_path.exists()
31
+
32
+ def init(self, name: str, url: str, token: str, workspace: str) -> Identity:
33
+ if not NAME_RE.match(name):
34
+ raise AgentError("name must match [A-Za-z0-9_-]{1,32}")
35
+ if self.exists():
36
+ raise AgentError(f"{self.path} is already initialised")
37
+ self.path.mkdir(parents=True, exist_ok=True, mode=0o700)
38
+ os.chmod(self.path, 0o700)
39
+ ident = Identity.generate()
40
+ ident.save(self.path / "agent.pem", os.environ.get("P7_PASSPHRASE") or None)
41
+ cfg = {"name": name, "url": url.rstrip("/"), "workspace": workspace, "token": token}
42
+ fd = os.open(self.cfg_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
43
+ with os.fdopen(fd, "w") as f:
44
+ json.dump(cfg, f, indent=2)
45
+ return ident
46
+
47
+ def config(self) -> dict:
48
+ try:
49
+ return json.loads(self.cfg_path.read_text())
50
+ except (OSError, ValueError) as e:
51
+ raise AgentError(f"agent not initialised in {self.path}: run `present-day agent init`") from e
52
+
53
+ def identity(self) -> Identity:
54
+ return Identity.load(self.path / "agent.pem", os.environ.get("P7_PASSPHRASE") or None)
55
+
56
+ def client(self) -> CloudClient:
57
+ c = self.config()
58
+ return CloudClient(c["url"], c["token"], c["workspace"], self.identity())
@@ -0,0 +1,87 @@
1
+ """Memory backends shared by the MCP server and the framework integrations (no optional dependencies).
2
+
3
+ * local : a node home on this machine (PRESENT_DAY_HOME, key P7_IDENTITY, default "agent")
4
+ * cloud : a hosted workspace; the agent signs with its own key and authenticates with an API key
5
+ (P7_AGENT_HOME, created by `present-day agent init`)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .agent import AgentHome
11
+ from .home import Home
12
+
13
+
14
+ class LocalBackend:
15
+ def __init__(self, home: Home, identity_name: str):
16
+ self.engine, self._home, self._name = home.engine(), home, identity_name
17
+
18
+ def _me(self):
19
+ return self._home.identity(self._name)
20
+
21
+ def remember(self, subject, predicate, obj, confidence, source_uri, supersedes) -> dict:
22
+ e = self.engine.claim(self._me(), subject, predicate, obj, confidence=confidence,
23
+ sources=[{"uri": source_uri}] if source_uri else None,
24
+ supersedes=[supersedes] if supersedes else None)
25
+ return {"entry": e.entry_hash, "seq": e.seq}
26
+
27
+ def recall(self, subject, predicate, status, limit) -> list[dict]:
28
+ return self.engine.query(subject=subject or None, predicate=predicate or None,
29
+ status=status or None, limit=limit)
30
+
31
+ def attest(self, target, stance, note) -> dict:
32
+ e = self.engine.attest(self._me(), target, stance, note)
33
+ return {"entry": e.entry_hash, "seq": e.seq}
34
+
35
+ def history(self, subject, predicate) -> list[dict]:
36
+ return self.engine.history(subject, predicate)
37
+
38
+ def provenance(self, entry) -> dict:
39
+ return self.engine.provenance(entry)
40
+
41
+ def verify(self) -> dict:
42
+ return self.engine.verify()
43
+
44
+ def alerts(self) -> list[dict]:
45
+ return [a.to_dict() for a in self.engine.alerts()]
46
+
47
+ def receipt(self, subject, predicate, question, answer) -> dict:
48
+ return self.engine.answer_receipt(subject, predicate, question, answer, self._me())
49
+
50
+ def evidence(self, subject, predicate) -> dict:
51
+ return self.engine.evidence_for(subject, predicate)
52
+
53
+
54
+ class CloudBackend:
55
+ def __init__(self, agent: AgentHome):
56
+ self.c = agent.client()
57
+
58
+ def remember(self, subject, predicate, obj, confidence, source_uri, supersedes) -> dict:
59
+ r = self.c.claim(subject, predicate, obj, confidence=confidence,
60
+ sources=[{"uri": source_uri}] if source_uri else None,
61
+ supersedes=[supersedes] if supersedes else None)
62
+ return {"entry": r["entry_hash"], "seq": r["seq"]}
63
+
64
+ def recall(self, subject, predicate, status, limit) -> list[dict]:
65
+ return self.c.recall(subject=subject, predicate=predicate, status=status, limit=str(limit))
66
+
67
+ def attest(self, target, stance, note) -> dict:
68
+ r = self.c.attest(target, stance, note) # signed locally; needs the full 64-hex hash
69
+ return {"entry": r["entry_hash"], "seq": r["seq"]}
70
+
71
+ def history(self, subject, predicate) -> list[dict]:
72
+ return self.c.history(subject, predicate)
73
+
74
+ def provenance(self, entry) -> dict:
75
+ return self.c.provenance(entry)
76
+
77
+ def verify(self) -> dict:
78
+ return self.c.verify()
79
+
80
+ def alerts(self) -> list[dict]:
81
+ return self.c.alerts()
82
+
83
+ def receipt(self, subject, predicate, question, answer) -> dict:
84
+ return self.c.answer_receipt(subject, predicate, question, answer)
85
+
86
+ def evidence(self, subject, predicate) -> dict:
87
+ return self.c.evidence(subject, predicate)
present_day/canon.py ADDED
@@ -0,0 +1,52 @@
1
+ """Canonical JSON (P7-canon-1): sorted keys, no whitespace, UTF-8, no floats.
2
+
3
+ Floats are rejected on purpose: their text form is not stable across runtimes,
4
+ which would break signatures. Confidence and similar values are integers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+
13
+ class CanonError(ValueError):
14
+ pass
15
+
16
+
17
+ _MAX_DEPTH = 32
18
+ _MAX_INT = 2**53
19
+
20
+
21
+ def _check(o: Any, depth: int = 0) -> None:
22
+ if depth > _MAX_DEPTH:
23
+ raise CanonError("structure too deep")
24
+ if o is None or isinstance(o, (bool, str)):
25
+ return
26
+ if isinstance(o, int):
27
+ if not -_MAX_INT < o < _MAX_INT:
28
+ raise CanonError("integer out of range")
29
+ return
30
+ if isinstance(o, float):
31
+ raise CanonError("floats are not allowed; use integers")
32
+ if isinstance(o, (list, tuple)):
33
+ for x in o:
34
+ _check(x, depth + 1)
35
+ return
36
+ if isinstance(o, dict):
37
+ for k, v in o.items():
38
+ if not isinstance(k, str):
39
+ raise CanonError("object keys must be strings")
40
+ _check(v, depth + 1)
41
+ return
42
+ raise CanonError(f"unsupported type: {type(o).__name__}")
43
+
44
+
45
+ def canonical(o: Any) -> bytes:
46
+ _check(o)
47
+ try:
48
+ return json.dumps(
49
+ o, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False
50
+ ).encode("utf-8")
51
+ except UnicodeEncodeError as e:
52
+ raise CanonError("invalid unicode (lone surrogate)") from e
present_day/claims.py ADDED
@@ -0,0 +1,148 @@
1
+ """P7 entry types and strict payload validation (unknown fields are rejected)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, Callable
7
+
8
+ HEX64 = re.compile(r"^[0-9a-f]{64}$")
9
+ CTRL = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") # control chars except \t \n \r
10
+
11
+ GOVERNANCE_TYPES = {"genesis", "identity", "vouch", "revoke"}
12
+ CONTENT_TYPES = {"claim", "attest", "observation", "retract", "rollback"}
13
+ ALL_TYPES = GOVERNANCE_TYPES | CONTENT_TYPES
14
+ ROLES = {"agent", "source", "human", "node"}
15
+ STANCES = {"support", "dispute"}
16
+
17
+
18
+ class SchemaError(ValueError):
19
+ pass
20
+
21
+
22
+ def _s(maxlen: int, *, oneline: bool = False, minlen: int = 0) -> Callable[[Any], str]:
23
+ def check(v: Any) -> str:
24
+ if not isinstance(v, str):
25
+ raise SchemaError("must be a string")
26
+ if not minlen <= len(v) <= maxlen:
27
+ raise SchemaError(f"length must be {minlen}..{maxlen}")
28
+ if CTRL.search(v) or (oneline and ("\n" in v or "\r" in v)):
29
+ raise SchemaError("contains control characters")
30
+ return v
31
+ return check
32
+
33
+
34
+ def _hex64(v: Any) -> str:
35
+ if not isinstance(v, str) or not HEX64.match(v):
36
+ raise SchemaError("must be 64 lowercase hex chars")
37
+ return v
38
+
39
+
40
+ def _int(lo: int, hi: int) -> Callable[[Any], int]:
41
+ def check(v: Any) -> int:
42
+ if isinstance(v, bool) or not isinstance(v, int) or not lo <= v <= hi:
43
+ raise SchemaError(f"must be an integer {lo}..{hi}")
44
+ return v
45
+ return check
46
+
47
+
48
+ def _enum(options: set[str]) -> Callable[[Any], str]:
49
+ def check(v: Any) -> str:
50
+ if v not in options:
51
+ raise SchemaError(f"must be one of {sorted(options)}")
52
+ return v
53
+ return check
54
+
55
+
56
+ def _list(item: Callable[[Any], Any], maxn: int) -> Callable[[Any], list]:
57
+ def check(v: Any) -> list:
58
+ if not isinstance(v, list) or len(v) > maxn:
59
+ raise SchemaError(f"must be a list of at most {maxn} items")
60
+ return [item(x) for x in v]
61
+ return check
62
+
63
+
64
+ def _source(v: Any) -> dict:
65
+ if not isinstance(v, dict):
66
+ raise SchemaError("source must be an object")
67
+ return _validate(v, {
68
+ "uri": (_s(2048, oneline=True, minlen=1), False),
69
+ "entry": (_hex64, False),
70
+ "content_hash": (_hex64, False),
71
+ }, require_any=("uri", "entry"))
72
+
73
+
74
+ # field -> (validator, required)
75
+ SCHEMAS: dict[str, dict[str, tuple[Callable[[Any], Any], bool]]] = {
76
+ "genesis": {"name": (_s(64, oneline=True, minlen=1), True)},
77
+ "identity": {
78
+ "role": (_enum(ROLES), True),
79
+ "name": (_s(64, oneline=True, minlen=1), True),
80
+ },
81
+ "vouch": {"target": (_hex64, True), "weight": (_int(1, 100), False)},
82
+ "revoke": {"target": (_hex64, True), "reason": (_s(256), False)},
83
+ "claim": {
84
+ "subject": (_s(256, oneline=True, minlen=1), True),
85
+ "predicate": (_s(128, oneline=True, minlen=1), True),
86
+ "object": (_s(4096), True),
87
+ "confidence": (_int(0, 100), False),
88
+ "sources": (_list(_source, 20), False),
89
+ "supersedes": (_list(_hex64, 20), False),
90
+ "tags": (_list(_s(32, oneline=True, minlen=1), 10), False),
91
+ "text": (_s(8192), False),
92
+ },
93
+ "attest": {
94
+ "target": (_hex64, True),
95
+ "stance": (_enum(STANCES), True),
96
+ "note": (_s(512), False),
97
+ },
98
+ "retract": {"targets": (_list(_hex64, 50), True), "reason": (_s(256), False)},
99
+ "rollback": {"from_seq": (_int(2, 2**40), True), "reason": (_s(256), False)},
100
+ "observation": {
101
+ "source": (_s(2048, oneline=True, minlen=1), True),
102
+ "content_hash": (_hex64, True),
103
+ "size": (_int(0, 2**40), True),
104
+ "note": (_s(512), False),
105
+ },
106
+ }
107
+
108
+
109
+ def _validate(p: dict, spec: dict, require_any: tuple[str, ...] = ()) -> dict:
110
+ if not isinstance(p, dict):
111
+ raise SchemaError("payload must be an object")
112
+ unknown = set(p) - set(spec)
113
+ if unknown:
114
+ raise SchemaError(f"unknown fields: {sorted(unknown)}")
115
+ out: dict[str, Any] = {}
116
+ for name, (check, required) in spec.items():
117
+ if name not in p:
118
+ if required:
119
+ raise SchemaError(f"missing field: {name}")
120
+ continue
121
+ try:
122
+ out[name] = check(p[name])
123
+ except SchemaError as e:
124
+ raise SchemaError(f"{name}: {e}") from e
125
+ if require_any and not any(k in out for k in require_any):
126
+ raise SchemaError(f"one of {list(require_any)} is required")
127
+ return out
128
+
129
+
130
+ def validate_payload(type_: str, payload: dict) -> dict:
131
+ spec = SCHEMAS.get(type_)
132
+ if spec is None:
133
+ raise SchemaError(f"unknown entry type: {type_}")
134
+ return _validate(payload, spec)
135
+
136
+
137
+ def text_fields(type_: str, payload: dict) -> list[tuple[str, str]]:
138
+ """Free-text fields an LLM could later read; used by the injection scanner."""
139
+ names = {
140
+ "claim": ("subject", "predicate", "object", "text"),
141
+ "attest": ("note",),
142
+ "observation": ("source", "note"),
143
+ "identity": ("name",),
144
+ "retract": ("reason",),
145
+ "rollback": ("reason",),
146
+ "revoke": ("reason",),
147
+ }.get(type_, ())
148
+ return [(n, payload[n]) for n in names if isinstance(payload.get(n), str)]