panoptis 0.3.1__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.
- panopti/__init__.py +166 -0
- panopti/__main__.py +8 -0
- panopti/_banner.py +37 -0
- panopti/attest.py +175 -0
- panopti/attest_keys.py +202 -0
- panopti/bench.py +127 -0
- panopti/cli.py +324 -0
- panopti/contracts.py +228 -0
- panopti/court.py +149 -0
- panopti/demo.py +383 -0
- panopti/doctor.py +90 -0
- panopti/egress.py +66 -0
- panopti/guard.py +260 -0
- panopti/killswitch.py +85 -0
- panopti/mesh.py +122 -0
- panopti/playground.html +791 -0
- panopti/playground.py +71 -0
- panopti/presets/__init__.py +35 -0
- panopti/presets/profiles.py +87 -0
- panopti/presets/rules.py +307 -0
- panopti/replay.py +197 -0
- panopti/reports.py +143 -0
- panopti/shield.py +53 -0
- panopti/synth.py +147 -0
- panopti/themes.py +205 -0
- panoptis/__init__.py +19 -0
- panoptis/__main__.py +6 -0
- panoptis/_version.py +30 -0
- panoptis/adapters/__init__.py +22 -0
- panoptis/adapters/autogen.py +188 -0
- panoptis/adapters/autogpt.py +55 -0
- panoptis/adapters/base.py +62 -0
- panoptis/adapters/claude_code.py +516 -0
- panoptis/adapters/crewai.py +119 -0
- panoptis/adapters/generic.py +143 -0
- panoptis/adapters/langchain.py +276 -0
- panoptis/adapters/langgraph.py +191 -0
- panoptis/adapters/openai_agents.py +117 -0
- panoptis/adapters/openclaw.py +32 -0
- panoptis/adapters/smolagents.py +189 -0
- panoptis/alerting/__init__.py +6 -0
- panoptis/alerting/channels.py +124 -0
- panoptis/alerting/engine.py +220 -0
- panoptis/analysis/__init__.py +0 -0
- panoptis/analysis/base.py +53 -0
- panoptis/analysis/capabilities.py +164 -0
- panoptis/analysis/entropy.py +174 -0
- panoptis/analysis/maturity.py +80 -0
- panoptis/analysis/registry.py +19 -0
- panoptis/api/__init__.py +1 -0
- panoptis/api/auth.py +214 -0
- panoptis/api/entitlement.py +102 -0
- panoptis/api/limits.py +172 -0
- panoptis/api/middleware/__init__.py +0 -0
- panoptis/api/middleware/rate_limiter.py +288 -0
- panoptis/api/protect_api.py +450 -0
- panoptis/api/protect_approvals.py +103 -0
- panoptis/api/protect_control.py +100 -0
- panoptis/api/protect_rbac.py +109 -0
- panoptis/api/server.py +2355 -0
- panoptis/api/tenancy.py +105 -0
- panoptis/api/tenant_auth.py +265 -0
- panoptis/api/tenant_store_sql.py +529 -0
- panoptis/api/v3.py +838 -0
- panoptis/behaviour/__init__.py +0 -0
- panoptis/behaviour/drift.py +186 -0
- panoptis/behaviour/motifs.py +603 -0
- panoptis/behaviour/profile.py +247 -0
- panoptis/causality/__init__.py +23 -0
- panoptis/causality/cones.py +181 -0
- panoptis/causality/decay.py +302 -0
- panoptis/causality/hypotheses.py +218 -0
- panoptis/circuit_breaker/__init__.py +34 -0
- panoptis/circuit_breaker/breaker.py +470 -0
- panoptis/circuit_breaker/loop_detector.py +134 -0
- panoptis/circuit_breaker/thresholds.py +66 -0
- panoptis/cli/__init__.py +11 -0
- panoptis/cli/_utils/__init__.py +11 -0
- panoptis/cli/_utils/run_cmd.py +170 -0
- panoptis/cli/animator.py +117 -0
- panoptis/cli/demo.py +606 -0
- panoptis/cli/init.py +124 -0
- panoptis/cli/main.py +3218 -0
- panoptis/cli/mcp.py +201 -0
- panoptis/cli/protect_ops.py +513 -0
- panoptis/cli/v3.py +1212 -0
- panoptis/cli/verify_env.py +151 -0
- panoptis/compare/__init__.py +0 -0
- panoptis/compare/runs.py +405 -0
- panoptis/contracts/__init__.py +149 -0
- panoptis/contracts/coverage.py +190 -0
- panoptis/contracts/evaluator.py +528 -0
- panoptis/contracts/irreversible.py +178 -0
- panoptis/contracts/mfotl.py +1614 -0
- panoptis/contracts/order.py +496 -0
- panoptis/contracts/remedies.py +44 -0
- panoptis/contracts/schema.py +198 -0
- panoptis/contracts/streaming.py +751 -0
- panoptis/core/__init__.py +37 -0
- panoptis/core/blast_radius.py +170 -0
- panoptis/core/config.py +72 -0
- panoptis/core/event_bus.py +384 -0
- panoptis/core/http_forwarder.py +87 -0
- panoptis/core/injection.py +145 -0
- panoptis/core/loop_detector.py +93 -0
- panoptis/core/models.py +835 -0
- panoptis/core/policy_dsl.py +373 -0
- panoptis/core/policy_loader.py +161 -0
- panoptis/core/recursion_depth_detector.py +78 -0
- panoptis/core/risk.py +101 -0
- panoptis/core/safety.py +933 -0
- panoptis/core/schema.py +501 -0
- panoptis/core/watcher.py +1131 -0
- panoptis/cost/__init__.py +8 -0
- panoptis/cost/anomaly.py +74 -0
- panoptis/cost/caching.py +13 -0
- panoptis/cost/comparator.py +80 -0
- panoptis/cost/complexity_router.py +213 -0
- panoptis/cost/governance.py +189 -0
- panoptis/cost/predictor.py +97 -0
- panoptis/cost/reporting.py +166 -0
- panoptis/cost/roi.py +100 -0
- panoptis/cost/router.py +16 -0
- panoptis/cost/semantic_cache.py +102 -0
- panoptis/cost/tracker.py +176 -0
- panoptis/entities/__init__.py +0 -0
- panoptis/entities/resolve.py +150 -0
- panoptis/eval/__init__.py +35 -0
- panoptis/eval/retrieval_drift.py +171 -0
- panoptis/eval/runner.py +258 -0
- panoptis/events/__init__.py +0 -0
- panoptis/events/codec.py +64 -0
- panoptis/events/model.py +289 -0
- panoptis/events/normalize.py +298 -0
- panoptis/events/normalizers/__init__.py +0 -0
- panoptis/events/normalizers/anthropic.py +3 -0
- panoptis/events/normalizers/claude_code.py +299 -0
- panoptis/events/normalizers/langchain.py +223 -0
- panoptis/events/normalizers/legacy.py +230 -0
- panoptis/events/normalizers/llm_api.py +221 -0
- panoptis/events/normalizers/mcp.py +185 -0
- panoptis/events/normalizers/native.py +297 -0
- panoptis/events/normalizers/openai.py +3 -0
- panoptis/events/normalizers/otel.py +355 -0
- panoptis/events/registry.py +44 -0
- panoptis/evidence/__init__.py +36 -0
- panoptis/evidence/attestation.py +465 -0
- panoptis/evidence/blobs.py +89 -0
- panoptis/evidence/canonical.py +63 -0
- panoptis/evidence/crypto.py +81 -0
- panoptis/evidence/ids.py +41 -0
- panoptis/evidence/merkle.py +186 -0
- panoptis/evidence/model.py +275 -0
- panoptis/evidence/recursive_proof.py +193 -0
- panoptis/evidence/redaction.py +149 -0
- panoptis/evidence/segments.py +160 -0
- panoptis/evidence/shredding.py +169 -0
- panoptis/evidence/verifier.py +238 -0
- panoptis/forecasting/__init__.py +0 -0
- panoptis/forecasting/trajectory.py +196 -0
- panoptis/governance/__init__.py +14 -0
- panoptis/governance/audit_log.py +272 -0
- panoptis/governance/causal.py +108 -0
- panoptis/governance/compliance_reporter.py +129 -0
- panoptis/governance/engine.py +360 -0
- panoptis/governance/eu_ai_act.py +146 -0
- panoptis/governance/gdpr.py +423 -0
- panoptis/governance/hipaa.py +107 -0
- panoptis/governance/iso42001.py +131 -0
- panoptis/governance/rbac.py +213 -0
- panoptis/governance/reports.py +134 -0
- panoptis/governance/residency.py +92 -0
- panoptis/graph/__init__.py +0 -0
- panoptis/graph/build.py +248 -0
- panoptis/graph/information.py +890 -0
- panoptis/graph/model.py +177 -0
- panoptis/graph/traverse.py +266 -0
- panoptis/hitl/__init__.py +31 -0
- panoptis/hitl/decisions.py +102 -0
- panoptis/hitl/feedback.py +154 -0
- panoptis/hitl/orchestrator.py +295 -0
- panoptis/hitl/thresholds.py +53 -0
- panoptis/infrastructure/__init__.py +1 -0
- panoptis/infrastructure/config.py +51 -0
- panoptis/infrastructure/router.py +176 -0
- panoptis/instrument.py +210 -0
- panoptis/lab/__init__.py +0 -0
- panoptis/lab/branch.py +252 -0
- panoptis/lab/observe.py +84 -0
- panoptis/lab/replay.py +237 -0
- panoptis/lab/replay_runtime.py +117 -0
- panoptis/lattice/__init__.py +29 -0
- panoptis/lattice/attention_scatter.py +198 -0
- panoptis/lattice/shadow_filesystem.py +200 -0
- panoptis/memory/__init__.py +15 -0
- panoptis/memory/causal_graph.py +130 -0
- panoptis/memory/decay.py +189 -0
- panoptis/memory/engine.py +636 -0
- panoptis/memory/governance.py +199 -0
- panoptis/memory/graph_query.py +403 -0
- panoptis/memory/health.py +118 -0
- panoptis/memory/identity.py +87 -0
- panoptis/memory/nlquery.py +183 -0
- panoptis/memory/resolver.py +76 -0
- panoptis/memory/temporal_decay.py +58 -0
- panoptis/memory/visualization.py +93 -0
- panoptis/models/__init__.py +0 -0
- panoptis/models/cache.py +14 -0
- panoptis/models/tenant.py +229 -0
- panoptis/monitoring/__init__.py +0 -0
- panoptis/monitoring/context_guard.py +363 -0
- panoptis/monitoring/metrics.py +122 -0
- panoptis/orchestration/__init__.py +19 -0
- panoptis/orchestration/consensus.py +84 -0
- panoptis/orchestration/crew_context.py +86 -0
- panoptis/orchestration/dag.py +119 -0
- panoptis/orchestration/deadlock.py +52 -0
- panoptis/orchestration/engine.py +418 -0
- panoptis/orchestration/handoff_tracker.py +91 -0
- panoptis/orchestration/propagation.py +57 -0
- panoptis/orchestration/race_condition.py +134 -0
- panoptis/orchestration/shapley.py +76 -0
- panoptis/orchestration/spawning.py +80 -0
- panoptis/orchestration/trust.py +61 -0
- panoptis/platform/__init__.py +1 -0
- panoptis/platform/cloud.py +105 -0
- panoptis/platform/eval_builder.py +90 -0
- panoptis/platform/intelligence.py +176 -0
- panoptis/platform/prompts.py +140 -0
- panoptis/platform/sharing.py +204 -0
- panoptis/plugins/__init__.py +6 -0
- panoptis/plugins/registry.py +102 -0
- panoptis/plugins/sandbox.py +155 -0
- panoptis/protect/__init__.py +230 -0
- panoptis/protect/approvals.py +433 -0
- panoptis/protect/arguments.py +1699 -0
- panoptis/protect/claude_hook.py +284 -0
- panoptis/protect/claude_hook_entry.py +21 -0
- panoptis/protect/control.py +268 -0
- panoptis/protect/counterfactual.py +306 -0
- panoptis/protect/crescendo_drift.py +255 -0
- panoptis/protect/discovery.py +621 -0
- panoptis/protect/enclave_attestation.py +255 -0
- panoptis/protect/enforce.py +290 -0
- panoptis/protect/formal_verifier.py +544 -0
- panoptis/protect/ifc_lattice.py +198 -0
- panoptis/protect/integrations.py +317 -0
- panoptis/protect/integrity.py +89 -0
- panoptis/protect/intent.py +674 -0
- panoptis/protect/kernel_sandbox.py +271 -0
- panoptis/protect/ledger.py +263 -0
- panoptis/protect/loops.py +460 -0
- panoptis/protect/ltl_trajectory.py +232 -0
- panoptis/protect/mcp_gateway.py +1012 -0
- panoptis/protect/memory_sentry.py +257 -0
- panoptis/protect/model.py +272 -0
- panoptis/protect/output.py +485 -0
- panoptis/protect/pipeline.py +1493 -0
- panoptis/protect/pqc_attestation.py +464 -0
- panoptis/protect/rag_sanitizer.py +185 -0
- panoptis/protect/rbac.py +946 -0
- panoptis/protect/registry.py +62 -0
- panoptis/protect/stego_neutralizer.py +182 -0
- panoptis/protect/support.py +208 -0
- panoptis/protect/taint_tracker.py +152 -0
- panoptis/protect/temporal_simulator.py +615 -0
- panoptis/protect/threats.py +234 -0
- panoptis/protect/transactor.py +100 -0
- panoptis/protect/wasi_sandbox.py +374 -0
- panoptis/protocol/__init__.py +1 -0
- panoptis/protocol/badge.py +81 -0
- panoptis/protocol/benchmark.py +104 -0
- panoptis/protocol/mcp_server.py +295 -0
- panoptis/protocol/schema_v1.py +132 -0
- panoptis/provenance/__init__.py +0 -0
- panoptis/provenance/ambiguity.py +103 -0
- panoptis/provenance/lineage.py +268 -0
- panoptis/query/__init__.py +0 -0
- panoptis/query/engine.py +368 -0
- panoptis/query/workspace.py +360 -0
- panoptis/reasoning/__init__.py +8 -0
- panoptis/reasoning/adversarial.py +97 -0
- panoptis/reasoning/auditor.py +359 -0
- panoptis/reasoning/benchmark.py +148 -0
- panoptis/reasoning/calibration.py +109 -0
- panoptis/reasoning/dual_eval.py +73 -0
- panoptis/reasoning/explainer.py +88 -0
- panoptis/reasoning/fingerprint.py +105 -0
- panoptis/reasoning/goal_drift.py +86 -0
- panoptis/reasoning/hallucination.py +122 -0
- panoptis/reasoning/quality.py +117 -0
- panoptis/reasoning/semantic_drift.py +92 -0
- panoptis/reasoning/trust_score.py +132 -0
- panoptis/replay/__init__.py +68 -0
- panoptis/replay/causal_scoring.py +186 -0
- panoptis/replay/counterfactual.py +459 -0
- panoptis/replay/divergence.py +293 -0
- panoptis/replay/engine.py +499 -0
- panoptis/replay/interventions.py +219 -0
- panoptis/replay/invariants.py +310 -0
- panoptis/replay/shadow.py +649 -0
- panoptis/rollback/__init__.py +11 -0
- panoptis/rollback/engine.py +507 -0
- panoptis/runs/__init__.py +0 -0
- panoptis/runs/segment.py +157 -0
- panoptis/runtime/__init__.py +0 -0
- panoptis/runtime/engine.py +472 -0
- panoptis/runtime/killswitch.py +7 -0
- panoptis/scoring/__init__.py +15 -0
- panoptis/scoring/confidence.py +354 -0
- panoptis/scoring/drift.py +170 -0
- panoptis/scoring/silence.py +143 -0
- panoptis/security/__init__.py +6 -0
- panoptis/security/abuse_detection.py +97 -0
- panoptis/security/checkout.py +50 -0
- panoptis/security/egress.py +7 -0
- panoptis/security/entitlement_store.py +96 -0
- panoptis/security/exfiltration.py +125 -0
- panoptis/security/license.py +176 -0
- panoptis/security/owasp.py +191 -0
- panoptis/security/payloads.json +72 -0
- panoptis/security/redaction.py +193 -0
- panoptis/security/redteam.py +224 -0
- panoptis/security/report.py +125 -0
- panoptis/security/sandbox.py +153 -0
- panoptis/security/shield.py +7 -0
- panoptis/security/webhook_signing.py +140 -0
- panoptis/sensors/__init__.py +58 -0
- panoptis/sensors/base.py +215 -0
- panoptis/sensors/claude_code/__init__.py +109 -0
- panoptis/sensors/groq_sensor.py +388 -0
- panoptis/sensors/langchain/__init__.py +418 -0
- panoptis/sensors/legacy/__init__.py +0 -0
- panoptis/sensors/legacy/translator.py +177 -0
- panoptis/sensors/llm_clients.py +168 -0
- panoptis/sensors/mcp.py +48 -0
- panoptis/sensors/mcp_error.py +74 -0
- panoptis/sensors/native/__init__.py +3 -0
- panoptis/sensors/native/launch.py +76 -0
- panoptis/sensors/native/recorder.py +493 -0
- panoptis/sensors/openrouter_sensor.py +421 -0
- panoptis/sensors/otel/__init__.py +223 -0
- panoptis/state/__init__.py +0 -0
- panoptis/state/latent.py +215 -0
- panoptis/storage/__init__.py +23 -0
- panoptis/storage/hyperedge.py +417 -0
- panoptis/storage/migration.py +189 -0
- panoptis/storage/schema.py +381 -0
- panoptis/storage/store.py +1833 -0
- panoptis/tasks.py +120 -0
- panoptis/telemetry/__init__.py +1 -0
- panoptis/telemetry/collector.py +271 -0
- panoptis/telemetry/execution_logger.py +189 -0
- panoptis/telemetry/exporter.py +139 -0
- panoptis/telemetry/otel.py +653 -0
- panoptis/tracing/__init__.py +8 -0
- panoptis/tracing/audit.py +136 -0
- panoptis/tracing/collector.py +367 -0
- panoptis/tracing/live.py +149 -0
- panoptis/tracing/otel.py +1 -0
- panoptis/tracing/sampling.py +128 -0
- panoptis/tracing/spans.py +186 -0
- panoptis/tracing/trajectory.py +147 -0
- panoptis/validation/__init__.py +0 -0
- panoptis/validation/schema_validator.py +36 -0
- panoptis-0.3.1.dist-info/METADATA +657 -0
- panoptis-0.3.1.dist-info/RECORD +370 -0
- panoptis-0.3.1.dist-info/WHEEL +4 -0
- panoptis-0.3.1.dist-info/entry_points.txt +3 -0
- panoptis-0.3.1.dist-info/licenses/LICENSE +201 -0
panopti/__init__.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Panopti: The All-Seeing Pre-Execution Guardrail & Forensic Engine for Autonomous AI.
|
|
2
|
+
|
|
3
|
+
Panopti is the successor to AgentWatch, providing pre-execution contract verification,
|
|
4
|
+
causal confidence decay tracking, cryptographic witness proofs, and deterministic forensic
|
|
5
|
+
shadow replay for agent swarms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from panoptis._version import __version__
|
|
12
|
+
except (ImportError, ModuleNotFoundError):
|
|
13
|
+
__version__ = "0.2.0"
|
|
14
|
+
|
|
15
|
+
from panopti.attest import (
|
|
16
|
+
AttestationEngine,
|
|
17
|
+
PanoptiAttestationSeal,
|
|
18
|
+
)
|
|
19
|
+
from panopti.attest_keys import AttestationKeyError, Keyring, keyring_from_env
|
|
20
|
+
from panopti.contracts import (
|
|
21
|
+
Contract,
|
|
22
|
+
ContractRegistry,
|
|
23
|
+
ContractViolationError,
|
|
24
|
+
Disallows,
|
|
25
|
+
MaxFrequency,
|
|
26
|
+
RequiresPrior,
|
|
27
|
+
)
|
|
28
|
+
from panopti.court import (
|
|
29
|
+
WitnessChain,
|
|
30
|
+
WitnessEntry,
|
|
31
|
+
)
|
|
32
|
+
from panopti.egress import (
|
|
33
|
+
EgressGuard,
|
|
34
|
+
SSRFBlockedError,
|
|
35
|
+
)
|
|
36
|
+
from panopti.killswitch import (
|
|
37
|
+
EmergencyKillSwitch,
|
|
38
|
+
EmergencyStopTriggered,
|
|
39
|
+
)
|
|
40
|
+
from panopti.mesh import (
|
|
41
|
+
SwarmAgentNode,
|
|
42
|
+
SwarmMesh,
|
|
43
|
+
)
|
|
44
|
+
from panopti.replay import (
|
|
45
|
+
DropStepPerturbation,
|
|
46
|
+
MutateResultPerturbation,
|
|
47
|
+
Perturbation,
|
|
48
|
+
RecordedCall,
|
|
49
|
+
ReplayReport,
|
|
50
|
+
ReplayTape,
|
|
51
|
+
ShadowReplayer,
|
|
52
|
+
)
|
|
53
|
+
from panopti.shield import (
|
|
54
|
+
AntiTamperShield,
|
|
55
|
+
TamperAttemptBlocked,
|
|
56
|
+
)
|
|
57
|
+
from panopti.synth import (
|
|
58
|
+
PolicySynthesizer,
|
|
59
|
+
SynthesizedInvariant,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from panopti.doctor import run_diagnostics
|
|
64
|
+
from panopti.guard import (
|
|
65
|
+
GuardDecision,
|
|
66
|
+
PanoptiGuard,
|
|
67
|
+
guard,
|
|
68
|
+
)
|
|
69
|
+
from panoptis.causality import (
|
|
70
|
+
CUMULATIVE_CONFIDENCE_COLLAPSE,
|
|
71
|
+
ConfidenceCollapseWarning,
|
|
72
|
+
ConfidenceDecayTracker,
|
|
73
|
+
)
|
|
74
|
+
from panoptis.circuit_breaker import (
|
|
75
|
+
AgentLoopError,
|
|
76
|
+
AgentLoopException,
|
|
77
|
+
CircuitBreaker,
|
|
78
|
+
CircuitState,
|
|
79
|
+
CircuitThresholds,
|
|
80
|
+
LoopDetector,
|
|
81
|
+
TripReason,
|
|
82
|
+
)
|
|
83
|
+
from panoptis.core.watcher import (
|
|
84
|
+
AgentWatchBlockedError,
|
|
85
|
+
GenericAdapter,
|
|
86
|
+
detect_framework,
|
|
87
|
+
detect_framework_label,
|
|
88
|
+
watch,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
PanoptiBlockedError = AgentWatchBlockedError
|
|
92
|
+
except ImportError: # pragma: no cover
|
|
93
|
+
watch = None # type: ignore[assignment]
|
|
94
|
+
guard = None # type: ignore[assignment]
|
|
95
|
+
detect_framework = None # type: ignore[assignment]
|
|
96
|
+
detect_framework_label = None # type: ignore[assignment]
|
|
97
|
+
GenericAdapter = None # type: ignore[assignment]
|
|
98
|
+
AgentWatchBlockedError = Exception # type: ignore[assignment,misc]
|
|
99
|
+
PanoptiBlockedError = AgentWatchBlockedError
|
|
100
|
+
CircuitBreaker = None # type: ignore[assignment]
|
|
101
|
+
CircuitState = None # type: ignore[assignment]
|
|
102
|
+
CircuitThresholds = None # type: ignore[assignment]
|
|
103
|
+
TripReason = None # type: ignore[assignment]
|
|
104
|
+
LoopDetector = None # type: ignore[assignment]
|
|
105
|
+
AgentLoopError = Exception # type: ignore[assignment,misc]
|
|
106
|
+
AgentLoopException = AgentLoopError
|
|
107
|
+
PanoptiGuard = None # type: ignore[assignment]
|
|
108
|
+
GuardDecision = None # type: ignore[assignment]
|
|
109
|
+
ConfidenceDecayTracker = None # type: ignore[assignment]
|
|
110
|
+
CUMULATIVE_CONFIDENCE_COLLAPSE = "CUMULATIVE_CONFIDENCE_COLLAPSE"
|
|
111
|
+
ConfidenceCollapseWarning = None # type: ignore[assignment]
|
|
112
|
+
run_diagnostics = None # type: ignore[assignment]
|
|
113
|
+
|
|
114
|
+
__all__ = [
|
|
115
|
+
"__version__",
|
|
116
|
+
"guard",
|
|
117
|
+
"watch",
|
|
118
|
+
"detect_framework",
|
|
119
|
+
"detect_framework_label",
|
|
120
|
+
"GenericAdapter",
|
|
121
|
+
"PanoptiBlockedError",
|
|
122
|
+
"AgentWatchBlockedError",
|
|
123
|
+
"CircuitBreaker",
|
|
124
|
+
"CircuitState",
|
|
125
|
+
"CircuitThresholds",
|
|
126
|
+
"TripReason",
|
|
127
|
+
"LoopDetector",
|
|
128
|
+
"AgentLoopError",
|
|
129
|
+
"AgentLoopException",
|
|
130
|
+
"ConfidenceDecayTracker",
|
|
131
|
+
"CUMULATIVE_CONFIDENCE_COLLAPSE",
|
|
132
|
+
"ConfidenceCollapseWarning",
|
|
133
|
+
"PanoptiGuard",
|
|
134
|
+
"GuardDecision",
|
|
135
|
+
"Contract",
|
|
136
|
+
"ContractRegistry",
|
|
137
|
+
"RequiresPrior",
|
|
138
|
+
"Disallows",
|
|
139
|
+
"MaxFrequency",
|
|
140
|
+
"ContractViolationError",
|
|
141
|
+
"WitnessEntry",
|
|
142
|
+
"WitnessChain",
|
|
143
|
+
"SwarmAgentNode",
|
|
144
|
+
"SwarmMesh",
|
|
145
|
+
"run_diagnostics",
|
|
146
|
+
"RecordedCall",
|
|
147
|
+
"ReplayTape",
|
|
148
|
+
"ShadowReplayer",
|
|
149
|
+
"ReplayReport",
|
|
150
|
+
"Perturbation",
|
|
151
|
+
"DropStepPerturbation",
|
|
152
|
+
"MutateResultPerturbation",
|
|
153
|
+
"PanoptiAttestationSeal",
|
|
154
|
+
"AttestationKeyError",
|
|
155
|
+
"Keyring",
|
|
156
|
+
"keyring_from_env",
|
|
157
|
+
"AttestationEngine",
|
|
158
|
+
"PolicySynthesizer",
|
|
159
|
+
"SynthesizedInvariant",
|
|
160
|
+
"EmergencyKillSwitch",
|
|
161
|
+
"EmergencyStopTriggered",
|
|
162
|
+
"AntiTamperShield",
|
|
163
|
+
"TamperAttemptBlocked",
|
|
164
|
+
"EgressGuard",
|
|
165
|
+
"SSRFBlockedError",
|
|
166
|
+
]
|
panopti/__main__.py
ADDED
panopti/_banner.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Panopti ASCII Banner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
PANOPTI_ASCII_BANNER = r"""
|
|
8
|
+
██████╗ █████╗ ███╗ ██╗ ██████╗ ██████╗ ████████╗██╗
|
|
9
|
+
██╔══██╗██╔══██╗████╗ ██║██╔═══██╗██╔══██╗╚══██╔══╝██║
|
|
10
|
+
██████╔╝███████║██╔██╗ ██║██║ ██║██████╔╝ ██║ ██║
|
|
11
|
+
██╔═══╝ ██╔══██║██║╚██╗██║██║ ██║██╔═══╝ ██║ ██║
|
|
12
|
+
██║ ██║ ██║██║ ╚████║╚██████╔╝██║ ██║ ██║
|
|
13
|
+
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
14
|
+
Argos Panoptes • All-Seeing Pre-Execution Guardrail
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
PANOPTI_ASCII_BANNER_SAFE = r"""
|
|
18
|
+
____ _ _
|
|
19
|
+
| _ \ __ _ _ __ ___ _ __ _ _ (_) | |
|
|
20
|
+
| |_) / _` | '_ \ / _ \| '_ \| |_| | | | |
|
|
21
|
+
| __/ (_| | | | | (_) | |_) | _ | | |_|
|
|
22
|
+
|_| \__,_|_| |_|\___/| .__/|_| |_|_| (_)
|
|
23
|
+
|_|
|
|
24
|
+
Argos Panoptes - All-Seeing Pre-Execution Guardrail
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_banner() -> None:
|
|
29
|
+
"""Print banner safely across all terminal encodings (e.g. Windows cp1252/cp437)."""
|
|
30
|
+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
|
|
31
|
+
try:
|
|
32
|
+
PANOPTI_ASCII_BANNER.encode(encoding)
|
|
33
|
+
sys.stdout.write(PANOPTI_ASCII_BANNER + "\n")
|
|
34
|
+
sys.stdout.flush()
|
|
35
|
+
except (UnicodeEncodeError, LookupError):
|
|
36
|
+
sys.stdout.write(PANOPTI_ASCII_BANNER_SAFE + "\n")
|
|
37
|
+
sys.stdout.flush()
|
panopti/attest.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Panopti Cryptographic Policy Attestation & Verifiable Audit Seals.
|
|
2
|
+
|
|
3
|
+
Generates tamper-evident cryptographic seals for agent execution traces, proving
|
|
4
|
+
formal invariant compliance and absence of runaway loops.
|
|
5
|
+
Pure standard library (Ponytail: Ultra).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import json
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from panopti.attest_keys import AttestationKeyError, Keyring, keyring_from_env
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _canonical_json(data: Any) -> str:
|
|
22
|
+
"""Serialize object to canonical, deterministic JSON."""
|
|
23
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SEAL_VERSION = "2"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class PanoptiAttestationSeal:
|
|
31
|
+
"""Cryptographic attestation certificate proving verified execution.
|
|
32
|
+
|
|
33
|
+
Version 2 seals name the key that signed them (``key_id``) and sign every field,
|
|
34
|
+
including the version and key id, as canonical JSON.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
seal_id: str
|
|
38
|
+
merkle_root: str
|
|
39
|
+
policy_verdict: str
|
|
40
|
+
confidence_floor: float
|
|
41
|
+
loop_status: str
|
|
42
|
+
tool_count: int
|
|
43
|
+
timestamp_iso: str
|
|
44
|
+
signature: str
|
|
45
|
+
key_id: str | None = None
|
|
46
|
+
version: str = "1"
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
return asdict(self)
|
|
50
|
+
|
|
51
|
+
def to_json(self) -> str:
|
|
52
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
53
|
+
|
|
54
|
+
def signed_fields(self) -> dict[str, Any]:
|
|
55
|
+
d = self.to_dict()
|
|
56
|
+
d.pop("signature")
|
|
57
|
+
d["confidence_floor"] = f"{float(d['confidence_floor']):.4f}"
|
|
58
|
+
return d
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: dict[str, Any]) -> PanoptiAttestationSeal:
|
|
62
|
+
return cls(
|
|
63
|
+
seal_id=data["seal_id"],
|
|
64
|
+
merkle_root=data["merkle_root"],
|
|
65
|
+
policy_verdict=data["policy_verdict"],
|
|
66
|
+
confidence_floor=float(data["confidence_floor"]),
|
|
67
|
+
loop_status=data["loop_status"],
|
|
68
|
+
tool_count=int(data["tool_count"]),
|
|
69
|
+
timestamp_iso=data["timestamp_iso"],
|
|
70
|
+
signature=data["signature"],
|
|
71
|
+
key_id=data.get("key_id"),
|
|
72
|
+
version=str(data.get("version", "1")),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_json(cls, json_str: str) -> PanoptiAttestationSeal:
|
|
77
|
+
return cls.from_dict(json.loads(json_str))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _keyring_for(keyring: Keyring | None, secret_key: str | bytes | None) -> Keyring:
|
|
81
|
+
"""The keyring to use: explicit, else a one-key ring from ``secret_key``, else the env."""
|
|
82
|
+
if keyring is not None:
|
|
83
|
+
return keyring
|
|
84
|
+
if secret_key is not None:
|
|
85
|
+
ring = Keyring()
|
|
86
|
+
key = secret_key.encode() if isinstance(secret_key, str) else secret_key
|
|
87
|
+
ring.add("explicit", key) # validated: strength, not a known public default
|
|
88
|
+
return ring
|
|
89
|
+
return keyring_from_env()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AttestationEngine:
|
|
93
|
+
"""Issues and cryptographically validates Panopti attestation seals."""
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _v1_payload(seal: PanoptiAttestationSeal) -> str:
|
|
97
|
+
return (
|
|
98
|
+
f"{seal.seal_id}|{seal.merkle_root}|{seal.policy_verdict}|"
|
|
99
|
+
f"{seal.confidence_floor:.4f}|{seal.loop_status}|{seal.tool_count}|{seal.timestamp_iso}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _sign(key: bytes, seal: PanoptiAttestationSeal) -> str:
|
|
104
|
+
return hmac.new(
|
|
105
|
+
key, _canonical_json(seal.signed_fields()).encode(), hashlib.sha256
|
|
106
|
+
).hexdigest()
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def create_seal(
|
|
110
|
+
cls,
|
|
111
|
+
merkle_root: str,
|
|
112
|
+
tool_count: int,
|
|
113
|
+
confidence_floor: float = 1.0,
|
|
114
|
+
is_compliant: bool = True,
|
|
115
|
+
loop_detected: bool = False,
|
|
116
|
+
secret_key: str | bytes | None = None,
|
|
117
|
+
*,
|
|
118
|
+
keyring: Keyring | None = None,
|
|
119
|
+
) -> PanoptiAttestationSeal:
|
|
120
|
+
"""Issue a signed (version 2) attestation seal.
|
|
121
|
+
|
|
122
|
+
The key comes from ``keyring``, else ``secret_key`` (checked: at least 32 bytes,
|
|
123
|
+
not a known public key), else ``AGENTWATCH_ATTEST_KEYS``. There is no default key:
|
|
124
|
+
with none configured this raises :class:`AttestationKeyError`.
|
|
125
|
+
"""
|
|
126
|
+
kid, key = _keyring_for(keyring, secret_key).signing_key()
|
|
127
|
+
seal = PanoptiAttestationSeal(
|
|
128
|
+
seal_id=f"seal_{uuid.uuid4().hex[:16]}",
|
|
129
|
+
merkle_root=merkle_root,
|
|
130
|
+
policy_verdict="COMPLIANT" if is_compliant else "VIOLATION",
|
|
131
|
+
confidence_floor=confidence_floor,
|
|
132
|
+
loop_status="LOOP_DETECTED" if loop_detected else "CLEAN",
|
|
133
|
+
tool_count=tool_count,
|
|
134
|
+
timestamp_iso=datetime.now(UTC).isoformat(),
|
|
135
|
+
signature="",
|
|
136
|
+
key_id=kid,
|
|
137
|
+
version=SEAL_VERSION,
|
|
138
|
+
)
|
|
139
|
+
seal.signature = cls._sign(key, seal)
|
|
140
|
+
return seal
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def verify_seal(
|
|
144
|
+
cls,
|
|
145
|
+
seal: PanoptiAttestationSeal,
|
|
146
|
+
secret_key: str | bytes | None = None,
|
|
147
|
+
*,
|
|
148
|
+
keyring: Keyring | None = None,
|
|
149
|
+
allow_legacy: bool = False,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""True only if the seal was signed by a known, unrevoked key and is unmodified.
|
|
152
|
+
|
|
153
|
+
Fails closed: an unknown or revoked key id, a missing key or any modified field
|
|
154
|
+
returns False. Version 1 seals (no key id) verify only with ``allow_legacy=True``
|
|
155
|
+
and an explicit, non-public ``secret_key``.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
ring = _keyring_for(keyring, secret_key)
|
|
159
|
+
except AttestationKeyError:
|
|
160
|
+
return False
|
|
161
|
+
if seal.version == "1" or seal.key_id is None:
|
|
162
|
+
if not allow_legacy or secret_key is None:
|
|
163
|
+
return False
|
|
164
|
+
key = ring.verification_key("explicit")
|
|
165
|
+
if key is None:
|
|
166
|
+
return False
|
|
167
|
+
expected = hmac.new(key, cls._v1_payload(seal).encode(), hashlib.sha256).hexdigest()
|
|
168
|
+
return hmac.compare_digest(seal.signature, expected)
|
|
169
|
+
if seal.version != SEAL_VERSION:
|
|
170
|
+
return False
|
|
171
|
+
kid = seal.key_id if keyring is not None or secret_key is None else "explicit"
|
|
172
|
+
key = ring.verification_key(kid)
|
|
173
|
+
if key is None:
|
|
174
|
+
return False
|
|
175
|
+
return hmac.compare_digest(seal.signature, cls._sign(key, seal))
|
panopti/attest_keys.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Signing keys for attestation seals: a keyring with ids, rotation and revocation.
|
|
2
|
+
|
|
3
|
+
Configuration (environment)::
|
|
4
|
+
|
|
5
|
+
AGENTWATCH_ATTEST_KEYS kid=<base64 or hex key>,kid2=... (one per line also works)
|
|
6
|
+
AGENTWATCH_ATTEST_ACTIVE_KID the kid new seals are signed with (default: the last listed)
|
|
7
|
+
AGENTWATCH_ATTEST_REVOKED kids that must no longer verify (comma separated)
|
|
8
|
+
AGENTWATCH_ATTEST_DEV_KEY=1 development only: an ephemeral random key for this process
|
|
9
|
+
|
|
10
|
+
There is no built-in key. A key must be at least 32 bytes and must not be a known public
|
|
11
|
+
default; the dev key is refused in production and cloud mode. Key material is never
|
|
12
|
+
logged or included in exceptions.
|
|
13
|
+
|
|
14
|
+
Generate a key::
|
|
15
|
+
|
|
16
|
+
python -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import binascii
|
|
23
|
+
import hmac
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import secrets
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
MIN_KEY_BYTES = 32
|
|
33
|
+
# Keys that shipped in source code; anyone can forge seals with them.
|
|
34
|
+
KNOWN_PUBLIC_KEYS = frozenset({b"panopti-seal-default-key", b"panopti-demo-hmac-key"})
|
|
35
|
+
_KID = re.compile(r"^[A-Za-z0-9_.\-]{1,64}$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AttestationKeyError(ValueError):
|
|
39
|
+
"""The signing-key configuration is missing or unsafe. The message never contains key material."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def decode_key(text: str) -> bytes:
|
|
43
|
+
"""A key given as base64 (preferred) or hex."""
|
|
44
|
+
t = text.strip()
|
|
45
|
+
if re.fullmatch(r"[0-9a-fA-F]+", t) and len(t) % 2 == 0 and len(t) >= 2 * MIN_KEY_BYTES:
|
|
46
|
+
return bytes.fromhex(t)
|
|
47
|
+
try:
|
|
48
|
+
return base64.b64decode(t, validate=True)
|
|
49
|
+
except (binascii.Error, ValueError) as exc:
|
|
50
|
+
raise AttestationKeyError("attestation key is neither valid base64 nor hex") from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_key(kid: str, key: bytes) -> None:
|
|
54
|
+
if not _KID.match(kid):
|
|
55
|
+
raise AttestationKeyError(f"invalid key id {kid!r}")
|
|
56
|
+
if any(hmac.compare_digest(key, k) for k in KNOWN_PUBLIC_KEYS):
|
|
57
|
+
raise AttestationKeyError(
|
|
58
|
+
f"key {kid!r} is a known public default key; anyone could forge seals with it"
|
|
59
|
+
)
|
|
60
|
+
if len(key) < MIN_KEY_BYTES:
|
|
61
|
+
raise AttestationKeyError(
|
|
62
|
+
f"key {kid!r} is {len(key)} bytes; at least {MIN_KEY_BYTES} are required"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_production() -> bool:
|
|
67
|
+
env = (os.environ.get("AGENTWATCH_ENV") or "").strip().lower()
|
|
68
|
+
cloud = (os.environ.get("AGENTWATCH_CLOUD_MODE") or "").strip().lower() in (
|
|
69
|
+
"1",
|
|
70
|
+
"true",
|
|
71
|
+
"yes",
|
|
72
|
+
"on",
|
|
73
|
+
)
|
|
74
|
+
return env in ("production", "prod") or cloud
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Keyring:
|
|
79
|
+
keys: dict[str, bytes] = field(default_factory=dict)
|
|
80
|
+
active_kid: str | None = None
|
|
81
|
+
revoked: frozenset[str] = frozenset()
|
|
82
|
+
ephemeral: bool = False # the development-only random key
|
|
83
|
+
|
|
84
|
+
def add(self, kid: str, key: bytes, *, activate: bool = True) -> None:
|
|
85
|
+
check_key(kid, key)
|
|
86
|
+
self.keys[kid] = key
|
|
87
|
+
if activate or self.active_kid is None:
|
|
88
|
+
self.active_kid = kid
|
|
89
|
+
|
|
90
|
+
def signing_key(self) -> tuple[str, bytes]:
|
|
91
|
+
if not self.active_kid or self.active_kid not in self.keys:
|
|
92
|
+
raise AttestationKeyError("no active attestation signing key is configured")
|
|
93
|
+
if self.active_kid in self.revoked:
|
|
94
|
+
raise AttestationKeyError(
|
|
95
|
+
f"the active key {self.active_kid!r} is revoked; activate another key"
|
|
96
|
+
)
|
|
97
|
+
return self.active_kid, self.keys[self.active_kid]
|
|
98
|
+
|
|
99
|
+
def verification_key(self, kid: str) -> bytes | None:
|
|
100
|
+
"""The key for ``kid``, or None if it is unknown or revoked (verification then fails)."""
|
|
101
|
+
if kid in self.revoked:
|
|
102
|
+
return None
|
|
103
|
+
return self.keys.get(kid)
|
|
104
|
+
|
|
105
|
+
def describe(self) -> dict[str, object]:
|
|
106
|
+
"""Safe summary for diagnostics (no key material)."""
|
|
107
|
+
return {
|
|
108
|
+
"kids": sorted(self.keys),
|
|
109
|
+
"active_kid": self.active_kid,
|
|
110
|
+
"revoked": sorted(self.revoked),
|
|
111
|
+
"ephemeral": self.ephemeral,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def parse_keys(raw: str | None) -> dict[str, bytes]:
|
|
116
|
+
out: dict[str, bytes] = {}
|
|
117
|
+
if not raw:
|
|
118
|
+
return out
|
|
119
|
+
for part in re.split(r"[,\n]", raw):
|
|
120
|
+
part = part.strip()
|
|
121
|
+
if not part:
|
|
122
|
+
continue
|
|
123
|
+
kid, sep, value = part.partition("=")
|
|
124
|
+
kid = kid.strip()
|
|
125
|
+
if not sep or not value.strip():
|
|
126
|
+
raise AttestationKeyError(
|
|
127
|
+
f"AGENTWATCH_ATTEST_KEYS: entry for {kid!r} must be kid=<key>"
|
|
128
|
+
)
|
|
129
|
+
if kid in out:
|
|
130
|
+
raise AttestationKeyError(f"AGENTWATCH_ATTEST_KEYS: key id {kid!r} is listed twice")
|
|
131
|
+
key = decode_key(value)
|
|
132
|
+
check_key(kid, key)
|
|
133
|
+
out[kid] = key
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def keyring_from_env(environ: dict[str, str] | None = None) -> Keyring:
|
|
138
|
+
"""The configured keyring. Raises :class:`AttestationKeyError` if it is missing or unsafe."""
|
|
139
|
+
env = os.environ if environ is None else environ
|
|
140
|
+
keys = parse_keys(env.get("AGENTWATCH_ATTEST_KEYS"))
|
|
141
|
+
revoked = frozenset(
|
|
142
|
+
k.strip() for k in (env.get("AGENTWATCH_ATTEST_REVOKED") or "").split(",") if k.strip()
|
|
143
|
+
)
|
|
144
|
+
ring = Keyring(keys=keys, revoked=revoked)
|
|
145
|
+
if keys:
|
|
146
|
+
active = (env.get("AGENTWATCH_ATTEST_ACTIVE_KID") or "").strip() or list(keys)[-1]
|
|
147
|
+
if active not in keys:
|
|
148
|
+
raise AttestationKeyError(
|
|
149
|
+
f"AGENTWATCH_ATTEST_ACTIVE_KID {active!r} is not in AGENTWATCH_ATTEST_KEYS"
|
|
150
|
+
)
|
|
151
|
+
ring.active_kid = active
|
|
152
|
+
return ring
|
|
153
|
+
if (env.get("AGENTWATCH_ATTEST_DEV_KEY") or "").strip().lower() in ("1", "true", "yes", "on"):
|
|
154
|
+
prod = (env.get("AGENTWATCH_ENV") or "").strip().lower() in ("production", "prod") or (
|
|
155
|
+
env.get("AGENTWATCH_CLOUD_MODE") or ""
|
|
156
|
+
).strip().lower() in ("1", "true", "yes", "on")
|
|
157
|
+
if prod:
|
|
158
|
+
raise AttestationKeyError(
|
|
159
|
+
"AGENTWATCH_ATTEST_DEV_KEY is development-only and is refused in production/cloud mode"
|
|
160
|
+
)
|
|
161
|
+
logger.warning(
|
|
162
|
+
"attestation: using an ephemeral development key; seals will not verify in another process"
|
|
163
|
+
)
|
|
164
|
+
ring.keys["dev-ephemeral"] = secrets.token_bytes(MIN_KEY_BYTES)
|
|
165
|
+
ring.active_kid = "dev-ephemeral"
|
|
166
|
+
ring.ephemeral = True
|
|
167
|
+
return ring
|
|
168
|
+
raise AttestationKeyError(
|
|
169
|
+
"no attestation signing key configured: set AGENTWATCH_ATTEST_KEYS (see docs/ATTESTATION_KEYS.md); "
|
|
170
|
+
"for local development only, AGENTWATCH_ATTEST_DEV_KEY=1"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def check_startup(environ: dict[str, str] | None = None) -> dict[str, object] | None:
|
|
175
|
+
"""Startup gate for servers: in production/cloud mode, refuse unsafe attestation config.
|
|
176
|
+
|
|
177
|
+
Returns a key-free summary of the keyring when one is configured, None when
|
|
178
|
+
attestation is neither configured nor required. Raises :class:`AttestationKeyError`
|
|
179
|
+
(the server must not start) when, in production/cloud mode, keys are configured but
|
|
180
|
+
weak/public/malformed, the development key is requested, or
|
|
181
|
+
AGENTWATCH_REQUIRE_ATTESTATION=1 and no key is configured. Outside production, a
|
|
182
|
+
broken configuration is still an error; an absent one is not.
|
|
183
|
+
"""
|
|
184
|
+
env = os.environ if environ is None else environ
|
|
185
|
+
prod = (env.get("AGENTWATCH_ENV") or "").strip().lower() in ("production", "prod") or (
|
|
186
|
+
env.get("AGENTWATCH_CLOUD_MODE") or ""
|
|
187
|
+
).strip().lower() in ("1", "true", "yes", "on")
|
|
188
|
+
configured = bool((env.get("AGENTWATCH_ATTEST_KEYS") or "").strip())
|
|
189
|
+
dev = (env.get("AGENTWATCH_ATTEST_DEV_KEY") or "").strip().lower() in ("1", "true", "yes", "on")
|
|
190
|
+
required = (env.get("AGENTWATCH_REQUIRE_ATTESTATION") or "").strip().lower() in (
|
|
191
|
+
"1",
|
|
192
|
+
"true",
|
|
193
|
+
"yes",
|
|
194
|
+
"on",
|
|
195
|
+
)
|
|
196
|
+
if not (configured or dev or required):
|
|
197
|
+
return None
|
|
198
|
+
if prod and required and not configured:
|
|
199
|
+
raise AttestationKeyError(
|
|
200
|
+
"AGENTWATCH_REQUIRE_ATTESTATION=1 but AGENTWATCH_ATTEST_KEYS is not set"
|
|
201
|
+
)
|
|
202
|
+
return keyring_from_env(env).describe()
|