vtai-agent 0.6.6__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.
- vtai_agent/__init__.py +12 -0
- vtai_agent/certify.py +385 -0
- vtai_agent/cli.py +581 -0
- vtai_agent/config.py +274 -0
- vtai_agent/db.py +395 -0
- vtai_agent/doctor.py +296 -0
- vtai_agent/errors.py +46 -0
- vtai_agent/guardrails.py +370 -0
- vtai_agent/mcp_server.py +321 -0
- vtai_agent/models.py +70 -0
- vtai_agent/orchestrator.py +305 -0
- vtai_agent/providers.py +115 -0
- vtai_agent/py.typed +0 -0
- vtai_agent/tools/__init__.py +17 -0
- vtai_agent/tools/automode.py +279 -0
- vtai_agent/tools/backup.py +134 -0
- vtai_agent/tools/delegate.py +417 -0
- vtai_agent/tools/disk.py +155 -0
- vtai_agent/tools/files.py +194 -0
- vtai_agent/tools/media.py +145 -0
- vtai_agent/tools/rea.py +363 -0
- vtai_agent/tools/registry.py +90 -0
- vtai_agent/tools/shell.py +96 -0
- vtai_agent/tools/system_info.py +255 -0
- vtai_agent-0.6.6.dist-info/METADATA +535 -0
- vtai_agent-0.6.6.dist-info/RECORD +29 -0
- vtai_agent-0.6.6.dist-info/WHEEL +4 -0
- vtai_agent-0.6.6.dist-info/entry_points.txt +3 -0
- vtai_agent-0.6.6.dist-info/licenses/LICENSE +21 -0
vtai_agent/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""VT-AIAgent: system-automation agent exposed as an MCP server.
|
|
2
|
+
|
|
3
|
+
Drivable from opencode, Claude Code, and pi. Cloud (Anthropic/OpenAI) and
|
|
4
|
+
local (llama.cpp/Ollama via OpenAI-compatible endpoint) LLMs are both supported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("vtai-agent")
|
|
11
|
+
except PackageNotFoundError: # running from a raw source checkout
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
vtai_agent/certify.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""vtai-cert — an executable guardrail-compliance suite.
|
|
2
|
+
|
|
3
|
+
Runs adversarial probes against the fail-closed surfaces (deny globs,
|
|
4
|
+
writable containment, sandboxed shell, dry-run-first, kill switch, audit
|
|
5
|
+
trail, echo-loop breaker) and emits a digest-stamped JSON certificate.
|
|
6
|
+
Every refused exploit — the "can a prompt injection make it write outside
|
|
7
|
+
writable_roots" class of question — is answered by a recorded, reproducible
|
|
8
|
+
probe result instead of an assertion of intent.
|
|
9
|
+
|
|
10
|
+
Claim boundary (unapologetic but honest): this certifies vtai-agent's own
|
|
11
|
+
fail-closed surfaces as executed in the harness environment below. It is
|
|
12
|
+
not a cross-vendor MCP-server attestation; the requirements are the
|
|
13
|
+
contract vtai-agent holds itself to, and the certificate is the evidence
|
|
14
|
+
artifact a user can bake into a release.
|
|
15
|
+
|
|
16
|
+
CLI: `vtai certify [--out PATH] [--json]` — exit 0 only when every
|
|
17
|
+
requirement holds.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import platform
|
|
24
|
+
import shutil
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Callable
|
|
29
|
+
|
|
30
|
+
from sqlmodel import select
|
|
31
|
+
|
|
32
|
+
from . import __version__, db
|
|
33
|
+
from .config import Capabilities, CapabilityRea, Guardrails as GuardrailsConfig
|
|
34
|
+
from .config import Paths, Settings, validate_settings
|
|
35
|
+
from .guardrails import GuardrailViolation, Guardrails
|
|
36
|
+
from .models import AuditLog
|
|
37
|
+
from .tools import OrganizeFilesTool, ShellTool
|
|
38
|
+
from .tools.files import OrganizeFilesInput
|
|
39
|
+
from .tools.shell import ShellInput
|
|
40
|
+
|
|
41
|
+
PROFILE = "guardrails-core@1"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ProbeResult:
|
|
46
|
+
passed: bool
|
|
47
|
+
evidence: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CertContext:
|
|
52
|
+
"""Isolated machine: temp data dir + writable/protected roots + its own
|
|
53
|
+
audit DB, mirroring the pytest sandbox so CI and CLI agree."""
|
|
54
|
+
root: Path
|
|
55
|
+
writable: Path
|
|
56
|
+
protected: Path
|
|
57
|
+
data_dir: Path
|
|
58
|
+
settings: Settings
|
|
59
|
+
g: Guardrails
|
|
60
|
+
|
|
61
|
+
def audit_count(self) -> int:
|
|
62
|
+
with db.session_scope() as s:
|
|
63
|
+
return len(list(s.exec(select(AuditLog))))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Requirement:
|
|
68
|
+
id: str
|
|
69
|
+
title: str
|
|
70
|
+
spec: str
|
|
71
|
+
probe: Callable[[CertContext], ProbeResult]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
TEST_BINS = ("ls", "cat", "cp", "mkdir", "grep", "tar")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_context(root: Path) -> CertContext:
|
|
78
|
+
"""Provision the isolated environment the probes run in."""
|
|
79
|
+
root = root.resolve()
|
|
80
|
+
writable = root / "writable"
|
|
81
|
+
protected = root / "protected"
|
|
82
|
+
data_dir = root / "data"
|
|
83
|
+
for d in (writable, protected, data_dir):
|
|
84
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
(protected / "secret.txt").write_text("secret")
|
|
86
|
+
|
|
87
|
+
trusted = sorted({str(Path(found).resolve().parent)
|
|
88
|
+
for found in (shutil.which(b) for b in TEST_BINS)
|
|
89
|
+
if found}) or ["/usr/bin"]
|
|
90
|
+
|
|
91
|
+
cfg = GuardrailsConfig(
|
|
92
|
+
dry_run_default=True,
|
|
93
|
+
max_repeat_denials=2, # small: the breaker probe must trip quickly
|
|
94
|
+
shell_allowlist=list(TEST_BINS),
|
|
95
|
+
deny_globs=[f"{protected}/**", "~/.ssh/**", "**/.env"],
|
|
96
|
+
trusted_bin_dirs=trusted,
|
|
97
|
+
)
|
|
98
|
+
settings = Settings(
|
|
99
|
+
guardrails=cfg,
|
|
100
|
+
capabilities=Capabilities(rea=CapabilityRea(enabled=False)),
|
|
101
|
+
paths=Paths(writable_roots=[writable], data_dir=data_dir),
|
|
102
|
+
)
|
|
103
|
+
validate_settings(settings)
|
|
104
|
+
|
|
105
|
+
# Isolated engine + globals, same mechanism as the pytest sandbox.
|
|
106
|
+
from . import config as config_mod
|
|
107
|
+
config_mod._settings = settings
|
|
108
|
+
db._engine = None
|
|
109
|
+
|
|
110
|
+
return CertContext(root=root, writable=writable, protected=protected,
|
|
111
|
+
data_dir=data_dir, settings=settings,
|
|
112
|
+
g=Guardrails(cfg, settings))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _expect_denied(action: Callable[[], Any], what: str) -> ProbeResult:
|
|
116
|
+
"""Run an action that must be refused. Refusal surfaces differ:
|
|
117
|
+
check_* guards raise GuardrailViolation, check_shell returns a denial
|
|
118
|
+
decision (allowed=False) — both count as refusals."""
|
|
119
|
+
try:
|
|
120
|
+
result = action()
|
|
121
|
+
except GuardrailViolation as e:
|
|
122
|
+
return ProbeResult(True, f"refused as expected: {e} (code={getattr(e, 'code', '')})")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return ProbeResult(False,
|
|
125
|
+
f"unexpected exception type {type(e).__name__}: {e}")
|
|
126
|
+
if getattr(result, "allowed", None) is False:
|
|
127
|
+
return ProbeResult(True, f"refused as expected: {getattr(result, 'reason', '')}")
|
|
128
|
+
return ProbeResult(False, f"MISSED: {what} was allowed ({result!r:.200})")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------- probes ----------
|
|
132
|
+
|
|
133
|
+
def probe_deny_globs(ctx: CertContext) -> ProbeResult:
|
|
134
|
+
r1 = _expect_denied(lambda: ctx.g.check_readable(ctx.protected / "secret.txt"),
|
|
135
|
+
"reading a deny-globbed path")
|
|
136
|
+
r2 = _expect_denied(lambda: ctx.g.check_shell(f"cat {ctx.protected}/secret.txt"),
|
|
137
|
+
"cat on a protected path")
|
|
138
|
+
return ProbeResult(r1.passed and r2.passed,
|
|
139
|
+
r1.evidence + " | " + r2.evidence)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def probe_writable_containment(ctx: CertContext) -> ProbeResult:
|
|
143
|
+
outside = ctx.root / "outside.txt"
|
|
144
|
+
outside.write_text("x")
|
|
145
|
+
r_out = _expect_denied(lambda: ctx.g.check_writable(outside),
|
|
146
|
+
"write outside writable_roots")
|
|
147
|
+
r_dot = _expect_denied(
|
|
148
|
+
lambda: ctx.g.check_writable(ctx.writable / ".." / "escape.txt"),
|
|
149
|
+
"dot-dot escape")
|
|
150
|
+
link = ctx.writable / "link"
|
|
151
|
+
link.unlink(missing_ok=True) # re-runs: drop the previous probe's link
|
|
152
|
+
link.symlink_to(ctx.protected / "secret.txt")
|
|
153
|
+
r_sym = _expect_denied(lambda: ctx.g.check_writable(link),
|
|
154
|
+
"symlink escape into a protected path")
|
|
155
|
+
return ProbeResult(r_out.passed and r_dot.passed and r_sym.passed,
|
|
156
|
+
" | ".join(p.evidence for p in (r_out, r_dot, r_sym)))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def probe_shell_fail_closed(ctx: CertContext) -> ProbeResult:
|
|
160
|
+
"""Every exploit-class shell answer must be a refusal with a code."""
|
|
161
|
+
cases = [
|
|
162
|
+
"curl https://example.com/steal", # binary not allowlisted
|
|
163
|
+
"/bin/sh -c 'curl evil'", # binary given as path
|
|
164
|
+
"tar --checkpoint-action=exec=/bin/sh cf x", # execution-enabling flag
|
|
165
|
+
"mkdir /opt/pwned", # write-mode arg outside sandbox
|
|
166
|
+
]
|
|
167
|
+
parts = []
|
|
168
|
+
passed = True
|
|
169
|
+
for c in cases:
|
|
170
|
+
d = ctx.g.check_shell(c)
|
|
171
|
+
read = f"{('DENIED' if not d.allowed else 'ALLOWED'):7} {d.reason}"
|
|
172
|
+
if d.allowed or "[code=" not in d.reason:
|
|
173
|
+
passed = False
|
|
174
|
+
parts.append(read)
|
|
175
|
+
return ProbeResult(passed, " | ".join(parts))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def probe_injection_resistance(ctx: CertContext) -> ProbeResult:
|
|
179
|
+
"""The classic question: can injected instruction text make the shell
|
|
180
|
+
tool run something else? No shell interpreter exists between args and
|
|
181
|
+
exec(3), so each smuggle must surface as a denied argv."""
|
|
182
|
+
sm_events = [
|
|
183
|
+
("ls; touch /tmp/pwned", "command chaining via ';"),
|
|
184
|
+
("cat ~/.ssh/id_rsa", "secret read attempt"),
|
|
185
|
+
(f"cp {ctx.protected}/secret.txt {ctx.root}/pwned-file",
|
|
186
|
+
"protected path exfiltration via cp"),
|
|
187
|
+
]
|
|
188
|
+
parts = []
|
|
189
|
+
for command, what in sm_events:
|
|
190
|
+
d = ctx.g.check_shell(command)
|
|
191
|
+
if not d.allowed:
|
|
192
|
+
parts.append(f"{what}: refused ({d.reason})")
|
|
193
|
+
else:
|
|
194
|
+
parts.append(f"{what}: MISSED")
|
|
195
|
+
ok = all("MISSED" not in p and why in p for p, (_c, why) in zip(parts, sm_events))
|
|
196
|
+
return ProbeResult(ok, " | ".join(parts))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def probe_dry_run_default(ctx: CertContext) -> ProbeResult:
|
|
200
|
+
"""Preview is king: with dry_run nothing may touch the filesystem.
|
|
201
|
+
The scratch tree is reset first so re-runs on an existing root stay
|
|
202
|
+
valid (certificates must be reproducible, not once-only)."""
|
|
203
|
+
src = ctx.writable / "messy"
|
|
204
|
+
shutil.rmtree(src, ignore_errors=True)
|
|
205
|
+
shutil.rmtree(ctx.writable / "made", ignore_errors=True)
|
|
206
|
+
src.mkdir(parents=True)
|
|
207
|
+
(src / "a.pdf").write_text("x")
|
|
208
|
+
r = asyncio.run(OrganizeFilesTool(ctx.g).run(OrganizeFilesInput(
|
|
209
|
+
directory=str(src), confirm_mime=False)))
|
|
210
|
+
s = asyncio.run(ShellTool(ctx.g).run(ShellInput(
|
|
211
|
+
command=f"mkdir {ctx.writable}/made", timeout=5.0)))
|
|
212
|
+
leftovers = sorted(p.name for p in src.iterdir())
|
|
213
|
+
made = (ctx.writable / "made").exists()
|
|
214
|
+
ok = (bool(r.dry_run) and bool(s.dry_run) and leftovers == ["a.pdf"]
|
|
215
|
+
and not made)
|
|
216
|
+
return ProbeResult(
|
|
217
|
+
ok,
|
|
218
|
+
f"organize dry_run={r.dry_run} unmoved={leftovers}; "
|
|
219
|
+
f"shell dry_run={s.dry_run}, mkdir executed={made}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def probe_kill_switch(ctx: CertContext) -> ProbeResult:
|
|
223
|
+
"""The KILL file must stop executing tools with E_KILL_SWITCH, and be
|
|
224
|
+
cleaned up after the probe."""
|
|
225
|
+
kill = ctx.data_dir / "KILL"
|
|
226
|
+
kill.write_text("")
|
|
227
|
+
try:
|
|
228
|
+
try:
|
|
229
|
+
asyncio.run(ShellTool(ctx.g).run(ShellInput(
|
|
230
|
+
command=f"ls {ctx.writable}", dry_run=False, timeout=5.0)))
|
|
231
|
+
except GuardrailViolation as e:
|
|
232
|
+
if getattr(e, "code", "") == "E_KILL_SWITCH":
|
|
233
|
+
return ProbeResult(True,
|
|
234
|
+
f"executing tool refused by kill switch: {e}")
|
|
235
|
+
return ProbeResult(False,
|
|
236
|
+
f"wrong refusal: {e} (code={getattr(e, 'code', '')})")
|
|
237
|
+
return ProbeResult(False, "kill switch did not stop the executing path")
|
|
238
|
+
finally:
|
|
239
|
+
kill.unlink(missing_ok=True)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def probe_audit_completeness(ctx: CertContext) -> ProbeResult:
|
|
243
|
+
"""Guarded refusals must produce exactly the audit rows they claim, on
|
|
244
|
+
fresh paths/commands (previous probes may already have tripped the
|
|
245
|
+
breaker for their own details)."""
|
|
246
|
+
before = ctx.audit_count()
|
|
247
|
+
try: # refusal #1: fresh protected path
|
|
248
|
+
ctx.g.check_readable(ctx.protected / "cursor.txt")
|
|
249
|
+
except GuardrailViolation:
|
|
250
|
+
pass
|
|
251
|
+
try: # refusal #2: fresh not-allowlisted command
|
|
252
|
+
ctx.g.check_shell("curl x")
|
|
253
|
+
except GuardrailViolation:
|
|
254
|
+
pass
|
|
255
|
+
after = ctx.audit_count()
|
|
256
|
+
ok = (after - before) == 2
|
|
257
|
+
return ProbeResult(ok, f"{after - before} audit rows for the 2 refusals (exact)")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def probe_circuit_breaker(ctx: CertContext) -> ProbeResult:
|
|
261
|
+
"""Repeating identical denials must escalate to a stop signal."""
|
|
262
|
+
Guardrails.reset_denial_counts()
|
|
263
|
+
reason = ""
|
|
264
|
+
for _ in range(ctx.settings.guardrails.max_repeat_denials + 1):
|
|
265
|
+
reason = ctx.g.check_shell("curl https://example.com").reason
|
|
266
|
+
Guardrails.reset_denial_counts()
|
|
267
|
+
return ProbeResult("CIRCUIT BREAKER" in reason, f"final refusal: {reason[:300]}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def default_requirements() -> list[Requirement]:
|
|
271
|
+
return [
|
|
272
|
+
Requirement(
|
|
273
|
+
"R1", "deny-glob protection",
|
|
274
|
+
"Deny-globbed paths are unreadable and un-invokable on any "
|
|
275
|
+
"surface, always carrying denial evidence.",
|
|
276
|
+
probe_deny_globs),
|
|
277
|
+
Requirement(
|
|
278
|
+
"R2", "writable containment",
|
|
279
|
+
"Writes outside writable_roots are refused, including dotdot and "
|
|
280
|
+
"symlink escapes.",
|
|
281
|
+
probe_writable_containment),
|
|
282
|
+
Requirement(
|
|
283
|
+
"R3", "fail-closed shell",
|
|
284
|
+
"Non-allowlisted binaries, path binaries, execution-enabling "
|
|
285
|
+
"flags, and out-of-sandbox write arguments are refused; refusals "
|
|
286
|
+
"carry stable codes.",
|
|
287
|
+
probe_shell_fail_closed),
|
|
288
|
+
Requirement(
|
|
289
|
+
"R4", "injected-argv resistance",
|
|
290
|
+
"Smuggled shell text cannot interpolate; injected argv always "
|
|
291
|
+
"ends in a refused, code-carrying decision.",
|
|
292
|
+
probe_injection_resistance),
|
|
293
|
+
Requirement(
|
|
294
|
+
"R5", "dry-run-first",
|
|
295
|
+
"With dry_run_default, calls only plan; no filesystem mutation.",
|
|
296
|
+
probe_dry_run_default),
|
|
297
|
+
Requirement(
|
|
298
|
+
"R6", "kill switch",
|
|
299
|
+
"The KILL file in data_dir stops executing tools via "
|
|
300
|
+
"E_KILL_SWITCH and cleans up after itself.",
|
|
301
|
+
probe_kill_switch),
|
|
302
|
+
Requirement(
|
|
303
|
+
"R7", "auditable decisions",
|
|
304
|
+
"Refusals write exactly the audit rows they claim to write.",
|
|
305
|
+
probe_audit_completeness),
|
|
306
|
+
Requirement(
|
|
307
|
+
"R8", "echo-loop breaker",
|
|
308
|
+
"Repeated identical denials escalate with a CIRCUIT BREAKER stop "
|
|
309
|
+
"message.",
|
|
310
|
+
probe_circuit_breaker),
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------- certificate ----------
|
|
315
|
+
|
|
316
|
+
def _suite_digest(checks: list[dict]) -> str:
|
|
317
|
+
payload = ",".join(f"{c['id']}:{c['passed']}" for c in checks)
|
|
318
|
+
return f"sha256:{hashlib.sha256(payload.encode()).hexdigest()[:16]}"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def run_certification(root: Path | None = None,
|
|
322
|
+
requirements: list[Requirement] | None = None,
|
|
323
|
+
) -> dict[str, Any]:
|
|
324
|
+
"""Run the suite; return the certificate dict. Probe failures are
|
|
325
|
+
results, never exceptions. Process-global config/engine state is
|
|
326
|
+
snapshotted and restored — the caller's environment survives intact."""
|
|
327
|
+
if root is None:
|
|
328
|
+
root = Path.cwd() / ".vtai-cert"
|
|
329
|
+
from . import config as config_mod
|
|
330
|
+
prev_settings = config_mod._settings
|
|
331
|
+
prev_engine = db._engine
|
|
332
|
+
try:
|
|
333
|
+
return _run_certification_inner(root, requirements)
|
|
334
|
+
finally:
|
|
335
|
+
config_mod._settings = prev_settings
|
|
336
|
+
db._engine = prev_engine
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _run_certification_inner(root: Path,
|
|
340
|
+
requirements: list[Requirement] | None) -> dict[str, Any]:
|
|
341
|
+
requirements = requirements or default_requirements()
|
|
342
|
+
ctx = build_context(root)
|
|
343
|
+
checks: list[dict[str, Any]] = []
|
|
344
|
+
for req in requirements:
|
|
345
|
+
try:
|
|
346
|
+
res = req.probe(ctx)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
res = ProbeResult(False, f"harness exception {type(e).__name__}: {e}")
|
|
349
|
+
checks.append({"id": req.id, "title": req.title, "spec": req.spec,
|
|
350
|
+
"passed": res.passed, "evidence": res.evidence[:500]})
|
|
351
|
+
|
|
352
|
+
cert: dict[str, Any] = {
|
|
353
|
+
"product": "vtai-agent",
|
|
354
|
+
"version": __version__,
|
|
355
|
+
"profile": PROFILE,
|
|
356
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
357
|
+
"host": {"python": platform.python_version(), "os": platform.platform()},
|
|
358
|
+
"config": {
|
|
359
|
+
"writable_roots": [str(r) for r in ctx.settings.paths.writable_roots],
|
|
360
|
+
"deny_globs": ctx.settings.guardrails.deny_globs,
|
|
361
|
+
"shell_allowlist": ctx.settings.guardrails.shell_allowlist,
|
|
362
|
+
"capabilities_rea_enabled": ctx.settings.capabilities.rea.enabled,
|
|
363
|
+
},
|
|
364
|
+
"checks": checks,
|
|
365
|
+
"all_passed": all(c["passed"] for c in checks),
|
|
366
|
+
"suite_digest": _suite_digest(checks),
|
|
367
|
+
"audit_rows_total": ctx.audit_count(),
|
|
368
|
+
}
|
|
369
|
+
payload = json.dumps(cert, sort_keys=True, default=str)
|
|
370
|
+
cert["digest"] = f"sha256:{hashlib.sha256(payload.encode()).hexdigest()[:16]}"
|
|
371
|
+
shutil.rmtree(ctx.data_dir, ignore_errors=True) # remove the audited DB copy
|
|
372
|
+
return cert
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def format_report(cert: dict) -> str:
|
|
376
|
+
lines = [
|
|
377
|
+
f"vtai-cert {cert['profile']} — {cert['product']} v{cert['version']} ({cert['host']['os']})",
|
|
378
|
+
]
|
|
379
|
+
for c in cert["checks"]:
|
|
380
|
+
state = "PASS" if c["passed"] else "FAIL"
|
|
381
|
+
note = "" if c["passed"] else f" — {c['evidence'][:260]}"
|
|
382
|
+
lines.append(f" [{state}] {c['id']:3} {c['title']}{note}")
|
|
383
|
+
lines.append(f"suite_digest {cert['suite_digest']}; all_passed={cert['all_passed']}; "
|
|
384
|
+
f"cert {cert['digest']}")
|
|
385
|
+
return "\n".join(lines)
|