swiftapi-python 1.2.2__py3-none-any.whl → 1.3.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.
- swiftapi/__init__.py +75 -96
- swiftapi/__main__.py +2 -0
- swiftapi/anthropic.py +236 -222
- swiftapi/brake/__init__.py +45 -0
- swiftapi/brake/benchmarks.py +111 -0
- swiftapi/brake/broker.py +274 -0
- swiftapi/brake/core.py +420 -0
- swiftapi/brake/install.py +268 -0
- swiftapi/brake/mcp.py +160 -0
- swiftapi/brake/observations.py +61 -0
- swiftapi/brake/qualification.py +116 -0
- swiftapi/brake/remote.py +90 -0
- swiftapi/cli.py +150 -0
- swiftapi/client.py +328 -309
- swiftapi/enforcement.py +281 -280
- swiftapi/openai.py +243 -225
- swiftapi/py.typed +0 -0
- swiftapi/verifier.py +182 -130
- swiftapi_python-1.3.0.dist-info/METADATA +147 -0
- swiftapi_python-1.3.0.dist-info/RECORD +26 -0
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.0.dist-info}/WHEEL +1 -1
- swiftapi_python-1.3.0.dist-info/entry_points.txt +2 -0
- swiftapi_python-1.2.2.dist-info/METADATA +0 -255
- swiftapi_python-1.2.2.dist-info/RECORD +0 -13
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {swiftapi_python-1.2.2.dist-info → swiftapi_python-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Local consequence enforcement; no model or cloud account required."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
ActionEnvelope,
|
|
5
|
+
BindingCondition,
|
|
6
|
+
ContinuationAuthority,
|
|
7
|
+
Decision,
|
|
8
|
+
DecisionReceipt,
|
|
9
|
+
LocalAuthority,
|
|
10
|
+
LocalGuard,
|
|
11
|
+
ReplayLedger,
|
|
12
|
+
AuthorizationDenied,
|
|
13
|
+
ReplayError,
|
|
14
|
+
InvalidEnvelope,
|
|
15
|
+
BrakeError,
|
|
16
|
+
canonical,
|
|
17
|
+
digest,
|
|
18
|
+
verify_receipt,
|
|
19
|
+
verify_decision,
|
|
20
|
+
)
|
|
21
|
+
from .broker import CapabilityBroker
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ActionEnvelope",
|
|
25
|
+
"BindingCondition",
|
|
26
|
+
"ContinuationAuthority",
|
|
27
|
+
"Decision",
|
|
28
|
+
"DecisionReceipt",
|
|
29
|
+
"LocalAuthority",
|
|
30
|
+
"LocalGuard",
|
|
31
|
+
"ReplayLedger",
|
|
32
|
+
"AuthorizationDenied",
|
|
33
|
+
"ReplayError",
|
|
34
|
+
"InvalidEnvelope",
|
|
35
|
+
"BrakeError",
|
|
36
|
+
"canonical",
|
|
37
|
+
"digest",
|
|
38
|
+
"verify_receipt",
|
|
39
|
+
"verify_decision",
|
|
40
|
+
"CapabilityBroker",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
from .remote import RemoteAuthority
|
|
44
|
+
|
|
45
|
+
__all__ += ["RemoteAuthority"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Import externally judged defensive evaluations without copying their corpora.
|
|
2
|
+
|
|
3
|
+
Rows must carry immutable framework/corpus identities and explicit executor
|
|
4
|
+
observations. This adapter does not infer safety, authority, or escape from text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable
|
|
11
|
+
from .core import InvalidEnvelope, digest, loads, write_exclusive
|
|
12
|
+
from .observations import CLASSES
|
|
13
|
+
|
|
14
|
+
FRAMEWORKS = {"JailbreakBench", "HarmBench", "StrongREJECT"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def summarize(rows: Iterable[dict], expected_ids: Iterable[str]) -> dict:
|
|
18
|
+
ids = list(expected_ids)
|
|
19
|
+
if not ids or len(ids) != len(set(ids)):
|
|
20
|
+
raise InvalidEnvelope("Expected unique nonempty benchmark case IDs")
|
|
21
|
+
results = list(rows)
|
|
22
|
+
seen = []
|
|
23
|
+
counts: Counter = Counter()
|
|
24
|
+
failures = []
|
|
25
|
+
for row in results:
|
|
26
|
+
required = {
|
|
27
|
+
"case_id",
|
|
28
|
+
"framework",
|
|
29
|
+
"revision",
|
|
30
|
+
"corpus_sha256",
|
|
31
|
+
"classification",
|
|
32
|
+
"authorized",
|
|
33
|
+
"consequence_observed",
|
|
34
|
+
"evidence_sha256",
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
not required <= set(row)
|
|
38
|
+
or row["framework"] not in FRAMEWORKS
|
|
39
|
+
or row["classification"] not in CLASSES
|
|
40
|
+
):
|
|
41
|
+
raise InvalidEnvelope("Missing framework, evidence or outcome fields")
|
|
42
|
+
for field, size in (("revision", 40), ("corpus_sha256", 64), ("evidence_sha256", 64)):
|
|
43
|
+
v = row[field]
|
|
44
|
+
if type(v) is not str or len(v) != size or any(c not in "0123456789abcdef" for c in v):
|
|
45
|
+
raise InvalidEnvelope("Immutable source and evidence hashes required")
|
|
46
|
+
if type(row["authorized"]) is not bool or type(row["consequence_observed"]) is not bool:
|
|
47
|
+
raise InvalidEnvelope("Explicit independent execution observations required")
|
|
48
|
+
seen.append(row["case_id"])
|
|
49
|
+
counts[row["classification"]] += 1
|
|
50
|
+
if row["consequence_observed"] and not row["authorized"]:
|
|
51
|
+
failures.append(row)
|
|
52
|
+
if len(seen) != len(set(seen)) or set(seen) != set(ids):
|
|
53
|
+
raise InvalidEnvelope("Missing, duplicate, or unexpected cases; denominator cannot change")
|
|
54
|
+
return {
|
|
55
|
+
"schema": "swiftapi.benchmark/v1",
|
|
56
|
+
"total": len(ids),
|
|
57
|
+
"counts": dict(counts),
|
|
58
|
+
"unauthorized_consequential_escapes": len(failures),
|
|
59
|
+
"status": "FAIL" if failures or counts["ERROR"] else "PASS",
|
|
60
|
+
"failures": failures,
|
|
61
|
+
"rows_sha256": digest(results),
|
|
62
|
+
"results": results,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def import_results(source: Path, output: Path, expected_ids: list) -> dict:
|
|
67
|
+
"""Preserve all supplied rows. Output must be new; failures remain regressions."""
|
|
68
|
+
rows = [loads(line) for line in source.read_bytes().splitlines() if line.strip()]
|
|
69
|
+
report = summarize(rows, expected_ids)
|
|
70
|
+
write_exclusive(output, report)
|
|
71
|
+
return report
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_cases(framework: str, corpus: Path, expected_sha256: str) -> list:
|
|
75
|
+
"""Read original upstream CSV bytes after pin verification; never rewrite corpora."""
|
|
76
|
+
import csv
|
|
77
|
+
import hashlib
|
|
78
|
+
import io
|
|
79
|
+
|
|
80
|
+
if framework not in FRAMEWORKS:
|
|
81
|
+
raise InvalidEnvelope("Unknown framework")
|
|
82
|
+
raw = corpus.read_bytes()
|
|
83
|
+
if hashlib.sha256(raw).hexdigest() != expected_sha256:
|
|
84
|
+
raise InvalidEnvelope("Corpus hash mismatch")
|
|
85
|
+
rows = list(csv.DictReader(io.StringIO(raw.decode("utf-8-sig"))))
|
|
86
|
+
mappings = {
|
|
87
|
+
"JailbreakBench": ("Goal", "Behavior", "Category"),
|
|
88
|
+
"HarmBench": ("Behavior", "BehaviorID", "SemanticCategory"),
|
|
89
|
+
"StrongREJECT": ("forbidden_prompt", None, "category"),
|
|
90
|
+
}
|
|
91
|
+
prompt, identifier, category = mappings[framework]
|
|
92
|
+
result = []
|
|
93
|
+
for index, row in enumerate(rows):
|
|
94
|
+
if prompt not in row or category not in row or (identifier and identifier not in row):
|
|
95
|
+
raise InvalidEnvelope("Unexpected upstream corpus columns")
|
|
96
|
+
result.append(
|
|
97
|
+
{
|
|
98
|
+
"case_id": framework
|
|
99
|
+
+ ":"
|
|
100
|
+
+ corpus.stem
|
|
101
|
+
+ ":"
|
|
102
|
+
+ (row[identifier] if identifier else str(index)),
|
|
103
|
+
"prompt": row[prompt],
|
|
104
|
+
"context": row.get("ContextString", ""),
|
|
105
|
+
"category": row[category],
|
|
106
|
+
"corpus_sha256": expected_sha256,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
if len({r["case_id"] for r in result}) != len(result):
|
|
110
|
+
raise InvalidEnvelope("Duplicate corpus IDs")
|
|
111
|
+
return result
|
swiftapi/brake/broker.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""A narrow credential-owning capability broker, independent of host hooks.
|
|
2
|
+
|
|
3
|
+
The bundled capability creates a NEW UTF-8 marker inside one configured sandbox.
|
|
4
|
+
No arbitrary shell execution, overwrite, path traversal, or model calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from .core import (
|
|
13
|
+
ActionEnvelope,
|
|
14
|
+
AuthorizationDenied,
|
|
15
|
+
BindingCondition,
|
|
16
|
+
Decision,
|
|
17
|
+
InvalidEnvelope,
|
|
18
|
+
LocalAuthority,
|
|
19
|
+
ReplayLedger,
|
|
20
|
+
digest,
|
|
21
|
+
loads,
|
|
22
|
+
secure_dir,
|
|
23
|
+
verify_decision,
|
|
24
|
+
verify_receipt,
|
|
25
|
+
write_exclusive,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CapabilityBroker:
|
|
30
|
+
def __init__(self, root: Optional[Path] = None):
|
|
31
|
+
self.root = secure_dir(
|
|
32
|
+
Path(root or os.environ.get("SWIFTAPI_HOME", str(Path.home() / ".local/share/swiftapi")))
|
|
33
|
+
)
|
|
34
|
+
self.sandbox = secure_dir(self.root / "sandbox")
|
|
35
|
+
self.pending = secure_dir(self.root / "pending")
|
|
36
|
+
self.grants = secure_dir(self.root / "grants")
|
|
37
|
+
self.receipts = secure_dir(self.root / "receipts")
|
|
38
|
+
self.authority = LocalAuthority.load(self.root)
|
|
39
|
+
self.ledger = ReplayLedger(self.root)
|
|
40
|
+
key = self.root / "public-key.json"
|
|
41
|
+
if not key.exists():
|
|
42
|
+
try:
|
|
43
|
+
write_exclusive(key, {"public_key": self.authority.public_key})
|
|
44
|
+
except FileExistsError:
|
|
45
|
+
pass
|
|
46
|
+
if key.is_symlink() or loads(key.read_bytes()) != {"public_key": self.authority.public_key}:
|
|
47
|
+
raise InvalidEnvelope("Trust key changed")
|
|
48
|
+
|
|
49
|
+
def status(self) -> dict:
|
|
50
|
+
return {
|
|
51
|
+
"version": "1.3.0",
|
|
52
|
+
"scope": "write_marker in configured sandbox only",
|
|
53
|
+
"sandbox": str(self.sandbox),
|
|
54
|
+
"public_key_file": str(self.root / "public-key.json"),
|
|
55
|
+
"public_key": self.authority.public_key,
|
|
56
|
+
"pending": sorted(p.stem for p in self.pending.glob("*.json")),
|
|
57
|
+
"receipt_count": len(list(self.receipts.glob("*.json"))),
|
|
58
|
+
"native_eos_claim": False,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _action(self, name: str, content: str) -> ActionEnvelope:
|
|
62
|
+
if type(name) is not str or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,99}", name):
|
|
63
|
+
raise InvalidEnvelope(
|
|
64
|
+
"Marker name must be a basename of 1..100 ASCII letters, digits, _, ., -"
|
|
65
|
+
)
|
|
66
|
+
if type(content) is not str or len(content.encode("utf-8")) > 4096:
|
|
67
|
+
raise InvalidEnvelope("Marker content must be UTF-8 text of at most 4096 bytes")
|
|
68
|
+
return ActionEnvelope(
|
|
69
|
+
"write_marker",
|
|
70
|
+
{"sandbox": str(self.sandbox), "name": name, "content": content},
|
|
71
|
+
"Create the exact marker once",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _state(self, action: ActionEnvelope) -> dict:
|
|
75
|
+
p = action.to_dict()["params"]
|
|
76
|
+
if self._action(p["name"], p["content"]).to_dict() != action.to_dict():
|
|
77
|
+
raise InvalidEnvelope("Unregistered capability or sandbox")
|
|
78
|
+
if self.sandbox.is_symlink() or self.sandbox.resolve() != self.sandbox:
|
|
79
|
+
raise InvalidEnvelope("Sandbox alias rejected")
|
|
80
|
+
s = self.sandbox.stat()
|
|
81
|
+
target = self.sandbox / p["name"]
|
|
82
|
+
return {
|
|
83
|
+
"sandbox": str(self.sandbox),
|
|
84
|
+
"device": s.st_dev,
|
|
85
|
+
"inode": s.st_ino,
|
|
86
|
+
"target_absent": not os.path.lexists(str(target)),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def propose(self, name: str, content: str) -> dict:
|
|
90
|
+
action = self._action(name, content)
|
|
91
|
+
state = self._state(action)
|
|
92
|
+
if state["target_absent"] is not True:
|
|
93
|
+
raise AuthorizationDenied("Existing targets cannot be overwritten")
|
|
94
|
+
condition = BindingCondition(action.action_hash, state, "operator-authorized-new-marker")
|
|
95
|
+
obj = {"action": action.to_dict(), "condition": condition.to_dict()}
|
|
96
|
+
request_id = digest(obj)
|
|
97
|
+
path = self.pending / (request_id + ".json")
|
|
98
|
+
if not path.exists():
|
|
99
|
+
try:
|
|
100
|
+
write_exclusive(path, obj)
|
|
101
|
+
except FileExistsError:
|
|
102
|
+
pass
|
|
103
|
+
return {
|
|
104
|
+
"request_id": request_id,
|
|
105
|
+
**obj,
|
|
106
|
+
"authorized": (self.grants / (request_id + ".json")).exists(),
|
|
107
|
+
"next": "Operator: swiftapi brake authorize " + request_id,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def request(self, request_id: str) -> tuple:
|
|
111
|
+
if type(request_id) is not str or not re.fullmatch("[a-f0-9]{64}", request_id):
|
|
112
|
+
raise InvalidEnvelope("Invalid request ID")
|
|
113
|
+
path = self.pending / (request_id + ".json")
|
|
114
|
+
if path.is_symlink():
|
|
115
|
+
raise InvalidEnvelope("Request alias rejected")
|
|
116
|
+
obj = loads(path.read_bytes())
|
|
117
|
+
if digest(obj) != request_id:
|
|
118
|
+
raise AuthorizationDenied("Request bytes changed")
|
|
119
|
+
action = ActionEnvelope(**obj["action"])
|
|
120
|
+
condition = BindingCondition(**obj["condition"])
|
|
121
|
+
self._state(action)
|
|
122
|
+
return action, condition
|
|
123
|
+
|
|
124
|
+
def authorize(self, request_id: str, ttl: int = 300) -> dict:
|
|
125
|
+
"""Operator/application entry point. Deliberately absent from MCP tools."""
|
|
126
|
+
action, condition = self.request(request_id)
|
|
127
|
+
state = self._state(action)
|
|
128
|
+
decision = self.authority.authorize(
|
|
129
|
+
condition=condition, action=action, state=state, ttl=ttl
|
|
130
|
+
)
|
|
131
|
+
verify_decision(decision, self.authority.public_key, action, condition, state)
|
|
132
|
+
# Never replace a consumed/expired grant. A request has at most one grant.
|
|
133
|
+
write_exclusive(self.grants / (request_id + ".json"), decision.to_dict())
|
|
134
|
+
return {
|
|
135
|
+
"request_id": request_id,
|
|
136
|
+
"decision": decision.to_dict(),
|
|
137
|
+
"classification": "VISIBLE_SAFE",
|
|
138
|
+
"executed": False,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def _receipt(
|
|
142
|
+
self,
|
|
143
|
+
classification: str,
|
|
144
|
+
request_id: str,
|
|
145
|
+
action: Optional[ActionEnvelope],
|
|
146
|
+
decision: Optional[Decision],
|
|
147
|
+
phase: str,
|
|
148
|
+
reason: str,
|
|
149
|
+
executed: bool,
|
|
150
|
+
) -> dict:
|
|
151
|
+
import secrets
|
|
152
|
+
import time
|
|
153
|
+
|
|
154
|
+
payload: dict = {
|
|
155
|
+
"schema": "swiftapi.execution/v1",
|
|
156
|
+
"issuer": self.authority.public_key,
|
|
157
|
+
"receipt_id": secrets.token_hex(32),
|
|
158
|
+
"created_at": int(time.time()),
|
|
159
|
+
"classification": classification,
|
|
160
|
+
"phase": phase,
|
|
161
|
+
"request_id": request_id,
|
|
162
|
+
"action_hash": action.action_hash if action else None,
|
|
163
|
+
"decision_hash": digest(decision.to_dict()) if decision else None,
|
|
164
|
+
"executed": executed,
|
|
165
|
+
"reason": reason,
|
|
166
|
+
}
|
|
167
|
+
receipt = self.authority.sign(payload)
|
|
168
|
+
path = self.receipts / (payload["receipt_id"] + ".json")
|
|
169
|
+
write_exclusive(path, receipt.to_dict())
|
|
170
|
+
return {
|
|
171
|
+
"classification": classification,
|
|
172
|
+
"executed": executed,
|
|
173
|
+
"receipt_path": str(path),
|
|
174
|
+
"receipt": receipt.to_dict(),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def execute(self, request_id: str) -> dict:
|
|
178
|
+
action = decision = None
|
|
179
|
+
try:
|
|
180
|
+
action, condition = self.request(request_id)
|
|
181
|
+
grant_path = self.grants / (request_id + ".json")
|
|
182
|
+
if grant_path.is_symlink():
|
|
183
|
+
raise AuthorizationDenied("Grant alias rejected")
|
|
184
|
+
decision = Decision.from_dict(loads(grant_path.read_bytes()))
|
|
185
|
+
state = self._state(action)
|
|
186
|
+
p = verify_decision(decision, self.authority.public_key, action, condition, state)
|
|
187
|
+
self.ledger.claim(p["issuer"], p["nonce"])
|
|
188
|
+
except (AuthorizationDenied, InvalidEnvelope, FileNotFoundError) as exc:
|
|
189
|
+
return self._receipt(
|
|
190
|
+
"GATE_BLOCK",
|
|
191
|
+
request_id,
|
|
192
|
+
action,
|
|
193
|
+
decision,
|
|
194
|
+
"authorization",
|
|
195
|
+
type(exc).__name__,
|
|
196
|
+
False,
|
|
197
|
+
)
|
|
198
|
+
# Persist authorization intent before executing. Any persistence failure aborts.
|
|
199
|
+
self._receipt(
|
|
200
|
+
"VISIBLE_SAFE",
|
|
201
|
+
request_id,
|
|
202
|
+
action,
|
|
203
|
+
decision,
|
|
204
|
+
"authorized_intent",
|
|
205
|
+
"verified; execution has not started",
|
|
206
|
+
False,
|
|
207
|
+
)
|
|
208
|
+
params = action.to_dict()["params"]
|
|
209
|
+
fd = None
|
|
210
|
+
try:
|
|
211
|
+
if self._state(action) != state:
|
|
212
|
+
raise AuthorizationDenied("State changed at execution boundary")
|
|
213
|
+
directory = os.open(str(self.sandbox), os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
214
|
+
try:
|
|
215
|
+
st = os.fstat(directory)
|
|
216
|
+
if (st.st_dev, st.st_ino) != (state["device"], state["inode"]):
|
|
217
|
+
raise AuthorizationDenied("Sandbox identity changed")
|
|
218
|
+
fd = os.open(
|
|
219
|
+
params["name"],
|
|
220
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
221
|
+
0o600,
|
|
222
|
+
dir_fd=directory,
|
|
223
|
+
)
|
|
224
|
+
with os.fdopen(fd, "wb") as stream:
|
|
225
|
+
fd = None
|
|
226
|
+
stream.write(params["content"].encode("utf-8"))
|
|
227
|
+
stream.flush()
|
|
228
|
+
os.fsync(stream.fileno())
|
|
229
|
+
os.fsync(directory)
|
|
230
|
+
finally:
|
|
231
|
+
os.close(directory)
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
if fd is not None:
|
|
234
|
+
os.close(fd)
|
|
235
|
+
# May contain a partial write; never retry or claim no consequence.
|
|
236
|
+
return self._receipt(
|
|
237
|
+
"ERROR",
|
|
238
|
+
request_id,
|
|
239
|
+
action,
|
|
240
|
+
decision,
|
|
241
|
+
"execution",
|
|
242
|
+
type(exc).__name__ + "; inspect sandbox; do not retry",
|
|
243
|
+
False,
|
|
244
|
+
)
|
|
245
|
+
return self._receipt(
|
|
246
|
+
"AUTHORIZED_EXECUTION",
|
|
247
|
+
request_id,
|
|
248
|
+
action,
|
|
249
|
+
decision,
|
|
250
|
+
"execution",
|
|
251
|
+
"exact new marker written",
|
|
252
|
+
True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def read_marker(self, name: str) -> dict:
|
|
256
|
+
action = self._action(name, "")
|
|
257
|
+
self._state(action)
|
|
258
|
+
directory = os.open(str(self.sandbox), os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
259
|
+
try:
|
|
260
|
+
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
|
|
261
|
+
with os.fdopen(fd, "rb") as f:
|
|
262
|
+
import stat
|
|
263
|
+
|
|
264
|
+
if not stat.S_ISREG(os.fstat(f.fileno()).st_mode):
|
|
265
|
+
raise InvalidEnvelope("Not a regular marker")
|
|
266
|
+
data = f.read(4097)
|
|
267
|
+
finally:
|
|
268
|
+
os.close(directory)
|
|
269
|
+
if len(data) > 4096:
|
|
270
|
+
raise InvalidEnvelope("Marker exceeds limit")
|
|
271
|
+
return {"classification": "VISIBLE_SAFE", "name": name, "content": data.decode("utf-8")}
|
|
272
|
+
|
|
273
|
+
def verify(self, path: Path) -> dict:
|
|
274
|
+
return verify_receipt(loads(path.read_bytes()), self.authority.public_key)
|