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.
Files changed (76) hide show
  1. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/PKG-INFO +1 -1
  2. agentnode_sdk-0.5.1/agentnode.lock +132 -0
  3. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/__init__.py +5 -1
  4. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/client.py +206 -30
  5. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/config.py +10 -1
  6. agentnode_sdk-0.5.1/agentnode_sdk/credential_handle.py +245 -0
  7. agentnode_sdk-0.5.1/agentnode_sdk/credential_resolver.py +321 -0
  8. agentnode_sdk-0.5.1/agentnode_sdk/credential_store.py +174 -0
  9. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/installer.py +25 -0
  10. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/models.py +234 -190
  11. agentnode_sdk-0.5.1/agentnode_sdk/policy.py +612 -0
  12. agentnode_sdk-0.5.1/agentnode_sdk/resource_provider.py +146 -0
  13. agentnode_sdk-0.5.1/agentnode_sdk/run_log.py +274 -0
  14. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runner.py +35 -1
  15. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtime.py +108 -20
  16. agentnode_sdk-0.5.1/agentnode_sdk/runtimes/agent_runner.py +2181 -0
  17. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/mcp_runner.py +3 -0
  18. agentnode_sdk-0.5.1/agentnode_sdk/runtimes/remote_runner.py +301 -0
  19. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/pyproject.toml +1 -1
  20. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/conftest.py +60 -0
  21. agentnode_sdk-0.5.1/tests/test_agent_runner.py +1725 -0
  22. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_cli.py +1 -1
  23. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_client_sprint_b.py +1 -1
  24. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_config.py +24 -0
  25. agentnode_sdk-0.5.1/tests/test_credential_handle.py +237 -0
  26. agentnode_sdk-0.5.1/tests/test_credential_integration.py +270 -0
  27. agentnode_sdk-0.5.1/tests/test_credential_resolver.py +236 -0
  28. agentnode_sdk-0.5.1/tests/test_credential_store.py +191 -0
  29. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_e2e_runtime.py +2 -1
  30. agentnode_sdk-0.5.1/tests/test_llm_binding.py +528 -0
  31. agentnode_sdk-0.5.1/tests/test_llm_call_runlog.py +162 -0
  32. agentnode_sdk-0.5.1/tests/test_policy.py +681 -0
  33. agentnode_sdk-0.5.1/tests/test_policy_integration.py +411 -0
  34. agentnode_sdk-0.5.1/tests/test_prompt_specs.py +196 -0
  35. agentnode_sdk-0.5.1/tests/test_remote_runner.py +245 -0
  36. agentnode_sdk-0.5.1/tests/test_resource_provider.py +111 -0
  37. agentnode_sdk-0.5.1/tests/test_resource_specs.py +140 -0
  38. agentnode_sdk-0.5.1/tests/test_run_log.py +245 -0
  39. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_runner.py +11 -5
  40. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_runtime.py +27 -19
  41. agentnode_sdk-0.4.1/agentnode.lock +0 -23
  42. agentnode_sdk-0.4.1/agentnode_sdk/policy.py +0 -15
  43. agentnode_sdk-0.4.1/agentnode_sdk/runtimes/remote_runner.py +0 -25
  44. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/.env.example +0 -0
  45. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/.gitignore +0 -0
  46. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/CHANGELOG.md +0 -0
  47. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/README.md +0 -0
  48. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/async_client.py +0 -0
  49. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/__init__.py +0 -0
  50. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/__main__.py +0 -0
  51. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/commands.py +0 -0
  52. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/main.py +0 -0
  53. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/output.py +0 -0
  54. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/cli/setup_wizard.py +0 -0
  55. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/compatibility.py +0 -0
  56. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/detect.py +0 -0
  57. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/exceptions.py +0 -0
  58. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/__init__.py +0 -0
  59. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/agentnode_sdk/runtimes/python_runner.py +0 -0
  60. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/analyze_scores.py +0 -0
  61. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/batch_verify.py +0 -0
  62. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/ci_smoke_test.py +0 -0
  63. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/generate_compatibility_artifacts.py +0 -0
  64. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/verify_toolcalls.py +0 -0
  65. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/scripts/weekly_retest.sh +0 -0
  66. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/__init__.py +0 -0
  67. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_async_client.py +0 -0
  68. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_auto_upgrade_policy.py +0 -0
  69. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_client.py +0 -0
  70. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_detect.py +0 -0
  71. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_detect_and_install.py +0 -0
  72. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_edge_cases.py +0 -0
  73. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_installer_sprint_b.py +0 -0
  74. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_provider_matrix.py +0 -0
  75. {agentnode_sdk-0.4.1 → agentnode_sdk-0.5.1}/tests/test_smart.py +0 -0
  76. {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.4.1
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.0"
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={"X-API-Key": api_key},
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 = detail.get("trust_level", "unverified")
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
- pass
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
- # Check trust — P1-SDK9: canonical default is "unverified",
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
- # Check permissions
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
- # Pick the highest-scored result
790
- best = result.results[0]
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
- # Trust filter
793
- if require_trusted and best.trust_level not in TRUST_LEVELS_TRUSTED:
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
- not require_trusted
804
- and require_verified
805
- and best.trust_level not in TRUST_LEVELS_VERIFIED
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=best.slug,
809
- version=best.version,
984
+ slug="",
985
+ version="",
810
986
  installed=False,
811
987
  already_installed=False,
812
- message=f"Best match '{best.slug}' has trust level '{best.trust_level}', but 'verified' required.",
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
- actual_value: Any = value.lower() == "true" if key == "trust.allow_unverified" else value.lower()
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