agentnode-sdk 0.4.1__tar.gz → 0.5.1__tar.gz
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.
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/PKG-INFO +1 -1
- agentnode_sdk-0.5.1/agentnode.lock +132 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/__init__.py +5 -1
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/client.py +206 -30
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/config.py +10 -1
- agentnode_sdk-0.5.1/agentnode_sdk/credential_handle.py +245 -0
- agentnode_sdk-0.5.1/agentnode_sdk/credential_resolver.py +321 -0
- agentnode_sdk-0.5.1/agentnode_sdk/credential_store.py +174 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/installer.py +25 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/models.py +234 -190
- agentnode_sdk-0.5.1/agentnode_sdk/policy.py +612 -0
- agentnode_sdk-0.5.1/agentnode_sdk/resource_provider.py +146 -0
- agentnode_sdk-0.5.1/agentnode_sdk/run_log.py +274 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runner.py +35 -1
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtime.py +108 -20
- agentnode_sdk-0.5.1/agentnode_sdk/runtimes/agent_runner.py +2181 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/mcp_runner.py +3 -0
- agentnode_sdk-0.5.1/agentnode_sdk/runtimes/remote_runner.py +301 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/pyproject.toml +1 -1
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/conftest.py +60 -0
- agentnode_sdk-0.5.1/tests/test_agent_runner.py +1725 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_cli.py +1 -1
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_client_sprint_b.py +1 -1
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_config.py +24 -0
- agentnode_sdk-0.5.1/tests/test_credential_handle.py +237 -0
- agentnode_sdk-0.5.1/tests/test_credential_integration.py +270 -0
- agentnode_sdk-0.5.1/tests/test_credential_resolver.py +236 -0
- agentnode_sdk-0.5.1/tests/test_credential_store.py +191 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_e2e_runtime.py +2 -1
- agentnode_sdk-0.5.1/tests/test_llm_binding.py +528 -0
- agentnode_sdk-0.5.1/tests/test_llm_call_runlog.py +162 -0
- agentnode_sdk-0.5.1/tests/test_policy.py +681 -0
- agentnode_sdk-0.5.1/tests/test_policy_integration.py +411 -0
- agentnode_sdk-0.5.1/tests/test_prompt_specs.py +196 -0
- agentnode_sdk-0.5.1/tests/test_remote_runner.py +245 -0
- agentnode_sdk-0.5.1/tests/test_resource_provider.py +111 -0
- agentnode_sdk-0.5.1/tests/test_resource_specs.py +140 -0
- agentnode_sdk-0.5.1/tests/test_run_log.py +245 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_runner.py +11 -5
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_runtime.py +27 -19
- agentnode_sdk-0.4.1/agentnode.lock +0 -23
- agentnode_sdk-0.4.1/agentnode_sdk/policy.py +0 -15
- agentnode_sdk-0.4.1/agentnode_sdk/runtimes/remote_runner.py +0 -25
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/.env.example +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/.gitignore +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/CHANGELOG.md +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/README.md +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/async_client.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/__init__.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/__main__.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/commands.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/main.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/output.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/setup_wizard.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/compatibility.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/detect.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/exceptions.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/__init__.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/python_runner.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/analyze_scores.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/batch_verify.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/ci_smoke_test.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/generate_compatibility_artifacts.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/verify_toolcalls.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/weekly_retest.sh +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/__init__.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_async_client.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_auto_upgrade_policy.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_client.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_detect.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_detect_and_install.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_edge_cases.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_installer_sprint_b.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_provider_matrix.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_smart.py +0 -0
- {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_v02.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentnode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents.
|
|
5
5
|
Project-URL: Homepage, https://agentnode.net
|
|
6
6
|
Project-URL: Repository, https://github.com/agentnode-ai/agentnode
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfile_version": "0.1",
|
|
3
|
+
"updated_at": "2026-04-24T13:59:54.296881+00:00",
|
|
4
|
+
"packages": {
|
|
5
|
+
"word-counter-pack": {
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"package_type": "toolpack",
|
|
8
|
+
"runtime": "python",
|
|
9
|
+
"entrypoint": "word_counter_pack.tool",
|
|
10
|
+
"capability_ids": [
|
|
11
|
+
"data_cleaning"
|
|
12
|
+
],
|
|
13
|
+
"tools": [
|
|
14
|
+
{
|
|
15
|
+
"name": "count_words",
|
|
16
|
+
"entrypoint": "word_counter_pack.tool:count_words"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"artifact_hash": "sha256:445388c86aa2b20c3e748f58b2d221548f1f3f71ff5c715a64cdc80966fab03c",
|
|
20
|
+
"installed_at": "2026-03-18T02:18:53.827396+00:00",
|
|
21
|
+
"source": "sdk",
|
|
22
|
+
"trust_level": "trusted",
|
|
23
|
+
"permissions": {
|
|
24
|
+
"network_level": "none",
|
|
25
|
+
"filesystem_level": "none",
|
|
26
|
+
"code_execution_level": "none",
|
|
27
|
+
"data_access_level": "input_only",
|
|
28
|
+
"user_approval_level": "never"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"pdf-reader-pack": {
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"package_type": "toolpack",
|
|
34
|
+
"runtime": "python",
|
|
35
|
+
"entrypoint": "pdf_reader_pack.tool",
|
|
36
|
+
"capability_ids": [
|
|
37
|
+
"pdf_extraction"
|
|
38
|
+
],
|
|
39
|
+
"tools": [],
|
|
40
|
+
"artifact_hash": "sha256:4635316cb5f8ee477f41ccad570c3bcd5026fc7cd1fbf402a12df575be54ae20",
|
|
41
|
+
"installed_at": "2026-04-15T23:53:44.341323+00:00",
|
|
42
|
+
"source": "sdk",
|
|
43
|
+
"trust_level": "trusted",
|
|
44
|
+
"permissions": {
|
|
45
|
+
"network_level": "none",
|
|
46
|
+
"filesystem_level": "temp",
|
|
47
|
+
"code_execution_level": "none",
|
|
48
|
+
"data_access_level": "input_only",
|
|
49
|
+
"user_approval_level": "never"
|
|
50
|
+
},
|
|
51
|
+
"prompts": [],
|
|
52
|
+
"resources": [],
|
|
53
|
+
"connector": null,
|
|
54
|
+
"agent": null
|
|
55
|
+
},
|
|
56
|
+
"web-search-pack": {
|
|
57
|
+
"version": "1.0.0",
|
|
58
|
+
"package_type": "toolpack",
|
|
59
|
+
"runtime": "python",
|
|
60
|
+
"entrypoint": "web_search_pack.tool",
|
|
61
|
+
"capability_ids": [
|
|
62
|
+
"web_search"
|
|
63
|
+
],
|
|
64
|
+
"tools": [],
|
|
65
|
+
"artifact_hash": "sha256:3e667ba523ceddf6137a1a15f4103d48a59085d1d277750f30addf2758228d12",
|
|
66
|
+
"installed_at": "2026-04-16T13:48:00.554169+00:00",
|
|
67
|
+
"source": "sdk",
|
|
68
|
+
"trust_level": "trusted",
|
|
69
|
+
"permissions": {
|
|
70
|
+
"network_level": "restricted",
|
|
71
|
+
"filesystem_level": "none",
|
|
72
|
+
"code_execution_level": "none",
|
|
73
|
+
"data_access_level": "input_only",
|
|
74
|
+
"user_approval_level": "never"
|
|
75
|
+
},
|
|
76
|
+
"prompts": [],
|
|
77
|
+
"resources": [],
|
|
78
|
+
"connector": null,
|
|
79
|
+
"agent": null
|
|
80
|
+
},
|
|
81
|
+
"web-design-pack": {
|
|
82
|
+
"version": "1.0.0",
|
|
83
|
+
"package_type": "toolpack",
|
|
84
|
+
"runtime": "python",
|
|
85
|
+
"entrypoint": "web_design_pack.tool",
|
|
86
|
+
"capability_ids": [
|
|
87
|
+
"web_design"
|
|
88
|
+
],
|
|
89
|
+
"tools": [],
|
|
90
|
+
"artifact_hash": "sha256:bf90bc3e2fb78682bc3df5f53d7a195675c564f0f3ede8dddfce403b59b73525",
|
|
91
|
+
"installed_at": "2026-04-16T13:48:58.751523+00:00",
|
|
92
|
+
"source": "sdk",
|
|
93
|
+
"trust_level": "trusted",
|
|
94
|
+
"permissions": {
|
|
95
|
+
"network_level": "none",
|
|
96
|
+
"filesystem_level": "temp",
|
|
97
|
+
"code_execution_level": "none",
|
|
98
|
+
"data_access_level": "input_only",
|
|
99
|
+
"user_approval_level": "never"
|
|
100
|
+
},
|
|
101
|
+
"prompts": [],
|
|
102
|
+
"resources": [],
|
|
103
|
+
"connector": null,
|
|
104
|
+
"agent": null
|
|
105
|
+
},
|
|
106
|
+
"csv-analyzer-pack": {
|
|
107
|
+
"version": "1.0.0",
|
|
108
|
+
"package_type": "toolpack",
|
|
109
|
+
"runtime": "python",
|
|
110
|
+
"entrypoint": "csv_analyzer_pack.tool",
|
|
111
|
+
"capability_ids": [
|
|
112
|
+
"csv_analysis"
|
|
113
|
+
],
|
|
114
|
+
"tools": [],
|
|
115
|
+
"artifact_hash": "sha256:62cfe9be62e97174ab41eff190e356d1c512b11ac88cb566d7c75e5f5e0f8d42",
|
|
116
|
+
"installed_at": "2026-04-24T13:59:54.295800+00:00",
|
|
117
|
+
"source": "sdk",
|
|
118
|
+
"trust_level": "trusted",
|
|
119
|
+
"permissions": {
|
|
120
|
+
"network_level": "none",
|
|
121
|
+
"filesystem_level": "temp",
|
|
122
|
+
"code_execution_level": "none",
|
|
123
|
+
"data_access_level": "input_only",
|
|
124
|
+
"user_approval_level": "never"
|
|
125
|
+
},
|
|
126
|
+
"prompts": [],
|
|
127
|
+
"resources": [],
|
|
128
|
+
"connector": null,
|
|
129
|
+
"agent": null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -28,6 +28,7 @@ from agentnode_sdk.models import (
|
|
|
28
28
|
SmartRunResult,
|
|
29
29
|
)
|
|
30
30
|
from agentnode_sdk.compatibility import recommend_model
|
|
31
|
+
from agentnode_sdk.policy import PolicyResult, check_install, check_run
|
|
31
32
|
from agentnode_sdk.runner import run_tool
|
|
32
33
|
from agentnode_sdk.runtime import AgentNodeRuntime
|
|
33
34
|
|
|
@@ -35,7 +36,7 @@ from agentnode_sdk.runtime import AgentNodeRuntime
|
|
|
35
36
|
Client = AgentNodeClient
|
|
36
37
|
ToolError = AgentNodeToolError
|
|
37
38
|
|
|
38
|
-
__version__ = "0.4.
|
|
39
|
+
__version__ = "0.4.1"
|
|
39
40
|
__all__ = [
|
|
40
41
|
"AgentNode",
|
|
41
42
|
"AsyncAgentNode",
|
|
@@ -67,5 +68,8 @@ __all__ = [
|
|
|
67
68
|
"DetectAndInstallResult",
|
|
68
69
|
"SmartRunResult",
|
|
69
70
|
"AgentNodeRuntime",
|
|
71
|
+
"PolicyResult",
|
|
72
|
+
"check_install",
|
|
73
|
+
"check_run",
|
|
70
74
|
"recommend_model",
|
|
71
75
|
]
|
|
@@ -89,15 +89,16 @@ class AgentNode:
|
|
|
89
89
|
|
|
90
90
|
def __init__(self, api_key: str | None = None, base_url: str = DEFAULT_BASE_URL):
|
|
91
91
|
api_key = api_key or os.environ.get("AGENTNODE_API_KEY")
|
|
92
|
-
if not api_key:
|
|
93
|
-
raise ValueError("api_key is required (pass explicitly or set AGENTNODE_API_KEY)")
|
|
94
92
|
# Ensure base_url ends with /v1 for API routing
|
|
95
93
|
base = base_url.rstrip("/")
|
|
96
94
|
if not base.endswith("/v1"):
|
|
97
95
|
base = f"{base}/v1"
|
|
96
|
+
headers: dict[str, str] = {}
|
|
97
|
+
if api_key:
|
|
98
|
+
headers["X-API-Key"] = api_key
|
|
98
99
|
self._client = httpx.Client(
|
|
99
100
|
base_url=base,
|
|
100
|
-
headers=
|
|
101
|
+
headers=headers,
|
|
101
102
|
timeout=30,
|
|
102
103
|
)
|
|
103
104
|
|
|
@@ -236,6 +237,38 @@ class AgentNode:
|
|
|
236
237
|
raise AgentNodeError("UNKNOWN", f"Invalid JSON response: {exc}") from exc
|
|
237
238
|
|
|
238
239
|
|
|
240
|
+
def _check_credential_available(provider: str) -> bool:
|
|
241
|
+
"""Check if a credential is available for a provider (env, local file, or API)."""
|
|
242
|
+
# Check env var
|
|
243
|
+
env_key = f"AGENTNODE_CRED_{provider.upper().replace('-', '_')}"
|
|
244
|
+
if os.environ.get(env_key):
|
|
245
|
+
return True
|
|
246
|
+
# Check local file
|
|
247
|
+
try:
|
|
248
|
+
from agentnode_sdk.credential_store import has_credential
|
|
249
|
+
if has_credential(provider):
|
|
250
|
+
return True
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
# Check if API credentials are configured (session token)
|
|
254
|
+
if os.environ.get("AGENTNODE_SESSION_TOKEN") and os.environ.get("AGENTNODE_API_URL"):
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_package_connector_provider(slug: str, client: "AgentNodeClient") -> str | None:
|
|
260
|
+
"""Get the connector provider from package metadata, if any."""
|
|
261
|
+
try:
|
|
262
|
+
meta = client._request("GET", f"/packages/{slug}/install-info")
|
|
263
|
+
connector = meta.get("connector")
|
|
264
|
+
if isinstance(connector, dict):
|
|
265
|
+
return connector.get("provider")
|
|
266
|
+
except Exception:
|
|
267
|
+
import logging as _logging
|
|
268
|
+
_logging.getLogger(__name__).debug("Failed to fetch connector provider for %s", slug, exc_info=True)
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
239
272
|
class AgentNodeClient:
|
|
240
273
|
"""Extended client returning typed dataclass models. Backward-compatible.
|
|
241
274
|
|
|
@@ -456,6 +489,7 @@ class AgentNodeClient:
|
|
|
456
489
|
capabilities=caps,
|
|
457
490
|
dependencies=deps,
|
|
458
491
|
permissions=perms,
|
|
492
|
+
agent=data.get("agent"),
|
|
459
493
|
)
|
|
460
494
|
|
|
461
495
|
# --- Download ---
|
|
@@ -522,11 +556,72 @@ class AgentNodeClient:
|
|
|
522
556
|
verification_tier = None
|
|
523
557
|
try:
|
|
524
558
|
detail = self._request("GET", f"/packages/{slug}")
|
|
525
|
-
trust_level
|
|
559
|
+
# trust_level lives in publisher.trust_level or blocks.trust.publisher_trust_level,
|
|
560
|
+
# NOT as a top-level field (which is None/missing in the API response).
|
|
561
|
+
trust_level = (
|
|
562
|
+
detail.get("publisher", {}).get("trust_level")
|
|
563
|
+
or detail.get("blocks", {}).get("trust", {}).get("publisher_trust_level")
|
|
564
|
+
or "unverified"
|
|
565
|
+
)
|
|
526
566
|
lv = detail.get("latest_version") or {}
|
|
527
567
|
verification_tier = lv.get("verification_tier")
|
|
528
568
|
except Exception:
|
|
529
|
-
|
|
569
|
+
import logging as _logging
|
|
570
|
+
_logging.getLogger(__name__).warning("Failed to fetch trust info for %s, defaulting to unverified", slug, exc_info=True)
|
|
571
|
+
|
|
572
|
+
# 2b. Policy check — authoritative (Phase B: hard enforcement)
|
|
573
|
+
from agentnode_sdk.policy import check_install as _policy_check_install
|
|
574
|
+
from agentnode_sdk.policy import audit_decision as _policy_audit
|
|
575
|
+
policy_entry = {
|
|
576
|
+
"trust_level": trust_level or "unverified",
|
|
577
|
+
"permissions": _permissions_to_dict(meta.permissions),
|
|
578
|
+
}
|
|
579
|
+
try:
|
|
580
|
+
decision = _policy_check_install(slug, policy_entry, interactive=True)
|
|
581
|
+
except Exception as exc:
|
|
582
|
+
# Policy check itself crashed (bug in policy.py, not config issue).
|
|
583
|
+
# Translate to a clean deny — never silently proceed.
|
|
584
|
+
import logging as _logging
|
|
585
|
+
_logging.getLogger(__name__).warning(
|
|
586
|
+
"Policy check raised for %s: %s", slug, exc,
|
|
587
|
+
)
|
|
588
|
+
return InstallResult(
|
|
589
|
+
slug=slug,
|
|
590
|
+
version=meta.version,
|
|
591
|
+
installed=False,
|
|
592
|
+
already_installed=False,
|
|
593
|
+
message=f"Policy check failed: internal error. Install denied.",
|
|
594
|
+
trust_level=trust_level,
|
|
595
|
+
verification_tier=verification_tier,
|
|
596
|
+
)
|
|
597
|
+
# Audit is best-effort — must never block the policy decision
|
|
598
|
+
try:
|
|
599
|
+
_policy_audit(
|
|
600
|
+
decision, "client_install", slug,
|
|
601
|
+
trust_level=policy_entry["trust_level"],
|
|
602
|
+
)
|
|
603
|
+
except Exception:
|
|
604
|
+
pass # Audit write failure is non-fatal
|
|
605
|
+
if decision.action == "deny":
|
|
606
|
+
return InstallResult(
|
|
607
|
+
slug=slug,
|
|
608
|
+
version=meta.version,
|
|
609
|
+
installed=False,
|
|
610
|
+
already_installed=False,
|
|
611
|
+
message=decision.reason,
|
|
612
|
+
trust_level=trust_level,
|
|
613
|
+
verification_tier=verification_tier,
|
|
614
|
+
)
|
|
615
|
+
if decision.action == "prompt":
|
|
616
|
+
return InstallResult(
|
|
617
|
+
slug=slug,
|
|
618
|
+
version=meta.version,
|
|
619
|
+
installed=False,
|
|
620
|
+
already_installed=False,
|
|
621
|
+
message=f"Approval required: {decision.reason}",
|
|
622
|
+
trust_level=trust_level,
|
|
623
|
+
verification_tier=verification_tier,
|
|
624
|
+
)
|
|
530
625
|
|
|
531
626
|
# 3. Trust check
|
|
532
627
|
if require_trusted or require_verified:
|
|
@@ -602,6 +697,7 @@ class AgentNodeClient:
|
|
|
602
697
|
verbose=verbose,
|
|
603
698
|
trust_level=trust_level,
|
|
604
699
|
permissions=_permissions_to_dict(meta.permissions),
|
|
700
|
+
agent=meta.agent,
|
|
605
701
|
)
|
|
606
702
|
|
|
607
703
|
return InstallResult(
|
|
@@ -625,6 +721,11 @@ class AgentNodeClient:
|
|
|
625
721
|
require_trusted: bool = False,
|
|
626
722
|
require_verified: bool = False,
|
|
627
723
|
allowed_permissions: list[str] | None = None,
|
|
724
|
+
# NOTE: Legacy path — require_trusted, require_verified,
|
|
725
|
+
# allowed_permissions, denied_permissions are pre-policy params.
|
|
726
|
+
# policy.check_install() is now the Single Source of Truth for
|
|
727
|
+
# trust and permission decisions. These params remain for backward
|
|
728
|
+
# compat only. Cleanup sprint: consolidate through policy.py.
|
|
628
729
|
denied_permissions: list[str] | None = None,
|
|
629
730
|
) -> CanInstallResult:
|
|
630
731
|
"""Check whether a package can be installed under given constraints.
|
|
@@ -632,6 +733,9 @@ class AgentNodeClient:
|
|
|
632
733
|
Evaluates trust level, permissions, and deprecation status
|
|
633
734
|
without performing any installation.
|
|
634
735
|
|
|
736
|
+
Phase B: delegates trust and permission checks to
|
|
737
|
+
``policy.check_install()`` as Single Source of Truth.
|
|
738
|
+
|
|
635
739
|
Args:
|
|
636
740
|
require_trusted: Require 'trusted' or 'curated' trust level.
|
|
637
741
|
require_verified: Require 'verified', 'trusted', or 'curated'.
|
|
@@ -640,7 +744,7 @@ class AgentNodeClient:
|
|
|
640
744
|
meta = self.get_install_metadata(slug, version)
|
|
641
745
|
pkg = self.get_package(slug)
|
|
642
746
|
|
|
643
|
-
# Check deprecation
|
|
747
|
+
# Check deprecation (install-specific, not policy)
|
|
644
748
|
if pkg.is_deprecated:
|
|
645
749
|
return CanInstallResult(
|
|
646
750
|
allowed=False,
|
|
@@ -651,7 +755,7 @@ class AgentNodeClient:
|
|
|
651
755
|
permissions=meta.permissions,
|
|
652
756
|
)
|
|
653
757
|
|
|
654
|
-
# Check artifact availability
|
|
758
|
+
# Check artifact availability (install-specific, not policy)
|
|
655
759
|
if not meta.artifact or not meta.artifact.url:
|
|
656
760
|
return CanInstallResult(
|
|
657
761
|
allowed=False,
|
|
@@ -662,9 +766,7 @@ class AgentNodeClient:
|
|
|
662
766
|
permissions=meta.permissions,
|
|
663
767
|
)
|
|
664
768
|
|
|
665
|
-
#
|
|
666
|
-
# never "unknown". Everything downstream expects the trust-level
|
|
667
|
-
# vocabulary from models.py (unverified/verified/trusted/curated).
|
|
769
|
+
# Fetch trust level — P1-SDK9: canonical default is "unverified"
|
|
668
770
|
trust_level = "unverified"
|
|
669
771
|
try:
|
|
670
772
|
detail = self._request("GET", f"/packages/{slug}")
|
|
@@ -672,6 +774,7 @@ class AgentNodeClient:
|
|
|
672
774
|
except Exception:
|
|
673
775
|
pass
|
|
674
776
|
|
|
777
|
+
# Caller-specified trust constraints (require_trusted / require_verified)
|
|
675
778
|
if require_trusted and trust_level not in TRUST_LEVELS_TRUSTED:
|
|
676
779
|
return CanInstallResult(
|
|
677
780
|
allowed=False,
|
|
@@ -692,7 +795,33 @@ class AgentNodeClient:
|
|
|
692
795
|
permissions=meta.permissions,
|
|
693
796
|
)
|
|
694
797
|
|
|
695
|
-
#
|
|
798
|
+
# Policy check via check_install() — Single Source of Truth
|
|
799
|
+
from agentnode_sdk.policy import check_install as _policy_check
|
|
800
|
+
policy_entry = {
|
|
801
|
+
"trust_level": trust_level,
|
|
802
|
+
"permissions": _permissions_to_dict(meta.permissions),
|
|
803
|
+
}
|
|
804
|
+
decision = _policy_check(slug, policy_entry, interactive=True)
|
|
805
|
+
if decision.action == "deny":
|
|
806
|
+
return CanInstallResult(
|
|
807
|
+
allowed=False,
|
|
808
|
+
slug=slug,
|
|
809
|
+
version=meta.version,
|
|
810
|
+
trust_level=trust_level,
|
|
811
|
+
reason=decision.reason,
|
|
812
|
+
permissions=meta.permissions,
|
|
813
|
+
)
|
|
814
|
+
if decision.action == "prompt":
|
|
815
|
+
return CanInstallResult(
|
|
816
|
+
allowed=False,
|
|
817
|
+
slug=slug,
|
|
818
|
+
version=meta.version,
|
|
819
|
+
trust_level=trust_level,
|
|
820
|
+
reason=f"Approval required: {decision.reason}",
|
|
821
|
+
permissions=meta.permissions,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
# Caller-specified permission deny-list (kept for backward compat)
|
|
696
825
|
if meta.permissions and denied_permissions:
|
|
697
826
|
perm_map = {
|
|
698
827
|
"network": meta.permissions.network_level,
|
|
@@ -786,30 +915,77 @@ class AgentNodeClient:
|
|
|
786
915
|
message=f"No packages found for capabilities: {capabilities}",
|
|
787
916
|
)
|
|
788
917
|
|
|
789
|
-
#
|
|
790
|
-
|
|
918
|
+
# Check credential availability for auto-install
|
|
919
|
+
from agentnode_sdk.config import load_config as _load_cfg
|
|
920
|
+
cfg = _load_cfg()
|
|
921
|
+
require_creds = cfg.get("credentials", {}).get("require_before_auto_install", True)
|
|
922
|
+
|
|
923
|
+
# Filter candidates: skip packages that need credentials we don't have
|
|
924
|
+
candidates = list(result.results)
|
|
925
|
+
best = None
|
|
926
|
+
skipped_for_creds: str | None = None
|
|
927
|
+
|
|
928
|
+
for candidate in candidates:
|
|
929
|
+
# Trust filter
|
|
930
|
+
if require_trusted and candidate.trust_level not in TRUST_LEVELS_TRUSTED:
|
|
931
|
+
continue
|
|
932
|
+
if (
|
|
933
|
+
not require_trusted
|
|
934
|
+
and require_verified
|
|
935
|
+
and candidate.trust_level not in TRUST_LEVELS_VERIFIED
|
|
936
|
+
):
|
|
937
|
+
continue
|
|
938
|
+
|
|
939
|
+
# Credential check for auto-install
|
|
940
|
+
connector_provider = _get_package_connector_provider(candidate.slug, self)
|
|
941
|
+
if connector_provider and require_creds:
|
|
942
|
+
if not _check_credential_available(connector_provider):
|
|
943
|
+
skipped_for_creds = skipped_for_creds or candidate.slug
|
|
944
|
+
import logging as _logging
|
|
945
|
+
_logging.getLogger("agentnode.client").info(
|
|
946
|
+
"Skipping %s: requires %s credentials (not configured)",
|
|
947
|
+
candidate.slug, connector_provider,
|
|
948
|
+
)
|
|
949
|
+
continue
|
|
950
|
+
elif connector_provider and not require_creds:
|
|
951
|
+
import logging as _logging
|
|
952
|
+
_logging.getLogger("agentnode.client").warning(
|
|
953
|
+
"Warning: %s requires %s credentials that are not configured",
|
|
954
|
+
candidate.slug, connector_provider,
|
|
955
|
+
)
|
|
791
956
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return InstallResult(
|
|
795
|
-
slug=best.slug,
|
|
796
|
-
version=best.version,
|
|
797
|
-
installed=False,
|
|
798
|
-
already_installed=False,
|
|
799
|
-
message=f"Best match '{best.slug}' has trust level '{best.trust_level}', but 'trusted' required.",
|
|
800
|
-
)
|
|
957
|
+
best = candidate
|
|
958
|
+
break
|
|
801
959
|
|
|
802
|
-
if
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
960
|
+
if best is None:
|
|
961
|
+
# All candidates were filtered out
|
|
962
|
+
if skipped_for_creds:
|
|
963
|
+
return InstallResult(
|
|
964
|
+
slug=skipped_for_creds,
|
|
965
|
+
version="",
|
|
966
|
+
installed=False,
|
|
967
|
+
already_installed=False,
|
|
968
|
+
message=(
|
|
969
|
+
f"Package {skipped_for_creds} requires credentials that are not configured. "
|
|
970
|
+
f"Run `agentnode auth <provider>` first. "
|
|
971
|
+
f"No credential-free alternative found for capabilities: {capabilities}"
|
|
972
|
+
),
|
|
973
|
+
)
|
|
974
|
+
top = candidates[0] if candidates else None
|
|
975
|
+
if top:
|
|
976
|
+
return InstallResult(
|
|
977
|
+
slug=top.slug,
|
|
978
|
+
version=top.version,
|
|
979
|
+
installed=False,
|
|
980
|
+
already_installed=False,
|
|
981
|
+
message=f"Best match '{top.slug}' has trust level '{top.trust_level}', but policy requirements not met.",
|
|
982
|
+
)
|
|
807
983
|
return InstallResult(
|
|
808
|
-
slug=
|
|
809
|
-
version=
|
|
984
|
+
slug="",
|
|
985
|
+
version="",
|
|
810
986
|
installed=False,
|
|
811
987
|
already_installed=False,
|
|
812
|
-
message=f"
|
|
988
|
+
message=f"No packages found for capabilities: {capabilities}",
|
|
813
989
|
)
|
|
814
990
|
|
|
815
991
|
return self.install(best.slug, verbose=verbose)
|
|
@@ -25,6 +25,9 @@ DEFAULTS: dict[str, Any] = {
|
|
|
25
25
|
"filesystem": "prompt",
|
|
26
26
|
"code_execution": "sandboxed",
|
|
27
27
|
},
|
|
28
|
+
"credentials": {
|
|
29
|
+
"require_before_auto_install": True,
|
|
30
|
+
},
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
VALID_VALUES: dict[str, tuple[str, ...]] = {
|
|
@@ -35,6 +38,7 @@ VALID_VALUES: dict[str, tuple[str, ...]] = {
|
|
|
35
38
|
"permissions.network": ("allow", "prompt", "deny"),
|
|
36
39
|
"permissions.filesystem": ("allow", "prompt", "deny"),
|
|
37
40
|
"permissions.code_execution": ("sandboxed", "prompt", "deny"),
|
|
41
|
+
"credentials.require_before_auto_install": ("true", "false"),
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
|
|
@@ -76,6 +80,10 @@ def _merge_defaults(data: dict) -> dict[str, Any]:
|
|
|
76
80
|
for k in ("network", "filesystem", "code_execution"):
|
|
77
81
|
if k in data["permissions"]:
|
|
78
82
|
cfg["permissions"][k] = data["permissions"][k]
|
|
83
|
+
if isinstance(data.get("credentials"), dict):
|
|
84
|
+
for k in ("require_before_auto_install",):
|
|
85
|
+
if k in data["credentials"]:
|
|
86
|
+
cfg["credentials"][k] = data["credentials"][k]
|
|
79
87
|
return cfg
|
|
80
88
|
|
|
81
89
|
|
|
@@ -138,7 +146,8 @@ def set_value(cfg: dict[str, Any], key: str, value: str) -> dict[str, Any]:
|
|
|
138
146
|
f"Allowed: {', '.join(allowed)}"
|
|
139
147
|
)
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
bool_keys = ("trust.allow_unverified", "credentials.require_before_auto_install")
|
|
150
|
+
actual_value: Any = value.lower() == "true" if key in bool_keys else value.lower()
|
|
142
151
|
|
|
143
152
|
parts = key.split(".")
|
|
144
153
|
current: Any = cfg
|