agenticdome-python-sdk 1.2.40__py3-none-any.whl → 1.2.43__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.
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/METADATA +5 -6
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/RECORD +18 -16
- agenticdome_sdk/_framework_firewall.py +12 -5
- agenticdome_sdk/catalog_snapshot.json +7 -7
- agenticdome_sdk/client.py +6 -0
- agenticdome_sdk/connect.py +2 -2
- agenticdome_sdk/guided_integration.py +3 -2
- agenticdome_sdk/hook_catalog.py +1 -1
- agenticdome_sdk/lifecycle.py +52 -18
- agenticdome_sdk/mcp_host.py +23 -4
- agenticdome_sdk/onboarding_cli.py +98 -16
- agenticdome_sdk/onboarding_observations.py +74 -0
- agenticdome_sdk/report_scope.py +58 -0
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/WHEEL +0 -0
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/entry_points.txt +0 -0
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/licenses/LICENSE +0 -0
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/licenses/NOTICE +0 -0
- {agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/top_level.txt +0 -0
{agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenticdome-python-sdk
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.43
|
|
4
4
|
Summary: Official Python SDK for AgenticDome security guardrails, DLP, tool authorization, and multi-agent delegation enforcement.
|
|
5
5
|
Author: AgenticDome
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -2845,10 +2845,7 @@ Plus, for every production deployment:
|
|
|
2845
2845
|
|
|
2846
2846
|
Use `VerifiedActionReporter` for executable tool calls, function calls, MCP operations and external side effects that should appear in **Verified Actions**. It is not intended for health checks or every policy lookup.
|
|
2847
2847
|
|
|
2848
|
-
|
|
2849
|
-
export AGENTICDOME_EVIDENCE_API_BASE="https://your-control-plane.example"
|
|
2850
|
-
export AGENTICDOME_EVIDENCE_TOKEN="<portal-token-scoped-to-evidence:write>"
|
|
2851
|
-
```
|
|
2848
|
+
Framework tool wrappers and the MCP host report lifecycle evidence automatically using the existing `AGENTICDOME_API_BASE`, `AGENTICDOME_API_KEY` and `AGENTICDOME_TENANT_ID`. No separate portal credential is required. Upgrade both the SDK and assigned runtime to use automatic reporting. Set `AGENTICDOME_EVIDENCE_ENABLED=false` to opt out.
|
|
2852
2849
|
|
|
2853
2850
|
```python
|
|
2854
2851
|
from agenticdome_sdk import VerifiedActionReporter, verified_action
|
|
@@ -2865,7 +2862,9 @@ def refresh_customer_record(customer_id: str):
|
|
|
2865
2862
|
...
|
|
2866
2863
|
```
|
|
2867
2864
|
|
|
2868
|
-
The reporter uses a bounded, non-blocking queue and retains hashes plus normalized lifecycle metadata only. It never sends raw arguments, results, prompts, source, or credentials. The
|
|
2865
|
+
The reporter uses a bounded, non-blocking queue and retains hashes plus normalized lifecycle metadata only. It never sends raw arguments, results, prompts, source, or credentials. The runtime queues accepted records in persistent Redis; its delivery worker relays them to the owning control plane using the existing per-sidecar service token. Network and storage failures remain pending for retry. Inspect `evidence.stats()` and call `evidence.flush()` before a short-lived process exits. SDK delivery failures and dropped records are counted and never change the protected operation's allow/deny decision.
|
|
2866
|
+
|
|
2867
|
+
Retained outcomes are labeled `sdk_reported`. Policy lookups alone do not prove execution, and streaming acceptance does not prove final completion. Existing `AGENTICDOME_EVIDENCE_API_BASE` plus `AGENTICDOME_EVIDENCE_TOKEN` remains an explicit legacy portal override. Automatic reporting starts with new wrapped calls; it does not backfill historical telemetry.
|
|
2869
2868
|
|
|
2870
2869
|
---
|
|
2871
2870
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
agenticdome_python_sdk-1.2.
|
|
2
|
-
agenticdome_python_sdk-1.2.
|
|
1
|
+
agenticdome_python_sdk-1.2.43.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
+
agenticdome_python_sdk-1.2.43.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
|
|
3
3
|
agenticdome_sdk/__init__.py,sha256=1JY5ELhtp0zg-82xhgt6NxZ7k5CWL5e_Tm6BrsuCFXc,1102
|
|
4
|
-
agenticdome_sdk/_framework_firewall.py,sha256=
|
|
4
|
+
agenticdome_sdk/_framework_firewall.py,sha256=h_g2A5eqHeztTjURRNe-ZsUyFBw0oloLmHHNl3Kgt8Q,32748
|
|
5
5
|
agenticdome_sdk/_mode.py,sha256=XvOgh5sW-FG8S39V483h3uLiFh8wSt14OmYXz_6eDO8,1283
|
|
6
6
|
agenticdome_sdk/action_path_proof.py,sha256=5Zlghy7-LNhtRJM0Unb4NPUASzIsAC0LaWNVFR2yzzo,13497
|
|
7
7
|
agenticdome_sdk/agno.py,sha256=L3yfjTBHW47xzT67a1DsjtwmvrAoYBkbxADpsr85Cp0,60766
|
|
8
8
|
agenticdome_sdk/attestation.py,sha256=ij4cMff7nzLaIS0W02d2WURFcyKe72bcUlSz-dYkaLE,10310
|
|
9
9
|
agenticdome_sdk/autogen.py,sha256=KP4qlIcsNXB-4ZT9D7Slc1xWEeWXRIWkc5HGW0U6k2c,29018
|
|
10
10
|
agenticdome_sdk/aws_bedrock.py,sha256=2zD9j7xi0jna0sMeMf-hKZdc3Y4RPwzj6tMVOUGgIDQ,69581
|
|
11
|
-
agenticdome_sdk/catalog_snapshot.json,sha256=
|
|
11
|
+
agenticdome_sdk/catalog_snapshot.json,sha256=cYOcI2eG8pj5-h_VDZs5Ve3CtpaIHjZ9dhr-lHF-Lp8,955
|
|
12
12
|
agenticdome_sdk/claude.py,sha256=XeY9AUu61SYcJjQVz_AtL_WaN6X4KqP5nprEbbr4Q6A,17456
|
|
13
|
-
agenticdome_sdk/client.py,sha256=
|
|
14
|
-
agenticdome_sdk/connect.py,sha256=
|
|
13
|
+
agenticdome_sdk/client.py,sha256=cHeMnd6Jf5C4gxaPVaOXpNcAGt1r5Ht3GADmbzGlzk8,70207
|
|
14
|
+
agenticdome_sdk/connect.py,sha256=WOh9hEOEy6OmX4zrJEWZPsgYJUbl3PR03ukODe3WC4Q,18864
|
|
15
15
|
agenticdome_sdk/copilot_ir.py,sha256=zT13tOX8xu11yrOtIPMRFVud8CQ81Gu3WxsXxyQ4XT0,17928
|
|
16
16
|
agenticdome_sdk/copilot_ir_collector.cjs,sha256=oxLY8ByONo7Es4YjYiJuRz_A7IKL65Sy486UXnu-4Rs,9681
|
|
17
17
|
agenticdome_sdk/crewai.py,sha256=sSJQ49O7-8kqOAD2_jrsMtMOmBOcJmFkeOv7S3-ayMs,52490
|
|
@@ -20,27 +20,29 @@ agenticdome_sdk/existing_integration.py,sha256=be6xFqe3P9d98YOydHzDeyRSbuAim4bLB
|
|
|
20
20
|
agenticdome_sdk/framework_edits.py,sha256=4BgWHxWKEkex8_DMhjVxjovpUVkrMF7TsCZzlS94mpA,23344
|
|
21
21
|
agenticdome_sdk/generic_python.py,sha256=JtXRcYEZ6_Dgxnnv4QgqeMZR8dFVr8EqFDM-XYQ-GMA,6823
|
|
22
22
|
agenticdome_sdk/google_adk.py,sha256=2TA-tUhY0lOnkGcs1BN9CUEz67ebVX4HJ7mnNoRPcZs,57258
|
|
23
|
-
agenticdome_sdk/guided_integration.py,sha256=
|
|
24
|
-
agenticdome_sdk/hook_catalog.py,sha256=
|
|
23
|
+
agenticdome_sdk/guided_integration.py,sha256=4NBCacAcwEQkVct86G8NGYwrcTNT7xaxXGZ21aPaygY,52449
|
|
24
|
+
agenticdome_sdk/hook_catalog.py,sha256=u72GkY76XjsV8fC1AxUz89AvYWwiOvRFeowR_Dv91hY,29291
|
|
25
25
|
agenticdome_sdk/identity.py,sha256=mrymGgZSVsKJ9THRde54qGmMT7j_uZHmPJ-HtWk7PXE,4635
|
|
26
26
|
agenticdome_sdk/langgraph.py,sha256=FUvF6DSA5aZP3eoBoNeldgGtPrz-dO5xcVmrPPSBEhI,84312
|
|
27
|
-
agenticdome_sdk/lifecycle.py,sha256=
|
|
27
|
+
agenticdome_sdk/lifecycle.py,sha256=obRIbJ9H90TPpfa46pZRTilg9Q8nhLTP5P2bsFbChbI,15210
|
|
28
28
|
agenticdome_sdk/llamaindex.py,sha256=BHplSUIsgMi7k8tBztprzaV9iaOXiljXbdeMRax_2h8,42860
|
|
29
|
-
agenticdome_sdk/mcp_host.py,sha256=
|
|
29
|
+
agenticdome_sdk/mcp_host.py,sha256=4jze8c40o-kcVbX6ZSUpKM9Psyt02KfdrMa7G09o-Z8,70662
|
|
30
30
|
agenticdome_sdk/mcp_http_gateway.py,sha256=XrTgj46ZHIKJ9xS3EQPsac2C9OGUZnrOXomcLdkMk_Y,13629
|
|
31
31
|
agenticdome_sdk/mcp_rehearsal_server.py,sha256=cox4XqUTL8d0gaDzqxDZcEALkaz8otjOjOqHvoUeeCo,2118
|
|
32
32
|
agenticdome_sdk/mcp_verification.py,sha256=WtRhAFVPTBqGOcHNf_db1m4jX9_TO0oxP98XWk4_ISY,14551
|
|
33
33
|
agenticdome_sdk/microsoft_agent_framework.py,sha256=GfIIuk0n-VhsZaTyoB7P6ilpz9bFLD95TWuX0rFSWeU,67219
|
|
34
34
|
agenticdome_sdk/microsoft_ai_foundry.py,sha256=YlO968s1dbAYcaEqhqufL92NFR66GuTtngnCv7-1mYc,56911
|
|
35
|
-
agenticdome_sdk/onboarding_cli.py,sha256=
|
|
35
|
+
agenticdome_sdk/onboarding_cli.py,sha256=8HK3xQ03Vp-SWZ_fZmxBXsgeZVKeUpFCZj-LmR6YbNI,169693
|
|
36
|
+
agenticdome_sdk/onboarding_observations.py,sha256=M4UWdpYipMGi6Nw_eP7PvMLZsAv5UpXRYK0qR28qX6o,4498
|
|
36
37
|
agenticdome_sdk/openai_agents.py,sha256=0z-7FDBIG4usxnILpan6yjQHF4zXIzm9YLsh3-bW8Lw,57678
|
|
37
38
|
agenticdome_sdk/outcomes.py,sha256=jadWDzQaZXrHNvpPme8lQY0jihYLCoM3s4CihAq0JRo,2507
|
|
38
39
|
agenticdome_sdk/proof.py,sha256=ECKc14xSE0V1HHmcpZjfVz8-Q2H_fooah3BfdblFV8M,2878
|
|
39
40
|
agenticdome_sdk/pydantic.py,sha256=WKb8X0Fgydfid-ep8E-M8Yslz2pWcCP6w7n_aBxt1rI,54961
|
|
41
|
+
agenticdome_sdk/report_scope.py,sha256=UVBkM6c_8ZUSdhcSyT3VMcravj5HLGrPZjlgdFanh5A,2939
|
|
40
42
|
agenticdome_sdk/simulation.py,sha256=UCbZy3u9AzfxQCfY38SCV3CxGxhinV9EA4aHMPlK4mc,8426
|
|
41
43
|
agenticdome_sdk/smolagents.py,sha256=wjMA1HVAQ-9wRNM38Jr9wlCpKMhF-9vxtTk6R-kwQhs,19094
|
|
42
|
-
agenticdome_python_sdk-1.2.
|
|
43
|
-
agenticdome_python_sdk-1.2.
|
|
44
|
-
agenticdome_python_sdk-1.2.
|
|
45
|
-
agenticdome_python_sdk-1.2.
|
|
46
|
-
agenticdome_python_sdk-1.2.
|
|
44
|
+
agenticdome_python_sdk-1.2.43.dist-info/METADATA,sha256=a2nyawPHyq2HWXpRIwaxs7yXXKKy0_w8gKMd-NvZU9Q,146163
|
|
45
|
+
agenticdome_python_sdk-1.2.43.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
46
|
+
agenticdome_python_sdk-1.2.43.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
|
|
47
|
+
agenticdome_python_sdk-1.2.43.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
|
|
48
|
+
agenticdome_python_sdk-1.2.43.dist-info/RECORD,,
|
|
@@ -202,7 +202,8 @@ class FrameworkFirewallBase:
|
|
|
202
202
|
timeout=config.timeout_s,
|
|
203
203
|
)
|
|
204
204
|
self.token_store = token_store or build_token_store(config, logger)
|
|
205
|
-
self.lifecycle_reporter = VerifiedActionReporter.from_env(tenant_id=config.tenant_id
|
|
205
|
+
self.lifecycle_reporter = VerifiedActionReporter.from_env(tenant_id=config.tenant_id, api_base=config.api_base, api_key=config.api_key,
|
|
206
|
+
sender=getattr(self.client, "report_action_evidence", None))
|
|
206
207
|
self.denied_error = denied_error
|
|
207
208
|
self.label = label
|
|
208
209
|
self.logger = logger
|
|
@@ -623,14 +624,17 @@ class FrameworkFirewallBase:
|
|
|
623
624
|
decision = await self.aauthorize_tool_call(
|
|
624
625
|
session_id=sid, agent_id=aid, tool_name=tool_name, tool_args=call_args,
|
|
625
626
|
tool_platform=tool_platform, text=f"[{self.label}] {aid} calls {tool_name}",
|
|
627
|
+
policy_context={"chain_id": lifecycle.chain_id, "action_id": lifecycle.action_id, "decision_id": lifecycle.action_id},
|
|
626
628
|
)
|
|
627
629
|
except Exception:
|
|
628
630
|
self.lifecycle_reporter.phase(lifecycle, "authorised", "blocked")
|
|
629
631
|
self.lifecycle_reporter.outcome(lifecycle, "not_attempted")
|
|
630
632
|
raise
|
|
631
|
-
self.lifecycle_reporter.phase(lifecycle, "authorised", "allowed")
|
|
632
|
-
self.lifecycle_reporter.phase(lifecycle, "admitted", "admitted")
|
|
633
633
|
clean = self.sanitized_args(decision, call_args)
|
|
634
|
+
lifecycle = self.lifecycle_reporter.with_arguments(lifecycle, clean)
|
|
635
|
+
if self.verdict(decision) in {"ALLOWED", "REDACTED"} and self.reason(decision) != "fail-open":
|
|
636
|
+
self.lifecycle_reporter.phase(lifecycle, "authorised", "allowed")
|
|
637
|
+
self.lifecycle_reporter.phase(lifecycle, "admitted", "admitted")
|
|
634
638
|
self.lifecycle_reporter.phase(lifecycle, "attempted", "attempted")
|
|
635
639
|
try:
|
|
636
640
|
if kwargs:
|
|
@@ -659,14 +663,17 @@ class FrameworkFirewallBase:
|
|
|
659
663
|
decision = self.authorize_tool_call(
|
|
660
664
|
session_id=sid, agent_id=aid, tool_name=tool_name, tool_args=call_args,
|
|
661
665
|
tool_platform=tool_platform, text=f"[{self.label}] {aid} calls {tool_name}",
|
|
666
|
+
policy_context={"chain_id": lifecycle.chain_id, "action_id": lifecycle.action_id, "decision_id": lifecycle.action_id},
|
|
662
667
|
)
|
|
663
668
|
except Exception:
|
|
664
669
|
self.lifecycle_reporter.phase(lifecycle, "authorised", "blocked")
|
|
665
670
|
self.lifecycle_reporter.outcome(lifecycle, "not_attempted")
|
|
666
671
|
raise
|
|
667
|
-
self.lifecycle_reporter.phase(lifecycle, "authorised", "allowed")
|
|
668
|
-
self.lifecycle_reporter.phase(lifecycle, "admitted", "admitted")
|
|
669
672
|
clean = self.sanitized_args(decision, call_args)
|
|
673
|
+
lifecycle = self.lifecycle_reporter.with_arguments(lifecycle, clean)
|
|
674
|
+
if self.verdict(decision) in {"ALLOWED", "REDACTED"} and self.reason(decision) != "fail-open":
|
|
675
|
+
self.lifecycle_reporter.phase(lifecycle, "authorised", "allowed")
|
|
676
|
+
self.lifecycle_reporter.phase(lifecycle, "admitted", "admitted")
|
|
670
677
|
self.lifecycle_reporter.phase(lifecycle, "attempted", "attempted")
|
|
671
678
|
try:
|
|
672
679
|
if kwargs:
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"schema": "agenticdome.catalog-snapshot.v1",
|
|
3
3
|
"catalog_schema": "agenticdome.hook-catalog.v1",
|
|
4
4
|
"verified_at": "2026-10-02",
|
|
5
|
-
"python_sdk_version": "1.2.
|
|
5
|
+
"python_sdk_version": "1.2.43",
|
|
6
6
|
"published_packages": {
|
|
7
7
|
"agenticdome-python-sdk": {
|
|
8
8
|
"registry": "pypi",
|
|
9
|
-
"version": "1.2.
|
|
10
|
-
"url": "https://pypi.org/project/agenticdome-python-sdk/1.2.
|
|
9
|
+
"version": "1.2.43",
|
|
10
|
+
"url": "https://pypi.org/project/agenticdome-python-sdk/1.2.43/"
|
|
11
11
|
},
|
|
12
12
|
"agenticdome-sdk": {
|
|
13
13
|
"registry": "npm",
|
|
14
|
-
"version": "0.6.
|
|
15
|
-
"url": "https://www.npmjs.com/package/agenticdome-sdk/v/0.6.
|
|
14
|
+
"version": "0.6.14",
|
|
15
|
+
"url": "https://www.npmjs.com/package/agenticdome-sdk/v/0.6.14"
|
|
16
16
|
},
|
|
17
17
|
"agenticdome-openclaw-security": {
|
|
18
18
|
"registry": "npm",
|
|
19
|
-
"version": "1.0.
|
|
20
|
-
"url": "https://www.npmjs.com/package/agenticdome-openclaw-security/v/1.0.
|
|
19
|
+
"version": "1.0.13",
|
|
20
|
+
"url": "https://www.npmjs.com/package/agenticdome-openclaw-security/v/1.0.13"
|
|
21
21
|
},
|
|
22
22
|
"openclaw": {
|
|
23
23
|
"registry": "npm",
|
agenticdome_sdk/client.py
CHANGED
|
@@ -375,6 +375,12 @@ class AgenticDomeClient:
|
|
|
375
375
|
if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
|
|
376
376
|
raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
|
|
377
377
|
|
|
378
|
+
def report_action_evidence(self, kind: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
379
|
+
"""Use the existing tenant SDK transport; runtime durably relays evidence."""
|
|
380
|
+
if kind not in {"events", "outcomes"}:
|
|
381
|
+
raise ValueError("Evidence kind must be events or outcomes")
|
|
382
|
+
return self._request("POST", "/mesh/evidence/" + kind, json_body=payload, timeout=5)
|
|
383
|
+
|
|
378
384
|
def _request(
|
|
379
385
|
self,
|
|
380
386
|
method: str,
|
agenticdome_sdk/connect.py
CHANGED
|
@@ -253,9 +253,9 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
|
|
|
253
253
|
broker_setup["sdk_mode"] = effective_config.get("execution_broker_mode", "existing_sdk_or_environment_default")
|
|
254
254
|
cross_part_review_required = semantic.get("workload_coverage", {}).get("cross_part_flow_proven") is False
|
|
255
255
|
next_action = (
|
|
256
|
-
"Review the generated pull request and run agenticdome verify.
|
|
256
|
+
"Review the generated pull request and run agenticdome verify. Cross-part limitations will be recorded as optional observations in .agenticdome/DISCLAIMER.md; no acknowledgement is required solely for them."
|
|
257
257
|
if args.open_pr else
|
|
258
|
-
"Review the generated patch, apply it on a branch, and run agenticdome verify.
|
|
258
|
+
"Review the generated patch, apply it on a branch, and run agenticdome verify. Cross-part limitations will be recorded as optional observations in .agenticdome/DISCLAIMER.md; no acknowledgement is required solely for them."
|
|
259
259
|
) if cross_part_review_required else (
|
|
260
260
|
"Review the generated pull request and run agenticdome verify."
|
|
261
261
|
if args.open_pr else "Review the generated patch, apply it on a branch, then run agenticdome verify."
|
|
@@ -490,9 +490,10 @@ def preview(root: Path, plan: Optional[Dict[str, Any]] = None, *, target: str =
|
|
|
490
490
|
+ ", A2A " + str(existing.get("a2a_call_candidates", 0))
|
|
491
491
|
+ ", MCP " + str(existing_surfaces.get("mcp", 0))
|
|
492
492
|
+ " (categories may overlap; no customer action was exercised)",
|
|
493
|
-
"-
|
|
494
|
-
"-
|
|
493
|
+
"- Raw semantic action candidates (lower severity may be optional): " + str(semantic_gate.get("action_required", "not assessed")),
|
|
494
|
+
"- Optional review observations: " + str(semantic_gate.get("review_required", "not assessed")),
|
|
495
495
|
"",
|
|
496
|
+
"Optional observations are written to .agenticdome/DISCLAIMER.md on verification; high-severity and required protection/test failures remain blocking.", "",
|
|
496
497
|
"## Framework hooks to attach or verify", "",
|
|
497
498
|
]
|
|
498
499
|
for recommendation in plan.get("hook_recommendations", []):
|
agenticdome_sdk/hook_catalog.py
CHANGED
|
@@ -325,7 +325,7 @@ FRAMEWORK_HOOK_CATALOG: Dict[str, Dict[str, Any]] = {
|
|
|
325
325
|
"adapter_module": "agenticdome-openclaw-security",
|
|
326
326
|
"adapter_class": "OpenClawFirewall",
|
|
327
327
|
"attachment_methods": ["protectedExecute", "sanitizeOutput"],
|
|
328
|
-
"native_hooks": ["before_agent_run", "before_tool_call", "tool_result_persist"],
|
|
328
|
+
"native_hooks": ["before_agent_run", "before_tool_call", "after_tool_call", "tool_result_persist"],
|
|
329
329
|
"docs": "agenticdome-openclaw-security package README",
|
|
330
330
|
},
|
|
331
331
|
"mcp-ts": {
|
agenticdome_sdk/lifecycle.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Privacy-bounded Verified Action lifecycle reporting.
|
|
2
2
|
|
|
3
3
|
The runtime authorization path remains authoritative. This module reports only
|
|
4
|
-
minimized lifecycle evidence through
|
|
4
|
+
minimized lifecycle evidence through the existing tenant runtime credential and
|
|
5
5
|
never sends tool arguments, prompts, responses, credentials, or source code.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -15,9 +15,11 @@ import logging
|
|
|
15
15
|
import os
|
|
16
16
|
import queue
|
|
17
17
|
import threading
|
|
18
|
+
import time
|
|
19
|
+
import urllib.error
|
|
18
20
|
import urllib.request
|
|
19
21
|
import uuid
|
|
20
|
-
from dataclasses import dataclass
|
|
22
|
+
from dataclasses import dataclass, replace
|
|
21
23
|
from datetime import datetime, timezone
|
|
22
24
|
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
23
25
|
|
|
@@ -72,33 +74,46 @@ class VerifiedActionDenied(RuntimeError):
|
|
|
72
74
|
class VerifiedActionReporter:
|
|
73
75
|
"""Non-blocking, bounded reporter for one tenant's Verified Actions.
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
they never weaken or bypass the primary AgenticDome authorization call.
|
|
77
|
+
SDK credentials enable reporting to the assigned runtime by default.
|
|
78
|
+
Explicit portal evidence credentials remain supported for existing callers.
|
|
78
79
|
"""
|
|
79
80
|
|
|
80
|
-
def __init__(self, portal: str = "", access_token: str = "", *, tenant_id: str = "", timeout: float = 5.0, queue_size: int = 256
|
|
81
|
+
def __init__(self, portal: str = "", access_token: str = "", *, tenant_id: str = "", timeout: float = 5.0, queue_size: int = 256,
|
|
82
|
+
runtime_base: str = "", api_key: str = "", sender: Optional[Callable[..., Any]] = None) -> None:
|
|
81
83
|
self.portal = portal.rstrip("/")
|
|
82
84
|
self.access_token = access_token.strip()
|
|
83
85
|
self.tenant_id = tenant_id.strip()
|
|
86
|
+
self.runtime_base = runtime_base.rstrip("/")
|
|
87
|
+
self.api_key = api_key.strip()
|
|
88
|
+
self.sender = sender
|
|
89
|
+
self.runtime_mode = not (self.portal and self.access_token)
|
|
84
90
|
self.timeout = max(1.0, min(float(timeout), 30.0))
|
|
85
91
|
self._queue: "queue.Queue[tuple[str, Dict[str, Any]] | None]" = queue.Queue(maxsize=max(16, min(int(queue_size), 4096)))
|
|
86
92
|
self._worker: Optional[threading.Thread] = None
|
|
87
93
|
self._lock = threading.Lock()
|
|
94
|
+
self._stats = {"queued": 0, "delivered": 0, "failed": 0, "dropped": 0}
|
|
88
95
|
|
|
89
96
|
@classmethod
|
|
90
|
-
def from_env(cls, *, tenant_id: str = "") -> "VerifiedActionReporter":
|
|
97
|
+
def from_env(cls, *, tenant_id: str = "", api_base: str = "", api_key: str = "", sender: Optional[Callable[..., Any]] = None) -> "VerifiedActionReporter":
|
|
98
|
+
if os.getenv("AGENTICDOME_EVIDENCE_ENABLED", "true").lower() in {"0", "false", "off", "no"}:
|
|
99
|
+
return cls()
|
|
91
100
|
return cls(
|
|
92
101
|
os.getenv("AGENTICDOME_EVIDENCE_API_BASE", ""),
|
|
93
102
|
os.getenv("AGENTICDOME_EVIDENCE_TOKEN", ""),
|
|
94
103
|
tenant_id=tenant_id or os.getenv("AGENTICDOME_TENANT_ID", ""),
|
|
95
104
|
timeout=float(os.getenv("AGENTICDOME_EVIDENCE_TIMEOUT_S", "5") or "5"),
|
|
96
105
|
queue_size=int(os.getenv("AGENTICDOME_EVIDENCE_QUEUE_SIZE", "256") or "256"),
|
|
106
|
+
runtime_base=api_base or os.getenv("AGENTICDOME_API_BASE", ""),
|
|
107
|
+
api_key=api_key or os.getenv("AGENTICDOME_API_KEY", ""), sender=sender,
|
|
97
108
|
)
|
|
98
109
|
|
|
99
110
|
@property
|
|
100
111
|
def enabled(self) -> bool:
|
|
101
|
-
return bool(self.
|
|
112
|
+
return bool(self.tenant_id and ((self.portal and self.access_token) or
|
|
113
|
+
(self.runtime_base.startswith(("https://", "http://")) and self.api_key)))
|
|
114
|
+
|
|
115
|
+
def stats(self) -> Dict[str, Any]:
|
|
116
|
+
return dict(self._stats, pending=self._queue.unfinished_tasks, enabled=self.enabled, transport="runtime" if self.runtime_mode else "portal")
|
|
102
117
|
|
|
103
118
|
def new_context(self, *, operation_type: str, tool_name: Optional[str] = None, tool_version: Optional[str] = None,
|
|
104
119
|
arguments: Any = None, destination: Optional[str] = None, chain_id: Optional[str] = None,
|
|
@@ -141,6 +156,10 @@ class VerifiedActionReporter:
|
|
|
141
156
|
}
|
|
142
157
|
return self._enqueue("/api/agentguard/verified-actions/events", payload)
|
|
143
158
|
|
|
159
|
+
def with_arguments(self, context: VerifiedActionContext, arguments: Any) -> VerifiedActionContext:
|
|
160
|
+
"""Keep correlation IDs while hashing the arguments actually admitted for execution."""
|
|
161
|
+
return replace(context, arguments_sha256=_digest(arguments))
|
|
162
|
+
|
|
144
163
|
def outcome(self, context: VerifiedActionContext, outcome_class: str, *, side_effect_reference: Optional[str] = None) -> bool:
|
|
145
164
|
allowed = {"not_attempted", "rejected", "accepted", "succeeded", "partially_succeeded", "failed", "rolled_back", "unknown"}
|
|
146
165
|
outcome = _token(outcome_class, allowed, "unknown")
|
|
@@ -150,8 +169,8 @@ class VerifiedActionReporter:
|
|
|
150
169
|
"chain_id": context.chain_id, "action_id": context.action_id,
|
|
151
170
|
"jti": "sdk_" + uuid.uuid4().hex, "outcome_class": outcome,
|
|
152
171
|
"assurance_level": "sdk_reported", "authorised_action_sha256": context.arguments_sha256,
|
|
153
|
-
"observed_action_sha256": context.arguments_sha256, "destination_sha256": context.destination_sha256,
|
|
154
|
-
"side_effect_ref_sha256": _digest(side_effect_reference), "
|
|
172
|
+
"observed_action_sha256": None if outcome == "not_attempted" else context.arguments_sha256, "destination_sha256": context.destination_sha256,
|
|
173
|
+
"side_effect_ref_sha256": _digest(side_effect_reference), "completed_at": now,
|
|
155
174
|
}
|
|
156
175
|
return self._enqueue("/api/agentguard/verified-actions/outcomes", payload)
|
|
157
176
|
|
|
@@ -162,7 +181,7 @@ class VerifiedActionReporter:
|
|
|
162
181
|
marker = {"_flush_event": completed}
|
|
163
182
|
if not self._enqueue("", marker):
|
|
164
183
|
return False
|
|
165
|
-
return completed.wait(max(0.1, timeout))
|
|
184
|
+
return completed.wait(max(0.1, timeout)) and self._stats["failed"] == 0
|
|
166
185
|
|
|
167
186
|
def _enqueue(self, path: str, payload: Dict[str, Any]) -> bool:
|
|
168
187
|
if not self.enabled:
|
|
@@ -170,8 +189,10 @@ class VerifiedActionReporter:
|
|
|
170
189
|
self._ensure_worker()
|
|
171
190
|
try:
|
|
172
191
|
self._queue.put_nowait((path, payload))
|
|
192
|
+
if path:self._stats["queued"] += 1
|
|
173
193
|
return True
|
|
174
194
|
except queue.Full:
|
|
195
|
+
self._stats["dropped"] += 1
|
|
175
196
|
logger.warning("Verified Action evidence queue is full; authorization behavior is unchanged.")
|
|
176
197
|
return False
|
|
177
198
|
|
|
@@ -193,14 +214,27 @@ class VerifiedActionReporter:
|
|
|
193
214
|
if isinstance(marker, threading.Event):
|
|
194
215
|
marker.set()
|
|
195
216
|
continue
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
217
|
+
for attempt in range(3):
|
|
218
|
+
try:
|
|
219
|
+
if self.runtime_mode and self.sender:
|
|
220
|
+
self.sender(path.rsplit("/", 1)[-1], payload)
|
|
221
|
+
else:
|
|
222
|
+
endpoint = (self.runtime_base + "/mesh/evidence/" + path.rsplit("/", 1)[-1]) if self.runtime_mode else self.portal + path
|
|
223
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json", "X-Tenant-Id": self.tenant_id}
|
|
224
|
+
headers.update({"X-API-Key": self.api_key} if self.runtime_mode else {"Authorization": "Bearer " + self.access_token})
|
|
225
|
+
request = urllib.request.Request(endpoint, data=json.dumps(payload, separators=(",", ":")).encode("utf-8"), method="POST", headers=headers)
|
|
226
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
227
|
+
response.read(1)
|
|
228
|
+
self._stats["delivered"] += 1
|
|
229
|
+
break
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
status = getattr(exc, "code", getattr(exc, "status_code", None))
|
|
232
|
+
if attempt == 2 or status is not None and status not in {429, 500, 502, 503, 504}:
|
|
233
|
+
raise
|
|
234
|
+
time.sleep(.1 * (2 ** attempt))
|
|
202
235
|
except Exception as exc: # evidence must never alter the protected operation
|
|
203
|
-
|
|
236
|
+
self._stats["failed"] += 1
|
|
237
|
+
logger.warning("Verified Action evidence delivery failed; authorization behavior is unchanged (%s)", type(exc).__name__)
|
|
204
238
|
finally:
|
|
205
239
|
self._queue.task_done()
|
|
206
240
|
|
agenticdome_sdk/mcp_host.py
CHANGED
|
@@ -13,6 +13,7 @@ import time
|
|
|
13
13
|
import uuid
|
|
14
14
|
import re
|
|
15
15
|
from collections import defaultdict, deque
|
|
16
|
+
from contextvars import ContextVar
|
|
16
17
|
from dataclasses import dataclass
|
|
17
18
|
from datetime import date
|
|
18
19
|
from inspect import isawaitable
|
|
@@ -23,6 +24,8 @@ from .client import AgenticDomeClient
|
|
|
23
24
|
from ._mode import credentials_or_local_sim
|
|
24
25
|
from .lifecycle import VerifiedActionReporter
|
|
25
26
|
|
|
27
|
+
_lifecycle_decision: ContextVar[Optional[Dict[str, Any]]] = ContextVar("agenticdome_mcp_lifecycle_decision", default=None)
|
|
28
|
+
|
|
26
29
|
try:
|
|
27
30
|
from .exceptions import AgenticDomeHTTPError
|
|
28
31
|
except Exception: # pragma: no cover - compatibility with older package layouts
|
|
@@ -375,7 +378,8 @@ class AgenticDomeMCPHostFirewall:
|
|
|
375
378
|
timeout=self.config.timeout_s,
|
|
376
379
|
)
|
|
377
380
|
self.token_store = token_store or _build_token_store(self.config)
|
|
378
|
-
self.lifecycle_reporter = lifecycle_reporter or VerifiedActionReporter.from_env(tenant_id=self.config.tenant_id
|
|
381
|
+
self.lifecycle_reporter = lifecycle_reporter or VerifiedActionReporter.from_env(tenant_id=self.config.tenant_id,
|
|
382
|
+
api_base=self.config.api_base, api_key=self.config.api_key, sender=getattr(self.client, "report_action_evidence", None))
|
|
379
383
|
self.provider_action_rules = dict(provider_action_rules or {})
|
|
380
384
|
self.resolve_provider_facts = resolve_provider_facts
|
|
381
385
|
self._rate_lock = Lock()
|
|
@@ -975,6 +979,9 @@ class AgenticDomeMCPHostFirewall:
|
|
|
975
979
|
)
|
|
976
980
|
|
|
977
981
|
async def _handle_blocking_decision(self, *, decision: Dict[str, Any], rid: Any, context: Dict[str, Any], method: str, operation: str) -> Optional[Dict[str, Any]]:
|
|
982
|
+
evidence = _lifecycle_decision.get()
|
|
983
|
+
if evidence is not None:
|
|
984
|
+
evidence.update(verdict=self._verdict(decision), reason=self._reason(decision))
|
|
978
985
|
verdict = self._verdict(decision)
|
|
979
986
|
if verdict == "ALLOWED" or (verdict == "REDACTED" and self._sanitized_args(decision, explicit_only=True) is not None):
|
|
980
987
|
return None
|
|
@@ -1433,7 +1440,7 @@ class AgenticDomeMCPHostFirewall:
|
|
|
1433
1440
|
lifecycle = self.lifecycle_reporter.new_context(
|
|
1434
1441
|
operation_type="mcp_operation", tool_name=tool_name, arguments=arguments,
|
|
1435
1442
|
destination=self.config.mcp_server_url or self.config.mcp_server_id,
|
|
1436
|
-
chain_id=self._safe_str(local_context.get("verified_chain_id") or local_context.get("action_chain_id")) or None,
|
|
1443
|
+
chain_id=self._safe_str(local_context.get("verified_chain_id") or local_context.get("action_chain_id") or local_context.get("chain_id")) or None,
|
|
1437
1444
|
action_id=self._safe_str(local_context.get("verified_action_id") or local_context.get("action_id")) or None,
|
|
1438
1445
|
parent_action_id=self._safe_str(local_context.get("parent_action_id")) or None,
|
|
1439
1446
|
initiator_type="human" if self._safe_str(local_context.get("user_id")) else "agent",
|
|
@@ -1441,13 +1448,25 @@ class AgenticDomeMCPHostFirewall:
|
|
|
1441
1448
|
)
|
|
1442
1449
|
self.lifecycle_reporter.phase(lifecycle, "requested", "requested")
|
|
1443
1450
|
|
|
1444
|
-
|
|
1451
|
+
local_context.update(chain_id=lifecycle.chain_id, action_id=lifecycle.action_id, decision_id=lifecycle.action_id)
|
|
1452
|
+
decision_evidence: Dict[str, Any] = {}
|
|
1453
|
+
decision_token = _lifecycle_decision.set(decision_evidence)
|
|
1454
|
+
try:
|
|
1455
|
+
gated = await self.preflight_request(mcp_request=mcp_request, context=local_context)
|
|
1456
|
+
except Exception:
|
|
1457
|
+
self.lifecycle_reporter.outcome(lifecycle, "not_attempted")
|
|
1458
|
+
raise
|
|
1459
|
+
finally:
|
|
1460
|
+
_lifecycle_decision.reset(decision_token)
|
|
1445
1461
|
if isinstance(gated, dict) and "error" in gated:
|
|
1446
1462
|
self.lifecycle_reporter.phase(lifecycle, "authorised", "blocked")
|
|
1447
1463
|
self.lifecycle_reporter.outcome(lifecycle, "not_attempted")
|
|
1448
1464
|
return gated
|
|
1449
1465
|
|
|
1450
|
-
self.
|
|
1466
|
+
admitted_arguments = self._extract_tool_call(gated)[1] if method == "tools/call" else self._params(gated)
|
|
1467
|
+
lifecycle = self.lifecycle_reporter.with_arguments(lifecycle, admitted_arguments)
|
|
1468
|
+
if decision_evidence.get("verdict") in {"ALLOWED", "REDACTED"} and decision_evidence.get("reason") != "fail-open":
|
|
1469
|
+
self.lifecycle_reporter.phase(lifecycle, "authorised", "allowed")
|
|
1451
1470
|
self.lifecycle_reporter.phase(lifecycle, "admitted", "admitted")
|
|
1452
1471
|
self.lifecycle_reporter.phase(lifecycle, "attempted", "attempted")
|
|
1453
1472
|
|
|
@@ -1645,6 +1645,17 @@ def integration_plan(root: Path) -> Dict[str, Any]:
|
|
|
1645
1645
|
hook["required_actions"].append("No candidate use of " + hook["recommended_attachment"]["method"] + " was observed in this selected source; attach it at the real execution path or document the external integration.")
|
|
1646
1646
|
semantic_bypasses = semantic.get("bypass_risks", []) if isinstance(semantic, dict) else []
|
|
1647
1647
|
semantic_reviews = semantic.get("review_findings", []) if isinstance(semantic, dict) else []
|
|
1648
|
+
from .onboarding_observations import POLICY as observations_policy
|
|
1649
|
+
workload_coverage = semantic.get("workload_coverage", {})
|
|
1650
|
+
assessment_complete = (
|
|
1651
|
+
workload_coverage.get("complete", True) is True
|
|
1652
|
+
and all(item.get("severity") in {"high", "medium", "low"} for item in semantic_bypasses)
|
|
1653
|
+
and workload_coverage.get("portal_display_capped") is not True
|
|
1654
|
+
and not any(item in {"attachment_points", "bypass_risks", "review_findings"}
|
|
1655
|
+
for item in workload_coverage.get("result_lists_at_cap", []))
|
|
1656
|
+
and (not workload_coverage.get("selected_parts")
|
|
1657
|
+
or workload_coverage.get("selected_parts") == workload_coverage.get("analyzed_parts"))
|
|
1658
|
+
)
|
|
1648
1659
|
return {
|
|
1649
1660
|
"schema": "agenticdome.integration-plan.v1",
|
|
1650
1661
|
"inspection_report_sha256": _exportable_inspection(report).get("report_sha256"),
|
|
@@ -1684,10 +1695,12 @@ def integration_plan(root: Path) -> Dict[str, Any]:
|
|
|
1684
1695
|
"unresolved_bypasses": len(semantic_bypasses),
|
|
1685
1696
|
"high_severity_bypasses": sum(1 for item in semantic_bypasses if item.get("severity") == "high"),
|
|
1686
1697
|
"review_required": len(semantic_reviews),
|
|
1687
|
-
"
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1698
|
+
"review_policy": "advisory",
|
|
1699
|
+
"assessment_complete": assessment_complete,
|
|
1700
|
+
"onboarding_policy": observations_policy,
|
|
1701
|
+
"production_ready": not any(item.get("severity") == "high" for item in semantic_bypasses)
|
|
1702
|
+
and semantic.get("confidence") in {"high", "partial"}
|
|
1703
|
+
and assessment_complete,
|
|
1691
1704
|
"cross_workload_review_required": semantic.get("workload_coverage", {}).get("cross_part_flow_proven") is False,
|
|
1692
1705
|
"cross_part_edges_observed": int(semantic.get("workload_coverage", {}).get("cross_part_edges", {}).get("observed_count", 0)),
|
|
1693
1706
|
},
|
|
@@ -2498,6 +2511,7 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
|
|
|
2498
2511
|
*, plan: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
|
|
2499
2512
|
from .client import AgenticDomeClient
|
|
2500
2513
|
from .action_path_proof import load_action_path_proof
|
|
2514
|
+
from .onboarding_observations import POLICY as observations_policy, observations_summary
|
|
2501
2515
|
|
|
2502
2516
|
plan = plan or integration_plan(root)
|
|
2503
2517
|
config_path = _agenticdome_dir(root) / "config.json"
|
|
@@ -2548,10 +2562,18 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
|
|
|
2548
2562
|
}
|
|
2549
2563
|
semantic_gate = plan.get("semantic_gate", {})
|
|
2550
2564
|
cross_part_review_required = bool(semantic_gate.get("cross_workload_review_required"))
|
|
2565
|
+
observations = observations_summary(semantic_gate)
|
|
2566
|
+
bypass_rows = plan.get("semantic_analysis", {}).get("bypass_risks", [])
|
|
2567
|
+
if not isinstance(bypass_rows, list) or any(
|
|
2568
|
+
not isinstance(row, dict) or row.get("severity") not in {"high", "medium", "low"}
|
|
2569
|
+
for row in bypass_rows
|
|
2570
|
+
):
|
|
2571
|
+
observations["classification_complete"] = False
|
|
2551
2572
|
semantic_ready = (
|
|
2552
2573
|
semantic_gate.get("confidence") in {"high", "partial"}
|
|
2553
|
-
and
|
|
2554
|
-
and int(semantic_gate.get("
|
|
2574
|
+
and observations["classification_complete"]
|
|
2575
|
+
and int(semantic_gate.get("high_severity_bypasses", 0)) == 0
|
|
2576
|
+
and semantic_gate.get("assessment_complete", True) is True
|
|
2555
2577
|
)
|
|
2556
2578
|
result = {
|
|
2557
2579
|
"schema": "agenticdome.verification-result.v1",
|
|
@@ -2559,6 +2581,8 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
|
|
|
2559
2581
|
"mode": "live_sidecar_fixed_payload" if live else "local_sim_fixed_payload",
|
|
2560
2582
|
"source_upload": False,
|
|
2561
2583
|
"framework_runtime_instantiated": False,
|
|
2584
|
+
"onboarding_policy": observations_policy,
|
|
2585
|
+
"onboarding_observations": observations,
|
|
2562
2586
|
"decision_cases": outcomes,
|
|
2563
2587
|
"static_coverage": plan["coverage"],
|
|
2564
2588
|
"semantic_gate": {
|
|
@@ -2569,10 +2593,13 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
|
|
|
2569
2593
|
"unresolved_bypasses": int(semantic_gate.get("unresolved_bypasses", 0)),
|
|
2570
2594
|
"high_severity_bypasses": int(semantic_gate.get("high_severity_bypasses", 0)),
|
|
2571
2595
|
"review_required": int(semantic_gate.get("review_required", 0)),
|
|
2596
|
+
"review_policy": "advisory",
|
|
2597
|
+
"review_owner": "coding_assistant_or_agenticdome",
|
|
2598
|
+
"assessment_complete": semantic_gate.get("assessment_complete", True) is True,
|
|
2572
2599
|
"passed": semantic_ready,
|
|
2573
2600
|
"cross_part_review_required": cross_part_review_required,
|
|
2574
2601
|
"cross_part_edges_observed": int(semantic_gate.get("cross_part_edges_observed", 0)),
|
|
2575
|
-
"production_ready": semantic_ready
|
|
2602
|
+
"production_ready": semantic_ready,
|
|
2576
2603
|
},
|
|
2577
2604
|
"application_tests": application_tests,
|
|
2578
2605
|
"action_path_proof": load_action_path_proof(root),
|
|
@@ -2580,17 +2607,39 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
|
|
|
2580
2607
|
and not plan["coverage"]["gaps"]
|
|
2581
2608
|
and (not run_tests or semantic_ready)
|
|
2582
2609
|
and (not run_tests or application_tests["passed"] is True),
|
|
2583
|
-
"clarification": "
|
|
2610
|
+
"clarification": "ready=true means required local checks passed, not production activation. Lower-severity static candidates, reviews and cross-part limitations are optional observations in .agenticdome/DISCLAIMER.md. High-severity findings, incomplete source, missing protection, failing tests and runtime proof failures remain blocking. Fixed decision cases do not execute customer actions.",
|
|
2584
2611
|
}
|
|
2585
2612
|
canonical = json.dumps(result, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
2586
2613
|
result["report_sha256"] = hashlib.sha256(canonical).hexdigest()
|
|
2614
|
+
_write_onboarding_disclaimer(root, plan, result)
|
|
2587
2615
|
return (0 if result["ready"] else 2), result
|
|
2588
2616
|
|
|
2589
2617
|
|
|
2618
|
+
def _write_onboarding_disclaimer(root: Path, plan: Dict[str, Any], verification: Dict[str, Any],
|
|
2619
|
+
*, evidence_status: str = "Current local verification") -> None:
|
|
2620
|
+
from .onboarding_observations import FILE, disclaimer_markdown
|
|
2621
|
+
target = root / FILE
|
|
2622
|
+
if target.is_symlink() or not target.resolve().is_relative_to(root.resolve()):
|
|
2623
|
+
raise SystemExit("The disclaimer output is a symlink or leaves this workload; use a regular local output path.")
|
|
2624
|
+
# Reject symlinked ancestors even when they happen to point inside the root.
|
|
2625
|
+
if any(parent.is_symlink() for parent in target.parents if parent != root and root in parent.parents):
|
|
2626
|
+
raise SystemExit("The disclaimer output directory is a symlink; use a regular local output path.")
|
|
2627
|
+
_write_local_text(target, disclaimer_markdown(plan, verification, evidence_status=evidence_status))
|
|
2628
|
+
|
|
2629
|
+
|
|
2590
2630
|
def _reconciliation_markdown(plan: Dict[str, Any], verification: Dict[str, Any], report_digest: str) -> str:
|
|
2591
2631
|
"""Give a bounded, source-free worklist without accepting AI claims as proof."""
|
|
2592
2632
|
semantic = plan.get("semantic_analysis") if isinstance(plan.get("semantic_analysis"), dict) else {}
|
|
2593
2633
|
gate = verification.get("semantic_gate") if isinstance(verification.get("semantic_gate"), dict) else {}
|
|
2634
|
+
from .onboarding_observations import POLICY as observations_policy
|
|
2635
|
+
bypass_rows = semantic.get("bypass_risks", [])
|
|
2636
|
+
known_severities = isinstance(bypass_rows, list) and all(
|
|
2637
|
+
isinstance(row, dict) and row.get("severity") in {"high", "medium", "low"}
|
|
2638
|
+
for row in bypass_rows
|
|
2639
|
+
)
|
|
2640
|
+
optional_policy = (verification.get("onboarding_policy") == observations_policy
|
|
2641
|
+
and verification.get("onboarding_observations", {}).get("classification_complete") is True
|
|
2642
|
+
and known_severities)
|
|
2594
2643
|
|
|
2595
2644
|
def groups(field: str) -> List[Tuple[str, str, int, List[int]]]:
|
|
2596
2645
|
grouped: Dict[Tuple[str, str], List[int]] = {}
|
|
@@ -2598,6 +2647,8 @@ def _reconciliation_markdown(plan: Dict[str, Any], verification: Dict[str, Any],
|
|
|
2598
2647
|
for item in rows[:100]:
|
|
2599
2648
|
if not isinstance(item, dict):
|
|
2600
2649
|
continue
|
|
2650
|
+
if optional_policy and field == "bypass_risks" and item.get("severity") != "high":
|
|
2651
|
+
continue
|
|
2601
2652
|
boundary = str(item.get("boundary") or "unknown")
|
|
2602
2653
|
if boundary not in {"prompt_ingress", "tool_execution", "output_egress"}:
|
|
2603
2654
|
boundary = "unknown"
|
|
@@ -2627,11 +2678,16 @@ def _reconciliation_markdown(plan: Dict[str, Any], verification: Dict[str, Any],
|
|
|
2627
2678
|
"This is a local, source-free worklist for the current validation run. It is not a bypass waiver, a code patch, or live tenant proof.",
|
|
2628
2679
|
"The AI-authored AgenticDome_Integration.md remains an assertion until these checks and real-path tests pass.", "",
|
|
2629
2680
|
f"- AI report SHA-256: `{report_digest}`",
|
|
2630
|
-
f"-
|
|
2631
|
-
f"-
|
|
2681
|
+
f"- Blocking findings: {int(gate.get('high_severity_bypasses' if optional_policy else 'unresolved_bypasses', 0))}",
|
|
2682
|
+
f"- Advisory internal or indirect paths: {int(gate.get('review_required', 0))} (owner: coding assistant or AgenticDome; not a local readiness blocker)",
|
|
2632
2683
|
f"- Semantic confidence: {gate.get('confidence', 'unavailable')}", "",
|
|
2684
|
+
"## Who does what", "",
|
|
2685
|
+
"The coding assistant traces grouped code candidates and runs real-path tests. The customer reviews the change summary, intended routes and deployment location—not scanner line numbers.",
|
|
2686
|
+
"Lower-severity candidates and review notes are optional tenant observations in .agenticdome/DISCLAIMER.md. They do not require an acknowledgement or AgenticDome clearance before onboarding continues." if optional_policy else "If an action-required candidate is already protected in a real-path test, supply a minimal reproduction to AgenticDome. Stop unchanged retries; do not add duplicate guards or waive the finding.",
|
|
2687
|
+
"Pending deployment settings and live proof belong to the service operator and are separate from optional observations.", "",
|
|
2633
2688
|
]
|
|
2634
|
-
|
|
2689
|
+
sections = (("bypass_risks", "Action required"),) if optional_policy else (("bypass_risks", "Action required"), ("review_findings", "Trace and classify"))
|
|
2690
|
+
for field, title in sections:
|
|
2635
2691
|
lines.extend([f"## {title}", ""])
|
|
2636
2692
|
found = groups(field)
|
|
2637
2693
|
if not found:
|
|
@@ -2652,10 +2708,14 @@ def _reconciliation_markdown(plan: Dict[str, Any], verification: Dict[str, Any],
|
|
|
2652
2708
|
lines.append("- The AI report itself says a relevant path was NOT ASSESSED or unprotected. Inspect and protect that path, then update the report; its zero-count rows cannot clear this scope gap.")
|
|
2653
2709
|
if plan.get("coverage", {}).get("gaps"):
|
|
2654
2710
|
lines.append("- Required boundary categories are missing from the inspected workload. Attach and test them before retrying.")
|
|
2655
|
-
if gate.get("
|
|
2711
|
+
if gate.get("assessment_complete") is False or not known_severities:
|
|
2712
|
+
lines.append("- Source assessment or severity classification is incomplete. The coding assistant must resolve that required check; unclassified candidates are not optional observations.")
|
|
2713
|
+
if gate.get("cross_part_review_required") and not optional_policy:
|
|
2656
2714
|
lines.append("- Analysis was split across parts; trace cross-part calls and alternate entry paths. Separate parts are not a proof of guard dominance.")
|
|
2657
2715
|
if tests.get("passed") is not True:
|
|
2658
2716
|
lines.append("- Workload tests did not pass or were not detected. Run the local test command directly to see its output; no test log is uploaded.")
|
|
2717
|
+
if report_scope.get("activation_pending"):
|
|
2718
|
+
lines.append("- Local source assessment and operator activation are separate. Complete the report's exact runtime handoff and safe staging proof before production activation.")
|
|
2659
2719
|
lines.extend([
|
|
2660
2720
|
"- Give these grouped paths to your coding assistant together with the real source. Keep the assistant's changes reviewable and update only the canonical AgenticDome_Integration.md.",
|
|
2661
2721
|
"- Rerun `agenticdome validate-report --report AgenticDome_Integration.md` after code and test changes. Do not edit the JSON evidence by hand.",
|
|
@@ -2688,7 +2748,19 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
|
|
|
2688
2748
|
scope = (quoted.group(1) if quoted else scope_match.group(1).split()[0]).strip().rstrip("/")
|
|
2689
2749
|
if Path(scope).name.casefold() != root.name.casefold() or ".." in Path(scope).parts:
|
|
2690
2750
|
raise SystemExit("The report's Workload scope does not match this workload root. Run from the correct deployable service.")
|
|
2691
|
-
|
|
2751
|
+
# Numbered operator steps are not interception categories. Older reports
|
|
2752
|
+
# without a table heading remain readable, with the original strict counts.
|
|
2753
|
+
inventory_lines = []
|
|
2754
|
+
has_header = bool(re.search(r"^\s*\|\s*Category\s*\|", markdown, re.MULTILINE))
|
|
2755
|
+
in_inventory = not has_header
|
|
2756
|
+
for line in markdown.splitlines():
|
|
2757
|
+
if has_header and not line.lstrip().startswith("|"):
|
|
2758
|
+
in_inventory = False
|
|
2759
|
+
if re.match(r"^\s*\|\s*Category\s*\|", line):
|
|
2760
|
+
in_inventory = True
|
|
2761
|
+
if in_inventory:
|
|
2762
|
+
inventory_lines.append(line)
|
|
2763
|
+
rows = re.findall(r"^\s*\|\s*\*?\*?([1-8])\.([^\r\n]*)", "\n".join(inventory_lines), re.MULTILINE)
|
|
2692
2764
|
if len(rows) != 8 or {number for number, _ in rows} != {str(index) for index in range(1, 9)}:
|
|
2693
2765
|
raise SystemExit("The integration report needs one valid row for each of the eight interception categories.")
|
|
2694
2766
|
reported_unprotected = 0
|
|
@@ -2700,7 +2772,9 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
|
|
|
2700
2772
|
if any(value > 10000 for value in (found, existing, new, unprotected, live)) or existing + new + unprotected != found or live > existing + new:
|
|
2701
2773
|
raise SystemExit("The report counts do not reconcile. Correct the AI integration inventory before validation.")
|
|
2702
2774
|
reported_unprotected += unprotected
|
|
2703
|
-
|
|
2775
|
+
from .report_scope import assessment_scope
|
|
2776
|
+
report_scope = assessment_scope(markdown)
|
|
2777
|
+
scope_incomplete = report_scope["scope_incomplete"]
|
|
2704
2778
|
digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
|
|
2705
2779
|
|
|
2706
2780
|
inspection = _exportable_inspection(inspect_repository(root))
|
|
@@ -2719,6 +2793,7 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
|
|
|
2719
2793
|
local_checks_passed = exit_code == 0 and verification.get("ready") is True
|
|
2720
2794
|
verification["ai_report_scope"] = {
|
|
2721
2795
|
"scope_incomplete": scope_incomplete,
|
|
2796
|
+
"activation_pending": report_scope["activation_pending"],
|
|
2722
2797
|
"reported_unprotected": reported_unprotected,
|
|
2723
2798
|
}
|
|
2724
2799
|
if scope_incomplete or reported_unprotected:
|
|
@@ -2739,18 +2814,25 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
|
|
|
2739
2814
|
_write_json(inspection_path, inspection)
|
|
2740
2815
|
_write_json(verification_path, verification)
|
|
2741
2816
|
_write_local_text(reconciliation_path, _reconciliation_markdown(plan, verification, digest))
|
|
2817
|
+
_write_onboarding_disclaimer(root, plan, verification)
|
|
2818
|
+
has_observations = int(verification.get("onboarding_observations", {}).get("total_count", 0)) > 0
|
|
2742
2819
|
partial_coverage = local_checks_passed and not verification["ready"]
|
|
2743
2820
|
return exit_code, {
|
|
2744
|
-
"status": "validation_passed" if verification["ready"] else (
|
|
2821
|
+
"status": ("validation_passed_with_observations" if has_observations else "validation_passed") if verification["ready"] else (
|
|
2745
2822
|
"partial_coverage" if partial_coverage else "validation_needs_attention"
|
|
2746
2823
|
),
|
|
2747
2824
|
"inspection": str(inspection_path.relative_to(root)),
|
|
2748
2825
|
"verification": str(verification_path.relative_to(root)),
|
|
2749
2826
|
"reconciliation": str(reconciliation_path.relative_to(root)),
|
|
2827
|
+
"disclaimer": ".agenticdome/DISCLAIMER.md",
|
|
2828
|
+
"optional_observations": int(verification.get("onboarding_observations", {}).get("total_count", 0)),
|
|
2750
2829
|
"ai_integration_report_sha256": digest,
|
|
2751
2830
|
"ready": verification["ready"],
|
|
2831
|
+
"local_status": ("passed_with_observations" if has_observations else "passed") if verification["ready"] else "needs_code_or_test_fix",
|
|
2832
|
+
"activation_status": "pending_operator_checks",
|
|
2833
|
+
"review_owner": "coding_assistant_or_agenticdome",
|
|
2752
2834
|
"next_action": (
|
|
2753
|
-
"Import both JSON files
|
|
2835
|
+
"Import both JSON files and continue tenant verification. Optional observations are in .agenticdome/DISCLAIMER.md; investigating them is the tenant's choice. Prove a safe real action before production activation."
|
|
2754
2836
|
if verification["ready"] else (
|
|
2755
2837
|
"Import both JSON files as partial evidence and run a tenant-policy diagnostic if connected. "
|
|
2756
2838
|
"Open .agenticdome/RECONCILIATION.md, assess or protect the remaining paths, then rerun validate-report. "
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Optional static observations, separate from runtime enforcement and proof."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
POLICY = "pass_with_observations_v1"
|
|
8
|
+
SCHEMA = "agenticdome.onboarding-observations.v1"
|
|
9
|
+
FILE = ".agenticdome/DISCLAIMER.md"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def observations_summary(gate):
|
|
13
|
+
names = ("action_required", "unresolved_bypasses", "review_required", "high_severity_bypasses")
|
|
14
|
+
values = {name: gate.get(name, 0) for name in names}
|
|
15
|
+
valid = "high_severity_bypasses" in gate and all(
|
|
16
|
+
isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= 10000
|
|
17
|
+
for value in values.values()
|
|
18
|
+
)
|
|
19
|
+
counts = {name: value if isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= 10000 else 0
|
|
20
|
+
for name, value in values.items()}
|
|
21
|
+
actions = max(counts["action_required"], counts["unresolved_bypasses"])
|
|
22
|
+
high = counts["high_severity_bypasses"]
|
|
23
|
+
valid = valid and high <= actions
|
|
24
|
+
candidates = max(0, actions - high)
|
|
25
|
+
cross_part = bool(gate.get("cross_part_review_required") or gate.get("cross_workload_review_required"))
|
|
26
|
+
return {
|
|
27
|
+
"schema": SCHEMA, "policy": POLICY, "file": FILE,
|
|
28
|
+
"classification_complete": valid,
|
|
29
|
+
"candidate_count": candidates, "review_count": counts["review_required"],
|
|
30
|
+
"cross_part_note": cross_part,
|
|
31
|
+
"total_count": candidates + counts["review_required"] + int(cross_part),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def disclaimer_markdown(plan, verification, *, evidence_status="Current local verification"):
|
|
36
|
+
gate = verification.get("semantic_gate", {})
|
|
37
|
+
observations = verification.get("onboarding_observations") or observations_summary(gate)
|
|
38
|
+
semantic = plan.get("semantic_analysis", {})
|
|
39
|
+
lines = [
|
|
40
|
+
"# AgenticDome onboarding observations and disclaimer", "",
|
|
41
|
+
f"Evidence status: {evidence_status}", "",
|
|
42
|
+
"These observations are optional. The tenant may investigate or improve these paths later; no acknowledgement or AgenticDome clearance is required to continue onboarding solely because of these observations.",
|
|
43
|
+
"Passing local checks does not certify every execution branch or activate production. High-severity bypass findings, missing protection, uninspected source, failed tests and runtime/tenant proof failures remain blocking. Runtime BLOCKED decisions, unknown verdicts and policy failures still stop execution.", "",
|
|
44
|
+
"| Observation | Count | Tenant action |",
|
|
45
|
+
"|---|---:|---|",
|
|
46
|
+
f"| Lower-severity analyzer candidates | {observations['candidate_count'] if observations['classification_complete'] else 0} | Optional investigation |",
|
|
47
|
+
f"| Review-only paths | {observations['review_count']} | Optional investigation |",
|
|
48
|
+
f"| Cross-part flow not automatically proven | {int(observations['cross_part_note'])} | Optional additional path tests |", "",
|
|
49
|
+
"## Optional locations", "",
|
|
50
|
+
]
|
|
51
|
+
if not observations["classification_complete"]:
|
|
52
|
+
lines.extend(["Severity classification is incomplete. Unclassified candidates remain in RECONCILIATION.md for the coding assistant to resolve; they are not optional observations.", ""])
|
|
53
|
+
for field in ("bypass_risks", "review_findings"):
|
|
54
|
+
rows = semantic.get(field, [])
|
|
55
|
+
if not isinstance(rows, list):
|
|
56
|
+
continue
|
|
57
|
+
for item in rows[:100]:
|
|
58
|
+
if not isinstance(item, dict) or (field == "bypass_risks" and (
|
|
59
|
+
not observations["classification_complete"] or item.get("severity") not in {"medium", "low"}
|
|
60
|
+
)):
|
|
61
|
+
continue
|
|
62
|
+
raw = str(item.get("path") or "")[:500].replace("\\", "/")
|
|
63
|
+
if not raw or raw.startswith("/") or ".." in Path(raw).parts:
|
|
64
|
+
location = "[invalid relative path]"
|
|
65
|
+
else:
|
|
66
|
+
location = re.sub(r"[^A-Za-z0-9_./ -]", "_", raw)
|
|
67
|
+
line = item.get("line", 1)
|
|
68
|
+
line = max(1, min(line, 10000000)) if isinstance(line, int) and not isinstance(line, bool) else 1
|
|
69
|
+
kind = "analyzer candidate" if field == "bypass_risks" else "review-only path"
|
|
70
|
+
lines.append(f"- `{location}:{line}` — {kind}; static metadata may not observe an existing shared or downstream guard.")
|
|
71
|
+
if observations["total_count"] == 0:
|
|
72
|
+
lines.append("No optional observations in this run.")
|
|
73
|
+
lines.extend(["", "This file is replaced in place on verification; it is a local note, not a protection certificate or an authorization to execute a business action.", ""])
|
|
74
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Separate current source assessment from historical and operator handoff text.
|
|
2
|
+
|
|
3
|
+
Markdown remains an assertion; inspection, semantic bypass and test gates still
|
|
4
|
+
run independently. Unknown application read sites remain source gaps.
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
OPERATOR_SECTIONS = {
|
|
10
|
+
"runtime configuration handoff", "must do steps to activate firewall",
|
|
11
|
+
"activation prerequisites", "production activation",
|
|
12
|
+
}
|
|
13
|
+
HISTORY_SECTIONS = {"preserved historical baseline and unique evidence", "historical evidence"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assessment_scope(markdown):
|
|
17
|
+
headings = []
|
|
18
|
+
source_incomplete = False
|
|
19
|
+
activation_pending = False
|
|
20
|
+
source_rows = 0
|
|
21
|
+
structured = False
|
|
22
|
+
for line in markdown.splitlines():
|
|
23
|
+
match = re.match(r"^\s*(#{1,6})\s+(.+?)\s*$", line)
|
|
24
|
+
if match:
|
|
25
|
+
level, title = len(match[1]), match[2].casefold()
|
|
26
|
+
headings = [(depth, name) for depth, name in headings if depth < level]
|
|
27
|
+
headings.append((level, title))
|
|
28
|
+
structured = structured or title == "source assessment scope"
|
|
29
|
+
continue
|
|
30
|
+
sections = {name for _, name in headings}
|
|
31
|
+
if sections & HISTORY_SECTIONS:
|
|
32
|
+
continue
|
|
33
|
+
cells = [cell.strip().strip("*`").strip() for cell in line.strip().strip("|").split("|")] if line.lstrip().startswith("|") else []
|
|
34
|
+
if "source assessment scope" in sections and cells:
|
|
35
|
+
if cells[0].casefold() == "runtime source paths":
|
|
36
|
+
source_rows += 1
|
|
37
|
+
source_incomplete |= len(cells) < 2 or cells[1].upper() != "ASSESSED" or bool(re.search(r"\bNOT ASSESSED\b", line, re.I))
|
|
38
|
+
continue
|
|
39
|
+
if cells[0].casefold() == "production activation":
|
|
40
|
+
activation_pending |= len(cells) < 2 or cells[1].upper() != "VERIFIED"
|
|
41
|
+
continue
|
|
42
|
+
# A missing code read site is a source defect, even in an operator table.
|
|
43
|
+
if cells and cells[0].startswith(("AGENTICDOME_", "AGENTIC_ORCHESTRATOR_")):
|
|
44
|
+
if len(cells) < 2 or re.search(r"NOT ASSESSED|Not found|Unknown", cells[1], re.I):
|
|
45
|
+
source_incomplete = True
|
|
46
|
+
not_assessed = bool(re.search(r"\bNOT ASSESSED\b", line, re.I))
|
|
47
|
+
operator_row = bool(cells and (
|
|
48
|
+
cells[0].startswith(("AGENTICDOME_", "AGENTIC_ORCHESTRATOR_"))
|
|
49
|
+
or (len(cells) == 4 and re.match(r"^[1-6]\.", cells[0]))
|
|
50
|
+
))
|
|
51
|
+
if sections & OPERATOR_SECTIONS:
|
|
52
|
+
activation_pending |= (not_assessed and operator_row) or bool(re.search(r"REQUIRED BEFORE LIVE|LIVE VERIFICATION PENDING|\bPENDING\b", line, re.I))
|
|
53
|
+
# Arbitrary uninspected source prose cannot be hidden in handoff.
|
|
54
|
+
source_incomplete |= not_assessed and not operator_row
|
|
55
|
+
else:
|
|
56
|
+
source_incomplete |= not_assessed
|
|
57
|
+
source_incomplete |= structured and source_rows != 1
|
|
58
|
+
return {"scope_incomplete": bool(source_incomplete), "activation_pending": bool(activation_pending)}
|
|
File without changes
|
{agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/licenses/NOTICE
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.40.dist-info → agenticdome_python_sdk-1.2.43.dist-info}/top_level.txt
RENAMED
|
File without changes
|