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.
- present_day/__init__.py +4 -0
- present_day/agent.py +58 -0
- present_day/backends.py +87 -0
- present_day/canon.py +52 -0
- present_day/claims.py +148 -0
- present_day/cli.py +1045 -0
- present_day/client.py +122 -0
- present_day/cloud/__init__.py +1 -0
- present_day/cloud/app.py +1041 -0
- present_day/cloud/backup.py +199 -0
- present_day/cloud/db.py +168 -0
- present_day/cloud/mail.py +53 -0
- present_day/cloud/metrics.py +112 -0
- present_day/cloud/migrate.py +90 -0
- present_day/cloud/oidc.py +140 -0
- present_day/cloud/pgdb.py +117 -0
- present_day/cloud/plans.py +21 -0
- present_day/cloud/ratelimit.py +66 -0
- present_day/cloud/remote.py +220 -0
- present_day/cloud/rotate.py +64 -0
- present_day/cloud/saml.py +87 -0
- present_day/cloud/security.py +162 -0
- present_day/cloud/service.py +1858 -0
- present_day/cloud/settings.py +159 -0
- present_day/cloud/stripe.py +67 -0
- present_day/cloud/web/__init__.py +0 -0
- present_day/cloud/web/app.html +22 -0
- present_day/cloud/web/cabinet.css +136 -0
- present_day/cloud/web/cabinet.js +868 -0
- present_day/cloud/web/login.html +54 -0
- present_day/cloud/web/login.js +111 -0
- present_day/crypto.py +105 -0
- present_day/engine.py +328 -0
- present_day/evalkit.py +156 -0
- present_day/evidence.py +284 -0
- present_day/export.py +118 -0
- present_day/gateway.py +496 -0
- present_day/home.py +92 -0
- present_day/injection.py +355 -0
- present_day/integrations/__init__.py +119 -0
- present_day/integrations/langchain.py +19 -0
- present_day/integrations/llamaindex.py +19 -0
- present_day/integrations/openai_agents.py +20 -0
- present_day/ledger.py +370 -0
- present_day/llm.py +224 -0
- present_day/mcp_server.py +143 -0
- present_day/merkle.py +168 -0
- present_day/node.py +278 -0
- present_day/recorder.py +151 -0
- present_day/redteam.py +220 -0
- present_day/rules.py +256 -0
- present_day/soc.py +528 -0
- present_day/soc_demo.py +120 -0
- present_day/stores.py +310 -0
- present_day/sync.py +70 -0
- present_day/trust.py +379 -0
- present_day/watch.py +386 -0
- present_day/web/__init__.py +0 -0
- present_day/web/dashboard.css +22 -0
- present_day/web/dashboard.js +107 -0
- present_day/web/index.html +34 -0
- present_day-0.14.0.dist-info/METADATA +52 -0
- present_day-0.14.0.dist-info/RECORD +67 -0
- present_day-0.14.0.dist-info/WHEEL +5 -0
- present_day-0.14.0.dist-info/entry_points.txt +3 -0
- present_day-0.14.0.dist-info/licenses/LICENSE +177 -0
- present_day-0.14.0.dist-info/top_level.txt +1 -0
present_day/__init__.py
ADDED
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())
|
present_day/backends.py
ADDED
|
@@ -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)]
|