actshield 0.9.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.
- actshield/__init__.py +439 -0
- actshield/__main__.py +46 -0
- actshield/agents/__init__.py +12 -0
- actshield/agents/agent.py +200 -0
- actshield/agents/identity.py +114 -0
- actshield/api/__init__.py +34 -0
- actshield/api/models.py +198 -0
- actshield/api/server.py +669 -0
- actshield/api/service.py +599 -0
- actshield/approval/__init__.py +15 -0
- actshield/approval/approval.py +164 -0
- actshield/cli/__init__.py +8 -0
- actshield/cli/graph.py +49 -0
- actshield/cli/main.py +802 -0
- actshield/cli/shell.py +406 -0
- actshield/cli/theme.py +110 -0
- actshield/client.py +757 -0
- actshield/config.py +88 -0
- actshield/context/__init__.py +14 -0
- actshield/context/context.py +161 -0
- actshield/context/provenance.py +108 -0
- actshield/context/taint.py +48 -0
- actshield/decisions/decision.py +74 -0
- actshield/delegation/__init__.py +11 -0
- actshield/delegation/authority.py +63 -0
- actshield/delegation/delegation.py +180 -0
- actshield/demo.py +139 -0
- actshield/doctor.py +94 -0
- actshield/drift/__init__.py +27 -0
- actshield/drift/drift_engine.py +149 -0
- actshield/drift/drift_models.py +63 -0
- actshield/evaluation/__init__.py +1 -0
- actshield/evaluation/cases/__init__.py +1 -0
- actshield/evaluation/eval_suite.py +254 -0
- actshield/forensics/__init__.py +53 -0
- actshield/forensics/access_graph.py +216 -0
- actshield/forensics/authority_graph.py +186 -0
- actshield/forensics/delegation_graph.py +119 -0
- actshield/forensics/explanation.py +254 -0
- actshield/forensics/models.py +488 -0
- actshield/forensics/queries.py +367 -0
- actshield/forensics/serializers.py +68 -0
- actshield/forensics/service.py +310 -0
- actshield/forensics/snapshots.py +174 -0
- actshield/gates/__init__.py +13 -0
- actshield/gates/gate_models.py +46 -0
- actshield/gates/security_gate.py +191 -0
- actshield/gateway/__init__.py +13 -0
- actshield/gateway/http.py +191 -0
- actshield/gateway/mcp.py +292 -0
- actshield/incidents/__init__.py +25 -0
- actshield/incidents/incident.py +94 -0
- actshield/incidents/incident_engine.py +163 -0
- actshield/incidents/incident_rules.py +116 -0
- actshield/incidents/incident_state.py +58 -0
- actshield/integrations/__init__.py +24 -0
- actshield/integrations/ai_secura.py +169 -0
- actshield/integrations/ai_secura_analysis.py +236 -0
- actshield/integrations/apiris.py +122 -0
- actshield/integrations/few_shot.py +104 -0
- actshield/integrations/ollama_adapter.py +538 -0
- actshield/integrations/prompts/__init__.py +1 -0
- actshield/integrations/prompts/examples/__init__.py +1 -0
- actshield/integrations/security_packet.py +82 -0
- actshield/llm_config.py +89 -0
- actshield/offensive/__init__.py +95 -0
- actshield/offensive/adaptive/__init__.py +22 -0
- actshield/offensive/adaptive/adaptive_engine.py +165 -0
- actshield/offensive/adaptive/attack_selector.py +51 -0
- actshield/offensive/adaptive/boundary_search.py +99 -0
- actshield/offensive/adaptive/campaign_memory.py +123 -0
- actshield/offensive/adaptive/feedback.py +84 -0
- actshield/offensive/adaptive/mutation_planner.py +57 -0
- actshield/offensive/attack.py +78 -0
- actshield/offensive/campaign.py +219 -0
- actshield/offensive/corpus.py +155 -0
- actshield/offensive/engine.py +466 -0
- actshield/offensive/evaluator.py +116 -0
- actshield/offensive/evidence.py +63 -0
- actshield/offensive/generator.py +128 -0
- actshield/offensive/mutation.py +101 -0
- actshield/offensive/results.py +77 -0
- actshield/offensive/safety.py +80 -0
- actshield/offensive/strategies/__init__.py +22 -0
- actshield/offensive/strategies/authority_escalation.py +100 -0
- actshield/offensive/strategies/base.py +34 -0
- actshield/offensive/strategies/context_manipulation.py +100 -0
- actshield/offensive/strategies/data_exfiltration.py +100 -0
- actshield/offensive/strategies/delegation_attacks.py +100 -0
- actshield/offensive/strategies/mcp_attacks.py +100 -0
- actshield/offensive/strategies/prompt_injection.py +100 -0
- actshield/offensive/strategies/tool_poisoning.py +100 -0
- actshield/offensive/target.py +113 -0
- actshield/offensive/vulnerable_target.py +121 -0
- actshield/persistence/__init__.py +18 -0
- actshield/persistence/siem.py +102 -0
- actshield/persistence/storage.py +240 -0
- actshield/policy/__init__.py +12 -0
- actshield/policy/evaluator.py +294 -0
- actshield/policy/intent.py +113 -0
- actshield/policy/policy.py +48 -0
- actshield/posture/__init__.py +26 -0
- actshield/posture/posture_engine.py +186 -0
- actshield/posture/posture_models.py +152 -0
- actshield/posture/posture_rules.py +137 -0
- actshield/providers/__init__.py +32 -0
- actshield/providers/anthropic_provider.py +146 -0
- actshield/providers/base.py +160 -0
- actshield/providers/gemini_provider.py +133 -0
- actshield/providers/null_provider.py +40 -0
- actshield/providers/ollama_provider.py +135 -0
- actshield/providers/openai_provider.py +142 -0
- actshield/providers/registry.py +244 -0
- actshield/py.typed +0 -0
- actshield/response/__init__.py +14 -0
- actshield/response/response_actions.py +39 -0
- actshield/response/response_audit.py +32 -0
- actshield/response/response_engine.py +139 -0
- actshield/risk/models.py +104 -0
- actshield/serve.py +117 -0
- actshield/static/404.html +1 -0
- actshield/static/__next.__PAGE__.txt +24 -0
- actshield/static/__next._full.txt +20 -0
- actshield/static/__next._tree.txt +4 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_buildManifest.js +11 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_clientMiddlewareManifest.js +1 -0
- actshield/static/_next/static/Yp_swxN-C9UoCRNOwkYnY/_ssgManifest.js +1 -0
- actshield/static/_next/static/chunks/09kny2eqjc2or.js +1 -0
- actshield/static/_next/static/chunks/0c1zba1u9whdp.js +1 -0
- actshield/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- actshield/static/_next/static/chunks/0dbjjeg01x25o.js +1 -0
- actshield/static/_next/static/chunks/0duobs55ne238.js +1 -0
- actshield/static/_next/static/chunks/0iaz41l6068e4.js +1 -0
- actshield/static/_next/static/chunks/0mjhyv72x2idp.js +1 -0
- actshield/static/_next/static/chunks/0nelpqfu_ciq6.js +1 -0
- actshield/static/_next/static/chunks/0p1bci9ch797v.js +1 -0
- actshield/static/_next/static/chunks/0tld_ygho5tgm.js +1 -0
- actshield/static/_next/static/chunks/0w1-y7oni66j9.js +1 -0
- actshield/static/_next/static/chunks/10537zyl427zj.js +1 -0
- actshield/static/_next/static/chunks/11pc_jkl3aggn.js +1 -0
- actshield/static/_next/static/chunks/16k7nw0b3t8ys.js +1 -0
- actshield/static/_next/static/chunks/19mx3mg6lkumu.js +1 -0
- actshield/static/_next/static/chunks/1_8p47b-kxg2a.js +1 -0
- actshield/static/_next/static/chunks/1dszjozwfqegx.js +1 -0
- actshield/static/_next/static/chunks/1h43jcb90-m2r.js +22 -0
- actshield/static/_next/static/chunks/1ly04cvt_wh_j.js +1 -0
- actshield/static/_next/static/chunks/1odxrypuwuwp_.js +1 -0
- actshield/static/_next/static/chunks/1oes4c35qz5ku.js +1 -0
- actshield/static/_next/static/chunks/1ow883jl4ln-r.js +16 -0
- actshield/static/_next/static/chunks/1po7bna1as8tc.js +1 -0
- actshield/static/_next/static/chunks/1rsh0og8ifkzx.js +1 -0
- actshield/static/_next/static/chunks/1v0lwzv-859z_.js +1 -0
- actshield/static/_next/static/chunks/25iat2epwx25a.js +1 -0
- actshield/static/_next/static/chunks/25uvt4s2f39a4.js +1 -0
- actshield/static/_next/static/chunks/27t_qfc-3_lzs.js +1 -0
- actshield/static/_next/static/chunks/28bkmy8mez7gm.js +1 -0
- actshield/static/_next/static/chunks/2b6brmw_drw1p.js +1 -0
- actshield/static/_next/static/chunks/2fnhtxi4j6vge.js +1 -0
- actshield/static/_next/static/chunks/2p087eftxemg4.js +6 -0
- actshield/static/_next/static/chunks/2s8pao_z8pgtq.js +1 -0
- actshield/static/_next/static/chunks/2yj7ooc2c-zpg.js +1 -0
- actshield/static/_next/static/chunks/3-mrrr8udt5tk.js +1 -0
- actshield/static/_next/static/chunks/310vm2bl3xxpt.js +31 -0
- actshield/static/_next/static/chunks/35mvsv251jx1r.css +3 -0
- actshield/static/_next/static/chunks/370vf0oetlsx9.js +1 -0
- actshield/static/_next/static/chunks/3e-2m72tyo556.js +1 -0
- actshield/static/_next/static/chunks/3fntmmi971322.js +1 -0
- actshield/static/_next/static/chunks/3fzw-g0hycl2c.js +1 -0
- actshield/static/_next/static/chunks/3lopq3lhmcjxg.js +129 -0
- actshield/static/_next/static/chunks/3mpe2jejel0v2.css +1 -0
- actshield/static/_next/static/chunks/3t8o4f2lzr2sf.js +1 -0
- actshield/static/_next/static/chunks/3y-v1neyllne5.js +1 -0
- actshield/static/_next/static/chunks/43oiwy8mx6cs1.js +1 -0
- actshield/static/_next/static/chunks/turbopack-23wzuebcy_lxm.js +1 -0
- actshield/static/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
- actshield/static/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
- actshield/static/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
- actshield/static/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
- actshield/static/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
- actshield/static/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
- actshield/static/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
- actshield/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- actshield/static/_not-found/__next._full.txt +17 -0
- actshield/static/_not-found/__next._not-found/__PAGE__.txt +23 -0
- actshield/static/_not-found/__next._tree.txt +3 -0
- actshield/static/_not-found.html +1 -0
- actshield/static/_not-found.txt +17 -0
- actshield/static/access/matrix/__next._full.txt +24 -0
- actshield/static/access/matrix/__next._tree.txt +4 -0
- actshield/static/access/matrix/__next.access/matrix/__PAGE__.txt +30 -0
- actshield/static/access/matrix.html +1 -0
- actshield/static/access/matrix.txt +24 -0
- actshield/static/activity/__next._full.txt +22 -0
- actshield/static/activity/__next._tree.txt +4 -0
- actshield/static/activity/__next.activity/__PAGE__.txt +27 -0
- actshield/static/activity.html +1 -0
- actshield/static/activity.txt +22 -0
- actshield/static/agents/__next._full.txt +22 -0
- actshield/static/agents/__next._tree.txt +4 -0
- actshield/static/agents/__next.agents/__PAGE__.txt +27 -0
- actshield/static/agents.html +1 -0
- actshield/static/agents.txt +22 -0
- actshield/static/ai-secura/__next._full.txt +22 -0
- actshield/static/ai-secura/__next._tree.txt +4 -0
- actshield/static/ai-secura/__next.ai-secura/__PAGE__.txt +27 -0
- actshield/static/ai-secura.html +1 -0
- actshield/static/ai-secura.txt +22 -0
- actshield/static/api/__next._full.txt +22 -0
- actshield/static/api/__next._tree.txt +4 -0
- actshield/static/api/__next.api/__PAGE__.txt +27 -0
- actshield/static/api.html +15 -0
- actshield/static/api.txt +22 -0
- actshield/static/apiris/__next._full.txt +22 -0
- actshield/static/apiris/__next._tree.txt +4 -0
- actshield/static/apiris/__next.apiris/__PAGE__.txt +27 -0
- actshield/static/apiris.html +1 -0
- actshield/static/apiris.txt +22 -0
- actshield/static/attack-graph/__next._full.txt +23 -0
- actshield/static/attack-graph/__next._tree.txt +5 -0
- actshield/static/attack-graph/__next.attack-graph/__PAGE__.txt +28 -0
- actshield/static/attack-graph.html +2 -0
- actshield/static/attack-graph.txt +23 -0
- actshield/static/campaigns/__next._full.txt +22 -0
- actshield/static/campaigns/__next._tree.txt +4 -0
- actshield/static/campaigns/__next.campaigns/__PAGE__.txt +27 -0
- actshield/static/campaigns.html +1 -0
- actshield/static/campaigns.txt +22 -0
- actshield/static/context/__next._full.txt +22 -0
- actshield/static/context/__next._tree.txt +4 -0
- actshield/static/context/__next.context/__PAGE__.txt +27 -0
- actshield/static/context.html +1 -0
- actshield/static/context.txt +22 -0
- actshield/static/dashboard/__next._full.txt +22 -0
- actshield/static/dashboard/__next._tree.txt +4 -0
- actshield/static/dashboard/__next.dashboard/__PAGE__.txt +27 -0
- actshield/static/dashboard.html +1 -0
- actshield/static/dashboard.txt +22 -0
- actshield/static/delegations/__next._full.txt +22 -0
- actshield/static/delegations/__next._tree.txt +4 -0
- actshield/static/delegations/__next.delegations/__PAGE__.txt +27 -0
- actshield/static/delegations.html +1 -0
- actshield/static/delegations.txt +22 -0
- actshield/static/demo/__next._full.txt +22 -0
- actshield/static/demo/__next._tree.txt +4 -0
- actshield/static/demo/__next.demo/__PAGE__.txt +27 -0
- actshield/static/demo.html +1 -0
- actshield/static/demo.txt +22 -0
- actshield/static/drift/__next._full.txt +22 -0
- actshield/static/drift/__next._tree.txt +4 -0
- actshield/static/drift/__next.drift/__PAGE__.txt +27 -0
- actshield/static/drift.html +1 -0
- actshield/static/drift.txt +22 -0
- actshield/static/favicon.ico +0 -0
- actshield/static/file.svg +1 -0
- actshield/static/forensics/__next._full.txt +22 -0
- actshield/static/forensics/__next._tree.txt +4 -0
- actshield/static/forensics/__next.forensics/__PAGE__.txt +27 -0
- actshield/static/forensics.html +1 -0
- actshield/static/forensics.txt +22 -0
- actshield/static/globe.svg +1 -0
- actshield/static/incidents/__next._full.txt +22 -0
- actshield/static/incidents/__next._tree.txt +4 -0
- actshield/static/incidents/__next.incidents/__PAGE__.txt +27 -0
- actshield/static/incidents.html +1 -0
- actshield/static/incidents.txt +22 -0
- actshield/static/index.html +3 -0
- actshield/static/index.txt +20 -0
- actshield/static/logo.png +0 -0
- actshield/static/metrics/__next._full.txt +22 -0
- actshield/static/metrics/__next._tree.txt +4 -0
- actshield/static/metrics/__next.metrics/__PAGE__.txt +27 -0
- actshield/static/metrics.html +1 -0
- actshield/static/metrics.txt +22 -0
- actshield/static/next.svg +1 -0
- actshield/static/offensive/__next._full.txt +22 -0
- actshield/static/offensive/__next._tree.txt +4 -0
- actshield/static/offensive/__next.offensive/__PAGE__.txt +27 -0
- actshield/static/offensive.html +1 -0
- actshield/static/offensive.txt +22 -0
- actshield/static/policies/__next._full.txt +22 -0
- actshield/static/policies/__next._tree.txt +4 -0
- actshield/static/policies/__next.policies/__PAGE__.txt +27 -0
- actshield/static/policies.html +1 -0
- actshield/static/policies.txt +22 -0
- actshield/static/posture/__next._full.txt +22 -0
- actshield/static/posture/__next._tree.txt +4 -0
- actshield/static/posture/__next.posture/__PAGE__.txt +27 -0
- actshield/static/posture.html +1 -0
- actshield/static/posture.txt +22 -0
- actshield/static/regressions/__next._full.txt +22 -0
- actshield/static/regressions/__next._tree.txt +4 -0
- actshield/static/regressions/__next.regressions/__PAGE__.txt +27 -0
- actshield/static/regressions.html +1 -0
- actshield/static/regressions.txt +22 -0
- actshield/static/reports/__next._full.txt +22 -0
- actshield/static/reports/__next._tree.txt +4 -0
- actshield/static/reports/__next.reports/__PAGE__.txt +27 -0
- actshield/static/reports.html +1 -0
- actshield/static/reports.txt +22 -0
- actshield/static/resources/__next._full.txt +22 -0
- actshield/static/resources/__next._tree.txt +4 -0
- actshield/static/resources/__next.resources/__PAGE__.txt +27 -0
- actshield/static/resources.html +1 -0
- actshield/static/resources.txt +22 -0
- actshield/static/security-gates/__next._full.txt +22 -0
- actshield/static/security-gates/__next._tree.txt +4 -0
- actshield/static/security-gates/__next.security-gates/__PAGE__.txt +27 -0
- actshield/static/security-gates.html +1 -0
- actshield/static/security-gates.txt +22 -0
- actshield/static/settings/__next._full.txt +22 -0
- actshield/static/settings/__next._tree.txt +4 -0
- actshield/static/settings/__next.settings/__PAGE__.txt +27 -0
- actshield/static/settings.html +1 -0
- actshield/static/settings.txt +22 -0
- actshield/static/tasks/__next._full.txt +22 -0
- actshield/static/tasks/__next._tree.txt +4 -0
- actshield/static/tasks/__next.tasks/__PAGE__.txt +27 -0
- actshield/static/tasks.html +1 -0
- actshield/static/tasks.txt +22 -0
- actshield/static/threats/__next._full.txt +22 -0
- actshield/static/threats/__next._tree.txt +4 -0
- actshield/static/threats/__next.threats/__PAGE__.txt +27 -0
- actshield/static/threats.html +1 -0
- actshield/static/threats.txt +22 -0
- actshield/static/tools/__next._full.txt +22 -0
- actshield/static/tools/__next._tree.txt +4 -0
- actshield/static/tools/__next.tools/__PAGE__.txt +27 -0
- actshield/static/tools.html +1 -0
- actshield/static/tools.txt +22 -0
- actshield/static/traces/__next._full.txt +22 -0
- actshield/static/traces/__next._tree.txt +4 -0
- actshield/static/traces/__next.traces/__PAGE__.txt +27 -0
- actshield/static/traces.html +1 -0
- actshield/static/traces.txt +22 -0
- actshield/static/vercel.svg +1 -0
- actshield/static/window.svg +1 -0
- actshield/tasks/__init__.py +6 -0
- actshield/tasks/task.py +143 -0
- actshield/threatmodel/__init__.py +70 -0
- actshield/threatmodel/actors.py +210 -0
- actshield/threatmodel/analyzer.py +131 -0
- actshield/threatmodel/assets.py +167 -0
- actshield/threatmodel/boundaries.py +165 -0
- actshield/threatmodel/exporters.py +49 -0
- actshield/threatmodel/mitigations.py +209 -0
- actshield/threatmodel/models.py +80 -0
- actshield/threatmodel/report.py +341 -0
- actshield/threatmodel/scenarios.py +212 -0
- actshield/threatmodel/scoring.py +66 -0
- actshield/threatmodel/threats.py +387 -0
- actshield/tools/__init__.py +20 -0
- actshield/tools/interceptor.py +256 -0
- actshield/tools/tool.py +71 -0
- actshield/tracing/__init__.py +38 -0
- actshield/tracing/correlation.py +173 -0
- actshield/tracing/events.py +75 -0
- actshield/tracing/tracer.py +305 -0
- actshield/watch.py +129 -0
- actshield-0.9.0.dist-info/METADATA +57 -0
- actshield-0.9.0.dist-info/RECORD +366 -0
- actshield-0.9.0.dist-info/WHEEL +5 -0
- actshield-0.9.0.dist-info/entry_points.txt +4 -0
- actshield-0.9.0.dist-info/top_level.txt +2 -0
- agentguard/__init__.py +39 -0
- agentguard/__main__.py +8 -0
- agentguard/py.typed +1 -0
actshield/config.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Configuration module for ActShield SDK."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Pattern
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_SECRET_PATTERNS: List[Pattern[str]] = [
|
|
9
|
+
re.compile(r"(?i)(bearer\s+)[a-zA-Z0-9_\-\.]{15,}"),
|
|
10
|
+
re.compile(r"(?i)(api[_\-]?key|secret|token|password|passwd|auth[_\-]?token)\s*[:=]\s*['\"]?([a-zA-Z0-9_\-\.]{8,})['\"]?"),
|
|
11
|
+
re.compile(r"(?i)(sk-[a-zA-Z0-9]{20,})"),
|
|
12
|
+
re.compile(r"(?i)(ghp_[a-zA-Z0-9]{36})"),
|
|
13
|
+
re.compile(r"(?i)(xox[baprs]-[a-zA-Z0-9\-]{10,})"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
SENSITIVE_KEY_NAMES = {
|
|
17
|
+
"api_key", "apikey", "secret", "password", "passwd", "token",
|
|
18
|
+
"access_token", "auth_token", "private_key", "secret_key",
|
|
19
|
+
"credentials", "authorization"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def default_redact_function(data: Any) -> Any:
|
|
24
|
+
"""Recursively redacts secrets and sensitive key values."""
|
|
25
|
+
if isinstance(data, str):
|
|
26
|
+
result = data
|
|
27
|
+
for pattern in DEFAULT_SECRET_PATTERNS:
|
|
28
|
+
result = pattern.sub(r"\1[REDACTED]", result)
|
|
29
|
+
return result
|
|
30
|
+
elif isinstance(data, dict):
|
|
31
|
+
redacted_dict: Dict[str, Any] = {}
|
|
32
|
+
for k, v in data.items():
|
|
33
|
+
if str(k).lower() in SENSITIVE_KEY_NAMES:
|
|
34
|
+
redacted_dict[k] = "[REDACTED]"
|
|
35
|
+
else:
|
|
36
|
+
redacted_dict[k] = default_redact_function(v)
|
|
37
|
+
return redacted_dict
|
|
38
|
+
elif isinstance(data, list):
|
|
39
|
+
return [default_redact_function(item) for item in data]
|
|
40
|
+
elif isinstance(data, tuple):
|
|
41
|
+
return tuple(default_redact_function(item) for item in data)
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ActShieldConfig(BaseModel):
|
|
46
|
+
"""Runtime configuration for actshield."""
|
|
47
|
+
|
|
48
|
+
app_name: str = Field(default="ActShield", description="Application or service name")
|
|
49
|
+
environment: str = Field(default="production", description="Runtime environment (development, staging, production)")
|
|
50
|
+
enforcement_mode: str = Field(default="STRICT", description="Control plane enforcement mode (STRICT, MONITOR, AUDIT)")
|
|
51
|
+
enforce_monotonic_delegation: bool = Field(
|
|
52
|
+
|
|
53
|
+
default=True,
|
|
54
|
+
description="Whether an agent is strictly forbidden from delegating authority it was not granted"
|
|
55
|
+
)
|
|
56
|
+
block_tainted_sink_access: bool = Field(
|
|
57
|
+
default=True,
|
|
58
|
+
description="Whether tainted untrusted context is blocked from invoking critical sensitive tools"
|
|
59
|
+
)
|
|
60
|
+
default_risk_threshold: float = Field(
|
|
61
|
+
default=0.7,
|
|
62
|
+
ge=0.0,
|
|
63
|
+
le=1.0,
|
|
64
|
+
description="Risk score above which actions are blocked or require human approval"
|
|
65
|
+
)
|
|
66
|
+
redact_secrets: bool = Field(
|
|
67
|
+
default=True,
|
|
68
|
+
description="Whether to sanitize secret patterns from emitted events, context, and trace payloads"
|
|
69
|
+
)
|
|
70
|
+
custom_redactor: Optional[Callable[[Any], Any]] = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
description="Custom redaction callable if specified"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def sanitize(self, data: Any) -> Any:
|
|
76
|
+
"""Sanitize data using configured redactor."""
|
|
77
|
+
if not self.redact_secrets:
|
|
78
|
+
return data
|
|
79
|
+
if self.custom_redactor:
|
|
80
|
+
return self.custom_redactor(data)
|
|
81
|
+
return default_redact_function(data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Backward compatibility alias
|
|
85
|
+
AgentGuardConfig = ActShieldConfig
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Context, Provenance, and Taint tracking package."""
|
|
2
|
+
|
|
3
|
+
from actshield.context.context import Context
|
|
4
|
+
from actshield.context.provenance import ContextSource, Provenance, ProvenanceHop
|
|
5
|
+
from actshield.context.taint import TaintState
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Context",
|
|
9
|
+
"ContextSource",
|
|
10
|
+
"Provenance",
|
|
11
|
+
"ProvenanceHop",
|
|
12
|
+
"TaintState",
|
|
13
|
+
]
|
|
14
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Context container tracking data lineage, provenance, and taint state."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from actshield.context.provenance import ContextSource, ContextTrustLevel, Provenance
|
|
8
|
+
from actshield.context.taint import TaintState
|
|
9
|
+
from actshield.tracing.correlation import generate_id
|
|
10
|
+
from actshield.tracing.events import EventType
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from actshield.client import ActShield
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Context(BaseModel):
|
|
17
|
+
"""Context and data lineage container tracked by actshield."""
|
|
18
|
+
|
|
19
|
+
context_id: str = Field(default_factory=lambda: generate_id("ctx"), description="Unique context ID")
|
|
20
|
+
data: Any = Field(default=None, description="Payload data content or summary")
|
|
21
|
+
source: ContextSource = Field(default=ContextSource.USER, description="Original source")
|
|
22
|
+
source_type: str = Field(default="text", description="MIME type or semantic type")
|
|
23
|
+
source_uri: Optional[str] = Field(default=None, description="URI or locator of context source")
|
|
24
|
+
trust_level: str = Field(default="trusted", description="Trust level assigned at ingestion ('trusted', 'untrusted', 'verified')")
|
|
25
|
+
taint_state: TaintState = Field(default=TaintState.CLEAN, description="Current taint classification ('CLEAN', 'UNTRUSTED', 'TAINTED', 'UNKNOWN')")
|
|
26
|
+
|
|
27
|
+
# Origin & Lineage
|
|
28
|
+
originating_event_id: Optional[str] = Field(default=None, description="Originating event ID")
|
|
29
|
+
originating_agent_id: Optional[str] = Field(default=None, description="Agent that ingested or produced this")
|
|
30
|
+
current_agent_id: Optional[str] = Field(default=None, description="Agent currently holding this context")
|
|
31
|
+
parent_context_id: Optional[str] = Field(default=None, description="Parent context if derived")
|
|
32
|
+
|
|
33
|
+
provenance: Provenance = Field(
|
|
34
|
+
default_factory=Provenance,
|
|
35
|
+
description="Chronological provenance graph"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
timestamp: datetime = Field(
|
|
39
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
40
|
+
description="Creation timestamp"
|
|
41
|
+
)
|
|
42
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Custom metadata tags")
|
|
43
|
+
|
|
44
|
+
def propagate(
|
|
45
|
+
self,
|
|
46
|
+
to_agent_id: str,
|
|
47
|
+
action: str = "propagated",
|
|
48
|
+
new_data: Any = None,
|
|
49
|
+
taint_override: Optional[TaintState] = None,
|
|
50
|
+
guard: Optional["ActShield"] = None,
|
|
51
|
+
) -> "Context":
|
|
52
|
+
"""Propagate or transform this context to another agent, recording the lineage hop.
|
|
53
|
+
|
|
54
|
+
Taint is strictly preserved across agent hops unless explicitly sanitized.
|
|
55
|
+
"""
|
|
56
|
+
new_taint = taint_override or self.taint_state
|
|
57
|
+
new_provenance = self.provenance.model_copy(deep=True)
|
|
58
|
+
|
|
59
|
+
derived_ctx = Context(
|
|
60
|
+
context_id=generate_id("ctx"),
|
|
61
|
+
data=new_data if new_data is not None else self.data,
|
|
62
|
+
source=self.source,
|
|
63
|
+
source_type=self.source_type,
|
|
64
|
+
source_uri=self.source_uri,
|
|
65
|
+
trust_level=self.trust_level,
|
|
66
|
+
taint_state=new_taint,
|
|
67
|
+
originating_event_id=self.originating_event_id,
|
|
68
|
+
originating_agent_id=self.originating_agent_id,
|
|
69
|
+
current_agent_id=to_agent_id,
|
|
70
|
+
parent_context_id=self.context_id,
|
|
71
|
+
provenance=new_provenance,
|
|
72
|
+
metadata=dict(self.metadata),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
event_id = None
|
|
76
|
+
if guard is not None:
|
|
77
|
+
guard.context_registry[derived_ctx.context_id] = derived_ctx
|
|
78
|
+
evt = guard.emit_event(
|
|
79
|
+
event_type=EventType.CONTEXT_PROPAGATED,
|
|
80
|
+
context_id=derived_ctx.context_id,
|
|
81
|
+
agent_id=to_agent_id,
|
|
82
|
+
parent_agent_id=self.current_agent_id,
|
|
83
|
+
payload={
|
|
84
|
+
"parent_context_id": self.context_id,
|
|
85
|
+
"taint_state": derived_ctx.taint_state.value,
|
|
86
|
+
"action": action,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
event_id = evt.event_id
|
|
90
|
+
|
|
91
|
+
derived_ctx.provenance.record_hop(
|
|
92
|
+
agent_id=to_agent_id,
|
|
93
|
+
action=action,
|
|
94
|
+
event_id=event_id,
|
|
95
|
+
details={"from_agent": self.current_agent_id, "taint": new_taint.value},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return derived_ctx
|
|
99
|
+
|
|
100
|
+
def sanitize(
|
|
101
|
+
self,
|
|
102
|
+
sanitizer_name: str,
|
|
103
|
+
reason: str,
|
|
104
|
+
transformed_data: Any = None,
|
|
105
|
+
new_taint: TaintState = TaintState.CLEAN,
|
|
106
|
+
guard: Optional["ActShield"] = None,
|
|
107
|
+
) -> "Context":
|
|
108
|
+
"""Perform an explicit, auditable sanitization operation on this context artifact."""
|
|
109
|
+
previous_taint = self.taint_state.value
|
|
110
|
+
new_provenance = self.provenance.model_copy(deep=True)
|
|
111
|
+
|
|
112
|
+
sanitized_ctx = Context(
|
|
113
|
+
context_id=generate_id("ctx"),
|
|
114
|
+
data=transformed_data if transformed_data is not None else self.data,
|
|
115
|
+
source=self.source,
|
|
116
|
+
source_type=f"sanitized:{self.source_type}",
|
|
117
|
+
source_uri=self.source_uri,
|
|
118
|
+
trust_level="verified",
|
|
119
|
+
taint_state=new_taint,
|
|
120
|
+
originating_event_id=self.originating_event_id,
|
|
121
|
+
originating_agent_id=self.originating_agent_id,
|
|
122
|
+
current_agent_id=self.current_agent_id,
|
|
123
|
+
parent_context_id=self.context_id,
|
|
124
|
+
provenance=new_provenance,
|
|
125
|
+
metadata={
|
|
126
|
+
**self.metadata,
|
|
127
|
+
"sanitized_by": sanitizer_name,
|
|
128
|
+
"sanitization_reason": reason,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
event_id = None
|
|
133
|
+
if guard is not None:
|
|
134
|
+
guard.context_registry[sanitized_ctx.context_id] = sanitized_ctx
|
|
135
|
+
evt = guard.emit_event(
|
|
136
|
+
event_type=EventType.CONTEXT_SANITIZED,
|
|
137
|
+
context_id=sanitized_ctx.context_id,
|
|
138
|
+
agent_id=self.current_agent_id,
|
|
139
|
+
payload={
|
|
140
|
+
"source_context_id": self.context_id,
|
|
141
|
+
"resulting_context_id": sanitized_ctx.context_id,
|
|
142
|
+
"sanitizer_name": sanitizer_name,
|
|
143
|
+
"reason": reason,
|
|
144
|
+
"previous_taint": previous_taint,
|
|
145
|
+
"new_taint": new_taint.value,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
event_id = evt.event_id
|
|
149
|
+
|
|
150
|
+
sanitized_ctx.provenance.record_sanitization(
|
|
151
|
+
sanitizer_name=sanitizer_name,
|
|
152
|
+
reason=reason,
|
|
153
|
+
previous_taint=previous_taint,
|
|
154
|
+
new_taint=new_taint.value,
|
|
155
|
+
event_id=event_id,
|
|
156
|
+
details={"source_context_id": self.context_id},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return sanitized_ctx
|
|
160
|
+
|
|
161
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Data provenance and lineage tracking models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextSource(str, Enum):
|
|
10
|
+
"""Origin sources for context ingested or generated by agents."""
|
|
11
|
+
USER = "user"
|
|
12
|
+
EXTERNAL_MCP = "external_mcp"
|
|
13
|
+
SYSTEM = "system"
|
|
14
|
+
TOOL_OUTPUT = "tool_output"
|
|
15
|
+
AGENT_GENERATED = "agent_generated"
|
|
16
|
+
EXTERNAL_API = "external_api"
|
|
17
|
+
DATABASE = "database"
|
|
18
|
+
FILE_SYSTEM = "file_system"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContextTrustLevel(str, Enum):
|
|
22
|
+
"""Source trust classification (orthogonal to data taint)."""
|
|
23
|
+
TRUSTED = "trusted"
|
|
24
|
+
UNTRUSTED = "untrusted"
|
|
25
|
+
VERIFIED = "verified"
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProvenanceHop(BaseModel):
|
|
30
|
+
"""An individual hop or transformation in the lifecycle of a context artifact."""
|
|
31
|
+
agent_id: Optional[str] = Field(default=None, description="Agent performing the hop/transformation")
|
|
32
|
+
action: str = Field(..., description="Action taken (e.g. 'received', 'transformed', 'delegated', 'injected')")
|
|
33
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
34
|
+
event_id: Optional[str] = Field(default=None, description="Event ID correlated with this hop")
|
|
35
|
+
details: Dict[str, Any] = Field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SanitizationRecord(BaseModel):
|
|
39
|
+
"""Auditable record of an explicit context sanitization transformation."""
|
|
40
|
+
sanitizer_name: str
|
|
41
|
+
reason: str
|
|
42
|
+
previous_taint: str
|
|
43
|
+
new_taint: str
|
|
44
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
45
|
+
event_id: Optional[str] = None
|
|
46
|
+
details: Dict[str, Any] = Field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Provenance(BaseModel):
|
|
50
|
+
"""Complete causal history of a piece of context or data."""
|
|
51
|
+
|
|
52
|
+
source: ContextSource = Field(default=ContextSource.USER, description="Original source type")
|
|
53
|
+
source_uri: Optional[str] = Field(default=None, description="URI or identifier of source (e.g. mcp://weather-server, /var/log)")
|
|
54
|
+
trust_level: str = Field(default="trusted", description="Trust level of the origin source")
|
|
55
|
+
originating_event_id: Optional[str] = Field(default=None, description="Initial event ID where context appeared")
|
|
56
|
+
originating_agent_id: Optional[str] = Field(default=None, description="Initial agent that ingested or produced this context")
|
|
57
|
+
originating_timestamp: datetime = Field(
|
|
58
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
59
|
+
description="Timestamp of origin"
|
|
60
|
+
)
|
|
61
|
+
hops: List[ProvenanceHop] = Field(
|
|
62
|
+
default_factory=list,
|
|
63
|
+
description="Chronological record of agents and actions touching this context"
|
|
64
|
+
)
|
|
65
|
+
sanitizations: List[SanitizationRecord] = Field(
|
|
66
|
+
default_factory=list,
|
|
67
|
+
description="Chronological record of auditable sanitizations"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def record_hop(
|
|
71
|
+
self,
|
|
72
|
+
agent_id: Optional[str],
|
|
73
|
+
action: str,
|
|
74
|
+
event_id: Optional[str] = None,
|
|
75
|
+
details: Optional[Dict[str, Any]] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Record an agent transformation or propagation step."""
|
|
78
|
+
self.hops.append(
|
|
79
|
+
ProvenanceHop(
|
|
80
|
+
agent_id=agent_id,
|
|
81
|
+
action=action,
|
|
82
|
+
timestamp=datetime.now(timezone.utc),
|
|
83
|
+
event_id=event_id,
|
|
84
|
+
details=details or {},
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def record_sanitization(
|
|
89
|
+
self,
|
|
90
|
+
sanitizer_name: str,
|
|
91
|
+
reason: str,
|
|
92
|
+
previous_taint: str,
|
|
93
|
+
new_taint: str,
|
|
94
|
+
event_id: Optional[str] = None,
|
|
95
|
+
details: Optional[Dict[str, Any]] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Record an explicit sanitization event."""
|
|
98
|
+
self.sanitizations.append(
|
|
99
|
+
SanitizationRecord(
|
|
100
|
+
sanitizer_name=sanitizer_name,
|
|
101
|
+
reason=reason,
|
|
102
|
+
previous_taint=previous_taint,
|
|
103
|
+
new_taint=new_taint,
|
|
104
|
+
timestamp=datetime.now(timezone.utc),
|
|
105
|
+
event_id=event_id,
|
|
106
|
+
details=details or {},
|
|
107
|
+
)
|
|
108
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Taint tracking models and propagation rules."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaintState(str, Enum):
|
|
8
|
+
"""Possible taint states for context and data artifacts.
|
|
9
|
+
|
|
10
|
+
Separates origin trust from data taint:
|
|
11
|
+
- CLEAN / TRUSTED: Verified safe content without malicious or unverified directives.
|
|
12
|
+
- UNTRUSTED: Originating from an unverified source, but containing clean/benign data.
|
|
13
|
+
- TAINTED: Ingested or influenced by unverified/adversarial prompt injections or unauthorized directives.
|
|
14
|
+
- UNKNOWN: Unclassified context.
|
|
15
|
+
"""
|
|
16
|
+
CLEAN = "CLEAN"
|
|
17
|
+
TRUSTED = "TRUSTED" # Backward compatibility alias for CLEAN
|
|
18
|
+
UNTRUSTED = "UNTRUSTED"
|
|
19
|
+
TAINTED = "TAINTED"
|
|
20
|
+
UNKNOWN = "UNKNOWN"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_safe(self) -> bool:
|
|
24
|
+
"""Indicates whether this taint level can flow to sensitive sinks without restrictions."""
|
|
25
|
+
return self in (TaintState.CLEAN, TaintState.TRUSTED)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_tainted(self) -> bool:
|
|
29
|
+
"""Indicates whether the data is flagged as carrying untrusted/injected payload."""
|
|
30
|
+
return self == TaintState.TAINTED
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def combine(cls, *states: "TaintState") -> "TaintState":
|
|
34
|
+
"""Combine multiple taint states into a single conservative worst-case taint state."""
|
|
35
|
+
state_list = [s for s in states if s is not None]
|
|
36
|
+
if not state_list:
|
|
37
|
+
return cls.UNKNOWN
|
|
38
|
+
|
|
39
|
+
if cls.TAINTED in state_list:
|
|
40
|
+
return cls.TAINTED
|
|
41
|
+
if cls.UNTRUSTED in state_list:
|
|
42
|
+
return cls.UNTRUSTED
|
|
43
|
+
if cls.UNKNOWN in state_list:
|
|
44
|
+
return cls.UNKNOWN
|
|
45
|
+
if cls.TRUSTED in state_list:
|
|
46
|
+
return cls.TRUSTED
|
|
47
|
+
return cls.CLEAN
|
|
48
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Security enforcement decision models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from actshield.risk.models import RiskFactorBreakdown, RiskLevel
|
|
9
|
+
from actshield.tracing.correlation import generate_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DecisionAction(str, Enum):
|
|
13
|
+
"""Deterministic security enforcement actions."""
|
|
14
|
+
ALLOW = "ALLOW"
|
|
15
|
+
MONITOR = "MONITOR"
|
|
16
|
+
HUMAN_APPROVAL = "HUMAN_APPROVAL"
|
|
17
|
+
QUARANTINE = "QUARANTINE"
|
|
18
|
+
BLOCK = "BLOCK"
|
|
19
|
+
REVOKE = "REVOKE"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SecurityDecision(BaseModel):
|
|
23
|
+
"""A deterministic security decision emitted after policy evaluation."""
|
|
24
|
+
|
|
25
|
+
decision_id: str = Field(default_factory=lambda: generate_id("dec"), description="Unique decision ID")
|
|
26
|
+
action: DecisionAction = Field(..., description="Deterministic enforcement action")
|
|
27
|
+
risk_level: RiskLevel = Field(default=RiskLevel.LOW, description="Assessed risk level")
|
|
28
|
+
risk_score: float = Field(default=0.0, ge=0.0, le=1.0, description="Risk score between 0.0 and 1.0")
|
|
29
|
+
reason_code: str = Field(..., description="Machine-readable reason code")
|
|
30
|
+
explanation: str = Field(..., description="Human-readable explanation of why the decision was rendered")
|
|
31
|
+
trace_id: str = Field(..., description="Trace ID for causal correlation")
|
|
32
|
+
policy_id: Optional[str] = Field(default=None, description="ID of the policy rule that determined this decision")
|
|
33
|
+
|
|
34
|
+
# Causal and structured evaluation details
|
|
35
|
+
evidence_references: List[str] = Field(
|
|
36
|
+
default_factory=list,
|
|
37
|
+
description="References to event_ids, context_ids, or delegation_ids establishing causal evidence"
|
|
38
|
+
)
|
|
39
|
+
influencing_context_ids: List[str] = Field(
|
|
40
|
+
default_factory=list,
|
|
41
|
+
description="IDs of context artifacts that influenced this execution request"
|
|
42
|
+
)
|
|
43
|
+
authority_containment: Dict[str, Any] = Field(
|
|
44
|
+
default_factory=dict,
|
|
45
|
+
description="Structured validation of requested capability vs delegated authority"
|
|
46
|
+
)
|
|
47
|
+
intent_alignment: Dict[str, Any] = Field(
|
|
48
|
+
default_factory=dict,
|
|
49
|
+
description="Structured evaluation of original user intent vs requested action"
|
|
50
|
+
)
|
|
51
|
+
risk_breakdown: Optional[RiskFactorBreakdown] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Explainable breakdown of risk factor dimensions"
|
|
54
|
+
)
|
|
55
|
+
structured_explanation: Dict[str, Any] = Field(
|
|
56
|
+
default_factory=dict,
|
|
57
|
+
description="Structured breakdown for auditing and SIEM reporting"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
evaluated_at: datetime = Field(
|
|
61
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
62
|
+
description="UTC timestamp of evaluation"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_allowed(self) -> bool:
|
|
67
|
+
"""Helper to test if execution is permitted to proceed immediately."""
|
|
68
|
+
return self.action in (DecisionAction.ALLOW, DecisionAction.MONITOR)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_blocked(self) -> bool:
|
|
72
|
+
"""Helper to test if execution was blocked or quarantined."""
|
|
73
|
+
return self.action in (DecisionAction.BLOCK, DecisionAction.QUARANTINE, DecisionAction.REVOKE)
|
|
74
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Authority granting and monotonic delegation validation."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Dict, List, Optional, Set
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from actshield.tracing.correlation import generate_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthorityGrant(BaseModel):
|
|
11
|
+
"""Explicit grant of capabilities from a delegator to a delegate."""
|
|
12
|
+
|
|
13
|
+
grant_id: str = Field(default_factory=lambda: generate_id("grnt"))
|
|
14
|
+
delegator_agent_id: str = Field(..., description="Agent ID yielding authority")
|
|
15
|
+
delegate_agent_id: str = Field(..., description="Agent ID receiving authority")
|
|
16
|
+
granted_capabilities: List[str] = Field(
|
|
17
|
+
default_factory=list,
|
|
18
|
+
description="Explicit list of capability names granted"
|
|
19
|
+
)
|
|
20
|
+
constraints: Dict[str, Any] = Field(
|
|
21
|
+
default_factory=dict,
|
|
22
|
+
description="Optional execution constraints (e.g. rate_limit, max_cost, valid_until)"
|
|
23
|
+
)
|
|
24
|
+
created_at: datetime = Field(
|
|
25
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
26
|
+
description="UTC grant creation timestamp"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def contains_capability(self, capability_name: str) -> bool:
|
|
30
|
+
"""Verify if a specific capability is covered by this grant."""
|
|
31
|
+
if "*" in self.granted_capabilities or capability_name in self.granted_capabilities:
|
|
32
|
+
return True
|
|
33
|
+
for cap in self.granted_capabilities:
|
|
34
|
+
if cap.endswith(".*") and capability_name.startswith(cap[:-2]):
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def validate_monotonic_reduction(
|
|
39
|
+
self,
|
|
40
|
+
delegator_available_capabilities: List[str],
|
|
41
|
+
strict: bool = True,
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""Enforce that granted capabilities are a strict subset of delegator available capabilities."""
|
|
44
|
+
if not strict:
|
|
45
|
+
return True
|
|
46
|
+
if "*" in delegator_available_capabilities:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
delegator_set: Set[str] = set(delegator_available_capabilities)
|
|
50
|
+
for requested_cap in self.granted_capabilities:
|
|
51
|
+
if requested_cap == "*":
|
|
52
|
+
return False # Cannot grant wildcard if delegator doesn't have wildcard
|
|
53
|
+
if requested_cap not in delegator_set:
|
|
54
|
+
# Check wildcard prefix
|
|
55
|
+
matched = False
|
|
56
|
+
for d_cap in delegator_set:
|
|
57
|
+
if d_cap.endswith(".*") and requested_cap.startswith(d_cap[:-2]):
|
|
58
|
+
matched = True
|
|
59
|
+
break
|
|
60
|
+
if not matched:
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
63
|
+
|