agentnode-sdk 0.14.0__tar.gz → 0.16.0__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 (191) hide show
  1. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/CHANGELOG.md +88 -0
  2. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/PKG-INFO +1 -1
  3. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/__init__.py +1 -1
  4. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/auth.py +105 -36
  5. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/setup_wizard.py +49 -19
  6. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/config.py +9 -1
  7. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/credential_store.py +12 -5
  8. agentnode_sdk-0.16.0/agentnode_sdk/llm_providers.py +116 -0
  9. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/agent_runner.py +79 -49
  10. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/pyproject.toml +1 -1
  11. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_auth_cli_vault.py +129 -1
  12. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_config.py +31 -0
  13. agentnode_sdk-0.16.0/tests/test_llm_providers.py +79 -0
  14. agentnode_sdk-0.16.0/tests/test_llm_vault_binding.py +309 -0
  15. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_setup_wizard.py +56 -1
  16. agentnode_sdk-0.14.0/tests/test_llm_vault_binding.py +0 -143
  17. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/.env.example +0 -0
  18. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/.gitignore +0 -0
  19. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/README.md +0 -0
  20. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/REGISTRY_SIGNING_ACTIVATION.md +0 -0
  21. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/REGISTRY_SIGNING_SPEC.md +0 -0
  22. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/THREAT_MODEL.md +0 -0
  23. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/TRUST_STACK.md +0 -0
  24. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode.lock +0 -0
  25. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/_fileutil.py +0 -0
  26. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/async_client.py +0 -0
  27. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/capability_graph.py +0 -0
  28. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/capability_taxonomy.py +0 -0
  29. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/__init__.py +0 -0
  30. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/__main__.py +0 -0
  31. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/audit.py +0 -0
  32. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/cassette_audit.py +0 -0
  33. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/commands.py +0 -0
  34. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/complements.py +0 -0
  35. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/init.py +0 -0
  36. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/main.py +0 -0
  37. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/mcp_commands.py +0 -0
  38. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/mcp_status.py +0 -0
  39. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/mcp_submit.py +0 -0
  40. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/mcp_verify.py +0 -0
  41. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/output.py +0 -0
  42. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/publish.py +0 -0
  43. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/record_cases.py +0 -0
  44. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/sandbox_commands.py +0 -0
  45. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/serve.py +0 -0
  46. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/smart_run.py +0 -0
  47. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/templates.py +0 -0
  48. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/validate.py +0 -0
  49. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/cli/verify_local.py +0 -0
  50. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/client.py +0 -0
  51. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/compatibility.py +0 -0
  52. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/credential_handle.py +0 -0
  53. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/credential_resolver.py +0 -0
  54. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/detect.py +0 -0
  55. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/exceptions.py +0 -0
  56. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/guard.py +0 -0
  57. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/input_guard.py +0 -0
  58. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/installer.py +0 -0
  59. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/key_status.py +0 -0
  60. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/lock_integrity.py +0 -0
  61. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/mcp_server.py +0 -0
  62. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/models.py +0 -0
  63. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/planner.py +0 -0
  64. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/policy.py +0 -0
  65. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/references.py +0 -0
  66. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/registry_trust.py +0 -0
  67. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/resolve.py +0 -0
  68. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/resource_provider.py +0 -0
  69. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/risk_profile.py +0 -0
  70. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/run_log.py +0 -0
  71. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runner.py +0 -0
  72. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtime.py +0 -0
  73. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/__init__.py +0 -0
  74. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/agent_llm_broker.py +0 -0
  75. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/agent_llm_policy.py +0 -0
  76. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/agent_sandbox.py +0 -0
  77. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/mcp_runner.py +0 -0
  78. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/python_runner.py +0 -0
  79. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/runtimes/remote_runner.py +0 -0
  80. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/__init__.py +0 -0
  81. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/agent_container_wrapper.py +0 -0
  82. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/agent_rpc.py +0 -0
  83. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/agent_session.py +0 -0
  84. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/backend.py +0 -0
  85. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/container_backend.py +0 -0
  86. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/policy.py +0 -0
  87. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/sandbox/types.py +0 -0
  88. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/signature.py +0 -0
  89. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/signing_key.py +0 -0
  90. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/agentnode_sdk/skill.py +0 -0
  91. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/sandbox-image/Dockerfile +0 -0
  92. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/sandbox-image/README.md +0 -0
  93. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/analyze_scores.py +0 -0
  94. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/batch_verify.py +0 -0
  95. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/ci_smoke_test.py +0 -0
  96. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/generate_compatibility_artifacts.py +0 -0
  97. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/verify_toolcalls.py +0 -0
  98. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/scripts/weekly_retest.sh +0 -0
  99. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/README.md +0 -0
  100. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/__init__.py +0 -0
  101. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/container_agent_wrapper.py +0 -0
  102. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/fake_llm.py +0 -0
  103. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/host_driver.py +0 -0
  104. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/spikes/agent_sandbox_routing/trivial_agent.py +0 -0
  105. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/__init__.py +0 -0
  106. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/conftest.py +0 -0
  107. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_llm_broker.py +0 -0
  108. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_llm_policy.py +0 -0
  109. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_rpc.py +0 -0
  110. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_runner.py +0 -0
  111. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_sandbox_e2e.py +0 -0
  112. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_sandbox_routing.py +0 -0
  113. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_sandbox_spike.py +0 -0
  114. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_agent_session_container.py +0 -0
  115. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_async_client.py +0 -0
  116. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_audit_ux.py +0 -0
  117. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_auto_upgrade_policy.py +0 -0
  118. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_cli.py +0 -0
  119. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_cli_lock.py +0 -0
  120. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_cli_run_resolution.py +0 -0
  121. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_client.py +0 -0
  122. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_client_json_guard.py +0 -0
  123. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_client_sprint_b.py +0 -0
  124. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_credential_handle.py +0 -0
  125. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_credential_integration.py +0 -0
  126. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_credential_resolver.py +0 -0
  127. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_credential_store.py +0 -0
  128. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_credential_vault.py +0 -0
  129. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_detect.py +0 -0
  130. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_detect_and_install.py +0 -0
  131. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_e2e_runtime.py +0 -0
  132. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_edge_cases.py +0 -0
  133. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard.py +0 -0
  134. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_check.py +0 -0
  135. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_config_cache.py +0 -0
  136. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_policy.py +0 -0
  137. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_preview.py +0 -0
  138. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_schema.py +0 -0
  139. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_set.py +0 -0
  140. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_status.py +0 -0
  141. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_tool_override.py +0 -0
  142. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_tool_override_audit.py +0 -0
  143. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_tool_override_cli.py +0 -0
  144. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_guard_ux.py +0 -0
  145. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_input_guard.py +0 -0
  146. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_input_guard_escalation.py +0 -0
  147. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_install_hardening.py +0 -0
  148. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_installer_sprint_b.py +0 -0
  149. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_intelligence.py +0 -0
  150. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_key_status.py +0 -0
  151. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_llm_binding.py +0 -0
  152. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_llm_call_runlog.py +0 -0
  153. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_lock_integrity.py +0 -0
  154. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_lock_runtime.py +0 -0
  155. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_mcp_audit.py +0 -0
  156. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_mcp_doctor.py +0 -0
  157. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_mcp_sandbox.py +0 -0
  158. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_mcp_server.py +0 -0
  159. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_observability.py +0 -0
  160. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_planner.py +0 -0
  161. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_policy.py +0 -0
  162. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_policy_integration.py +0 -0
  163. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_prompt_specs.py +0 -0
  164. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_provider_matrix.py +0 -0
  165. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_publish.py +0 -0
  166. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_references.py +0 -0
  167. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_registry_trust.py +0 -0
  168. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_remote_hardening.py +0 -0
  169. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_remote_runner.py +0 -0
  170. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_resource_provider.py +0 -0
  171. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_resource_specs.py +0 -0
  172. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_risk_profile.py +0 -0
  173. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_run_log.py +0 -0
  174. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_runner.py +0 -0
  175. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_runtime.py +0 -0
  176. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_runtime_audit.py +0 -0
  177. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_sandbox_backend.py +0 -0
  178. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_sandbox_doctor.py +0 -0
  179. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_sandbox_e2e.py +0 -0
  180. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_sandbox_gate.py +0 -0
  181. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_security_hardening.py +0 -0
  182. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_signature.py +0 -0
  183. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_signing_key.py +0 -0
  184. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_skill.py +0 -0
  185. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_smart.py +0 -0
  186. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_stability.py +0 -0
  187. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_toolpack_sandbox.py +0 -0
  188. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_v02.py +0 -0
  189. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_validate.py +0 -0
  190. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/test_validate_skill.py +0 -0
  191. {agentnode_sdk-0.14.0 → agentnode_sdk-0.16.0}/tests/validation_lockfile.json +0 -0
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 — Registry providers in auth CLI and setup wizard
4
+
5
+ ### Added
6
+
7
+ - **`agentnode auth status` now surfaces all registry providers** — OpenAI,
8
+ Anthropic, OpenRouter, DeepSeek, Mistral, Qwen, Gemini — with their storage
9
+ backend and effective source, plus **Ollama as a local keyless provider**
10
+ (shown as selected/configured/not selected instead of "missing"). Custom
11
+ providers configured under `llm.providers.<name>` appear automatically.
12
+ - **`agentnode auth test` supports the registry providers.** Compatible
13
+ providers are validated with a free, no-completion `GET {base_url}/models`
14
+ probe; `agentnode auth test ollama` is a keyless reachability check
15
+ (exit 0 reachable, exit 3 unreachable — never "rejected", and it never
16
+ starts Ollama). Custom configured endpoints can be tested the same way.
17
+ - **The setup wizard lists all registry providers**, grouped for readability
18
+ (Recommended / More / Local), with Skip remaining the default. Selecting
19
+ Ollama never asks for an API key — it sets `llm.default_provider` through
20
+ the wizard's normal save step.
21
+ - Additive `vendor` field in host-side LLM bindings: logs and tooling can now
22
+ show the real provider name; the protocol discriminator used by the broker
23
+ is unchanged.
24
+
25
+ ### Changed
26
+
27
+ - The auth CLI and the setup wizard now use the provider registry as their
28
+ single source of truth — no hardcoded provider lists or environment
29
+ variable names remain in those surfaces.
30
+ - OpenRouter keeps its dedicated `/auth/key` validation path (its `/models`
31
+ endpoint is unauthenticated and validates nothing).
32
+
33
+ ### Hardened
34
+
35
+ - Provider response bodies are never printed (they can echo key fragments);
36
+ keys are never printed, logged, or exposed in exceptions.
37
+ - Ollama is never started or probed automatically — only an explicit
38
+ `agentnode auth test ollama` performs a localhost reachability check.
39
+ - The `vendor` field stays host-side and does not change the broker/sandbox
40
+ wire shape.
41
+
42
+ ### BREAKING / Upgrade Notes
43
+
44
+ - None. All changes are additive; existing `auth` and `setup` flows behave
45
+ identically for the previously supported providers.
46
+
47
+ ## 0.15.0 — Generic OpenAI-compatible LLM providers
48
+
49
+ ### Added
50
+
51
+ - **Generic OpenAI-compatible provider registry.** The LLM runtime can now
52
+ bind any endpoint that speaks the OpenAI-compatible protocol. Custom
53
+ endpoints are plain config entries — `llm.providers.<name>` with
54
+ `base_url`, `model`, and optionally `api_key_env`/`requires_key` — no code
55
+ changes needed per provider.
56
+ - **Built-in presets** for OpenRouter, DeepSeek, Mistral, Qwen, Gemini
57
+ (Google's OpenAI-compatible endpoint) and Ollama, each with the canonical
58
+ base URL, API-key environment variable, and a sensible default model
59
+ (overridable via `llm.providers.<name>.model`).
60
+ - **Ollama as a key-free local provider** — the first agent path with no
61
+ account and no per-token cost. Opt-in only: select it explicitly with
62
+ `agentnode config set llm.default_provider ollama` (or configure it under
63
+ `llm.providers`); AgentNode never probes or binds it by surprise.
64
+ - Per-provider credentials: each provider's environment variable (e.g.
65
+ `DEEPSEEK_API_KEY`, `GEMINI_API_KEY`) and stored vault credentials both
66
+ work; the environment always overrides the stored key.
67
+
68
+ ### Changed
69
+
70
+ - The OpenRouter binding now goes through the shared provider registry
71
+ (same behavior, including the namespaced default model — one code path
72
+ instead of a special case).
73
+ - The sandboxed-agent LLM broker automatically inherits every compatible
74
+ provider: keys stay host-side, the container still sees only RPC
75
+ completions, and the C1/C2 access policy applies unchanged.
76
+
77
+ ### Hardened
78
+
79
+ - No provider keys in logs or error messages (client failures are reported
80
+ by exception type only).
81
+ - A custom provider without a configured model is skipped instead of
82
+ guessing — a wrong default model would fail confusingly at call time.
83
+ - Keyless providers are never attempted unless explicitly selected or
84
+ configured.
85
+
86
+ ### BREAKING / Upgrade Notes
87
+
88
+ - None. Existing OpenAI/Anthropic setups (env vars, vault, per-agent config)
89
+ behave identically; the new providers are purely additive.
90
+
3
91
  ## 0.14.0 — Setup wizard: guided credentials + sandbox onboarding
4
92
 
5
93
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentnode-sdk
3
- Version: 0.14.0
3
+ Version: 0.16.0
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
@@ -38,7 +38,7 @@ from agentnode_sdk.runtime import AgentNodeRuntime
38
38
  Client = AgentNodeClient
39
39
  ToolError = AgentNodeToolError
40
40
 
41
- __version__ = "0.14.0"
41
+ __version__ = "0.16.0"
42
42
  __all__ = [
43
43
  "AgentNode",
44
44
  "AsyncAgentNode",
@@ -10,7 +10,6 @@ import os
10
10
  import sys
11
11
 
12
12
  from agentnode_sdk.credential_store import (
13
- LLM_PROVIDER_ENV,
14
13
  has_credential,
15
14
  list_credentials,
16
15
  load_credentials,
@@ -30,17 +29,28 @@ def _storage_short(storage: str) -> str:
30
29
  return "OS keychain" if storage == "keyring" else "plaintext file"
31
30
 
32
31
 
33
- def _llm_effective_source(provider: str) -> tuple[str | None, str]:
32
+ def _host_config() -> dict:
33
+ try:
34
+ from agentnode_sdk.config import load_config
35
+ return load_config() or {}
36
+ except Exception:
37
+ return {}
38
+
39
+
40
+ def _llm_effective_source(provider: str, host_config: dict | None = None) -> tuple[str | None, str]:
34
41
  """(effective key or None, human-readable source) for an LLM provider.
35
42
 
36
43
  Mirrors the runtime's resolution: env (incl. ~/.agentnode/.env) overrides
37
- the stored credential. Uses metadata only for the stored check here —
44
+ the stored credential. Env var names come from the provider REGISTRY
45
+ (single source of truth). Uses metadata only for the stored check here —
38
46
  the real keychain read happens in ``auth test``."""
47
+ from agentnode_sdk.llm_providers import resolve_provider_spec
39
48
  from agentnode_sdk.runtimes.agent_runner import _load_agentnode_env
40
49
  _load_agentnode_env()
41
50
 
42
- env_var = LLM_PROVIDER_ENV[provider]
43
- env_key = os.environ.get(env_var) or None
51
+ spec = resolve_provider_spec(provider, host_config) or {}
52
+ env_var = spec.get("api_key_env") or ""
53
+ env_key = (os.environ.get(env_var) or None) if env_var else None
44
54
  meta = load_credentials().get("providers", {}).get(provider)
45
55
  stored = isinstance(meta, dict)
46
56
  storage = (meta or {}).get("storage", "file")
@@ -54,6 +64,19 @@ def _llm_effective_source(provider: str) -> tuple[str | None, str]:
54
64
  return None, "not configured"
55
65
 
56
66
 
67
+ def _registry_key_providers(host_config: dict | None) -> list[str]:
68
+ """Key-requiring providers from the registry (presets + custom config
69
+ entries), in stable registry order."""
70
+ from agentnode_sdk.llm_providers import known_provider_names, resolve_provider_spec
71
+
72
+ names: list[str] = []
73
+ for name in known_provider_names(host_config):
74
+ spec = resolve_provider_spec(name, host_config)
75
+ if spec and spec.get("requires_key", True):
76
+ names.append(name)
77
+ return names
78
+
79
+
57
80
  def _resolve_auth_type(provider: str) -> str:
58
81
  """Read auth_type from lockfile connector entry. Falls back to 'api_key'.
59
82
 
@@ -132,15 +155,25 @@ def cmd_auth_remove(provider: str) -> int:
132
155
 
133
156
  def cmd_auth_status() -> int:
134
157
  # --- LLM providers (host runtime / sandbox broker) -----------------------
158
+ host_cfg = _host_config()
135
159
  print()
136
160
  print(section("LLM Providers"))
137
161
  print(f" {'Provider':<14}{'Status':<16}{'Effective source'}")
138
162
  print(f" {'--------':<14}{'------':<16}{'----------------'}")
139
- for provider in sorted(LLM_PROVIDER_ENV):
140
- key, source = _llm_effective_source(provider)
163
+ for provider in _registry_key_providers(host_cfg):
164
+ key, source = _llm_effective_source(provider, host_cfg)
141
165
  configured = key is not None or source not in ("not configured",)
142
166
  status = "configured" if configured else "missing"
143
167
  print(f" {provider:<14}{status:<16}{source}")
168
+ # ollama: keyless local provider — selection is config, not a credential
169
+ llm_cfg = host_cfg.get("llm") or {}
170
+ if llm_cfg.get("default_provider") == "ollama":
171
+ ollama_source = "selected via llm.default_provider"
172
+ elif "ollama" in (llm_cfg.get("providers") or {}):
173
+ ollama_source = "configured via llm.providers"
174
+ else:
175
+ ollama_source = "not selected — agentnode config set llm.default_provider ollama"
176
+ print(f" {'ollama':<14}{'local (keyless)':<16}{ollama_source}")
144
177
  print(dim(" Env vars always override stored credentials. "
145
178
  "Test a key: agentnode auth test <provider>"))
146
179
 
@@ -183,61 +216,97 @@ def cmd_auth_status() -> int:
183
216
 
184
217
 
185
218
  # Free, cost-less validation endpoints (no completion call, no charge).
186
- # anthropic REQUIRES the anthropic-version header (a 400 without it would be
187
- # misread as a bad key); openrouter's /models is UNAUTHENTICATED (validates
188
- # nothing) /auth/key is the correct probe.
189
- _TEST_REQUESTS = {
190
- "openai": lambda key: (
219
+ # GENERIC rule for OpenAI-compatible providers: GET {base_url}/models with a
220
+ # Bearer token (authenticated on deepseek/mistral/qwen/gemini and most custom
221
+ # endpoints). EXCEPTIONS: openai (official URL), anthropic (REQUIRES the
222
+ # anthropic-version header — a 400 without it would be misread as a bad key),
223
+ # openrouter (its /models is UNAUTHENTICATED, validates nothing — /auth/key is
224
+ # the correct probe).
225
+ _TEST_EXCEPTIONS = {
226
+ "openai": lambda key, spec: (
191
227
  "https://api.openai.com/v1/models",
192
228
  {"Authorization": f"Bearer {key}"},
193
229
  ),
194
- "anthropic": lambda key: (
230
+ "anthropic": lambda key, spec: (
195
231
  "https://api.anthropic.com/v1/models",
196
232
  {"x-api-key": key, "anthropic-version": "2023-06-01"},
197
233
  ),
198
- "openrouter": lambda key: (
234
+ "openrouter": lambda key, spec: (
199
235
  "https://openrouter.ai/api/v1/auth/key",
200
236
  {"Authorization": f"Bearer {key}"},
201
237
  ),
202
238
  }
203
239
 
204
240
 
241
+ def _test_request(provider: str, key: str | None, spec: dict) -> tuple[str, dict] | None:
242
+ if provider in _TEST_EXCEPTIONS:
243
+ return _TEST_EXCEPTIONS[provider](key, spec)
244
+ base = (spec.get("base_url") or "").rstrip("/")
245
+ if not base:
246
+ return None # no compat endpoint to probe
247
+ headers = {"Authorization": f"Bearer {key}"} if key else {}
248
+ return (base + "/models", headers)
249
+
250
+
205
251
  def cmd_auth_test(provider: str) -> int:
206
252
  """Validate the EFFECTIVE key for an LLM provider (env beats vault) via a
207
- free endpoint. Exit codes: 0 valid / 1 rejected (401/403) / 2 not
208
- configured or unsupported / 3 indeterminate (network/5xx never reported
209
- as invalid). Never prints the key (masked last-4 only) and never echoes
210
- the provider's response body (it can contain key fragments)."""
253
+ free endpoint registry-driven, so custom ``llm.providers`` entries work
254
+ too. Exit codes: 0 valid (key providers) / reachable (keyless) · 1
255
+ rejected (401/403, key providers only) · 2 not configured or unsupported ·
256
+ 3 indeterminate (network/5xx never reported as invalid; for keyless
257
+ providers this means unreachable). Never prints the key (masked last-4
258
+ only), never echoes provider response bodies (they can contain key
259
+ fragments), never starts anything."""
211
260
  provider = provider.lower().strip()
212
- if provider not in _TEST_REQUESTS:
213
- supported = ", ".join(sorted(_TEST_REQUESTS))
214
- print(f" auth test supports: {supported}", file=sys.stderr)
261
+ host_cfg = _host_config()
262
+ from agentnode_sdk.llm_providers import resolve_provider_spec
263
+ spec = resolve_provider_spec(provider, host_cfg)
264
+ if spec is None:
265
+ print(f" Unknown provider '{provider}'. Known: see `agentnode auth status`; "
266
+ "custom endpoints go under llm.providers.<name>.", file=sys.stderr)
215
267
  return 2
216
268
 
217
- key, source = _llm_effective_source(provider)
218
- if key is None:
219
- # No env override — resolve the stored credential (real keychain read).
220
- from agentnode_sdk.credential_store import get_llm_api_key
221
- key = get_llm_api_key(provider)
222
- if not key:
223
- print(f"\n {provider}: not configured. Run `agentnode auth set {provider}`.\n")
269
+ requires_key = spec.get("requires_key", True)
270
+ if requires_key:
271
+ key, source = _llm_effective_source(provider, host_cfg)
272
+ if key is None:
273
+ # No env override — resolve the stored credential (real keychain read).
274
+ from agentnode_sdk.credential_store import get_llm_api_key
275
+ key = get_llm_api_key(provider)
276
+ if not key:
277
+ print(f"\n {provider}: not configured. Run `agentnode auth set {provider}`.\n")
278
+ return 2
279
+ shown = f"{_masked(key)}, {source}"
280
+ else:
281
+ key, shown = None, "local (keyless)"
282
+
283
+ req = _test_request(provider, key, spec)
284
+ if req is None:
285
+ print(f"\n {provider}: no endpoint to probe (no base_url configured).\n")
224
286
  return 2
287
+ url, headers = req
225
288
 
226
- url, headers = _TEST_REQUESTS[provider](key)
227
289
  import httpx
228
290
  try:
229
291
  resp = httpx.get(url, headers=headers, timeout=10.0)
230
292
  except Exception:
231
- print(f"\n {provider} ({_masked(key)}, {source}): could not reach the "
232
- "provider key validity unknown.\n")
293
+ if not requires_key:
294
+ print(f"\n {provider} ({shown}): could not reach {url} — "
295
+ "is the local server (e.g. Ollama) running?\n")
296
+ else:
297
+ print(f"\n {provider} ({shown}): could not reach the "
298
+ "provider — key validity unknown.\n")
233
299
  return 3
234
300
  if resp.status_code == 200:
235
- print(f"\n {provider} ({_masked(key)}, {source}): key is valid.\n")
301
+ if requires_key:
302
+ print(f"\n {provider} ({shown}): key is valid.\n")
303
+ else:
304
+ print(f"\n {provider} ({shown}): endpoint is reachable.\n")
236
305
  return 0
237
- if resp.status_code in (401, 403):
238
- print(f"\n {provider} ({_masked(key)}, {source}): key was rejected by "
306
+ if requires_key and resp.status_code in (401, 403):
307
+ print(f"\n {provider} ({shown}): key was rejected by "
239
308
  f"the provider (HTTP {resp.status_code}).\n")
240
309
  return 1
241
- print(f"\n {provider} ({_masked(key)}, {source}): unexpected provider "
242
- f"response (HTTP {resp.status_code}) — key validity unknown.\n")
310
+ print(f"\n {provider} ({shown}): unexpected response "
311
+ f"(HTTP {resp.status_code}) — status unknown.\n")
243
312
  return 3
@@ -22,7 +22,14 @@ from agentnode_sdk.config import (
22
22
  )
23
23
  from agentnode_sdk.cli.output import bold, dim, kv, section
24
24
 
25
- _LLM_CHOICES = {"1": "openai", "2": "anthropic", "3": "openrouter"}
25
+ # Key-provider menu (grouped for readability); choice 8 = ollama (keyless,
26
+ # handled separately — never a key prompt), choice 9 = skip (the default).
27
+ _LLM_CHOICES = {
28
+ "1": "openai", "2": "anthropic", "3": "openrouter",
29
+ "4": "deepseek", "5": "mistral", "6": "qwen", "7": "gemini",
30
+ }
31
+ _OLLAMA_CHOICE = "8"
32
+ _SKIP_CHOICE = "9"
26
33
 
27
34
 
28
35
  def run_wizard() -> int:
@@ -59,15 +66,16 @@ def _store_llm_key(provider: str) -> str | None:
59
66
  import os
60
67
 
61
68
  from agentnode_sdk.cli.auth import _masked, _storage_short, cmd_auth_test
62
- from agentnode_sdk.credential_store import LLM_PROVIDER_ENV, set_credential
69
+ from agentnode_sdk.credential_store import set_credential
63
70
 
64
- env_var = LLM_PROVIDER_ENV[provider]
71
+ from agentnode_sdk.llm_providers import resolve_provider_spec
72
+ env_var = (resolve_provider_spec(provider) or {}).get("api_key_env") or ""
65
73
  try:
66
74
  from agentnode_sdk.runtimes.agent_runner import _load_agentnode_env
67
75
  _load_agentnode_env()
68
76
  except Exception:
69
77
  pass
70
- env_val = (os.environ.get(env_var) or "").strip()
78
+ env_val = (os.environ.get(env_var) or "").strip() if env_var else ""
71
79
 
72
80
  token = ""
73
81
  if env_val:
@@ -101,9 +109,11 @@ def _store_llm_key(provider: str) -> str | None:
101
109
  return _storage_short(storage)
102
110
 
103
111
 
104
- def _credentials_screen() -> list[tuple[str, str]]:
105
- """Screen 5: LLM credentials (optional, default = skip). Returns the
106
- (provider, storage label) pairs stored in this run."""
112
+ def _credentials_screen(cfg: dict) -> list[tuple[str, str]]:
113
+ """Screen 5: LLM credentials (optional, default = skip). Key providers
114
+ come from the registry-backed menu; ollama is a keyless CONFIG selection
115
+ (written to the wizard cfg, persisted only via the normal Save confirm).
116
+ Returns the (provider, storage label) pairs selected in this run."""
107
117
  import sys
108
118
 
109
119
  print()
@@ -120,19 +130,38 @@ def _credentials_screen() -> list[tuple[str, str]]:
120
130
  return []
121
131
 
122
132
  stored: list[tuple[str, str]] = []
133
+ all_options = list(_LLM_CHOICES) + [_OLLAMA_CHOICE, _SKIP_CHOICE]
123
134
  while True:
124
- print(" [1] OpenAI [2] Anthropic [3] OpenRouter [4] Skip for now")
125
- c = _choice(" Choice", ["1", "2", "3", "4"], "4")
126
- if c == "4":
135
+ print(" Recommended: [1] OpenAI [2] Anthropic [3] OpenRouter")
136
+ print(" More: [4] DeepSeek [5] Mistral [6] Qwen [7] Gemini")
137
+ print(" Local: [8] Ollama keyless, requires a running Ollama")
138
+ print(" [9] Skip for now")
139
+ c = _choice(" Choice", all_options, _SKIP_CHOICE)
140
+ if c == _SKIP_CHOICE:
127
141
  break
128
- provider = _LLM_CHOICES[c]
129
- if any(p == provider for p, _ in stored):
130
- print(f" {provider} already added in this run.")
142
+ if c == _OLLAMA_CHOICE:
143
+ # keyless local provider: a config selection, NOT a credential.
144
+ # No key prompt, no probing, no starting anything.
145
+ if any(p == "ollama" for p, _ in stored):
146
+ print(" ollama already selected in this run.")
147
+ else:
148
+ print(" Ollama runs models locally — no API key, no per-token cost.")
149
+ print(dim(" Requires a running Ollama (https://ollama.com); the wizard"))
150
+ print(dim(" does not start or probe it."))
151
+ use = _prompt(" Use Ollama as your default LLM provider? [Y/n]: ", "y")
152
+ if use.lower() != "n":
153
+ cfg["llm"]["default_provider"] = "ollama"
154
+ stored.append(("ollama", "local, keyless"))
155
+ print(dim(" Selected. Test later with `agentnode auth test ollama`."))
131
156
  else:
132
- label = _store_llm_key(provider)
133
- if label is not None:
134
- stored.append((provider, label))
135
- if len(stored) >= len(_LLM_CHOICES):
157
+ provider = _LLM_CHOICES[c]
158
+ if any(p == provider for p, _ in stored):
159
+ print(f" {provider} already added in this run.")
160
+ else:
161
+ label = _store_llm_key(provider)
162
+ if label is not None:
163
+ stored.append((provider, label))
164
+ if len(stored) >= len(_LLM_CHOICES) + 1:
136
165
  break
137
166
  more = _prompt(" Add another provider? [y/N]: ", "n")
138
167
  if more.lower() != "y":
@@ -283,8 +312,9 @@ def _wizard_flow() -> dict | None:
283
312
  c = _choice(" Choice", ["1", "2", "3"], "1")
284
313
  cfg["trust"]["minimum_trust_level"] = {"1": "verified", "2": "trusted", "3": "curated"}[c]
285
314
 
286
- # Screen 5: LLM credentials (optional — UX-3A)
287
- stored_providers = _credentials_screen()
315
+ # Screen 5: LLM credentials (optional — UX-3A; provider list from the
316
+ # registry incl. keyless ollama — Endpoint-B)
317
+ stored_providers = _credentials_screen(cfg)
288
318
 
289
319
  # Screen 6: Local sandbox (UX-3B)
290
320
  sandbox_status, image_missing = _sandbox_screen(cfg)
@@ -46,6 +46,7 @@ DEFAULTS: dict[str, Any] = {
46
46
  },
47
47
  "llm": {
48
48
  "default_provider": "openai",
49
+ "providers": {},
49
50
  },
50
51
  }
51
52
 
@@ -68,7 +69,10 @@ VALID_VALUES: dict[str, tuple[str, ...]] = {
68
69
  "guard.compute": ("allow", "prompt", "deny"),
69
70
  "guard.unknown": ("allow", "prompt", "deny"),
70
71
  "agent_sandbox.enabled": ("true", "false"),
71
- "llm.default_provider": ("openai", "anthropic", "openrouter"),
72
+ "llm.default_provider": (
73
+ "openai", "anthropic", "openrouter", "deepseek", "mistral", "qwen",
74
+ "gemini", "ollama",
75
+ ),
72
76
  }
73
77
 
74
78
 
@@ -160,6 +164,10 @@ def _merge_defaults(data: dict) -> dict[str, Any]:
160
164
  for k in ("default_provider",):
161
165
  if k in data["llm"]:
162
166
  cfg["llm"][k] = data["llm"][k]
167
+ # llm.providers holds nested endpoint specs (base_url/model/...) —
168
+ # pass through verbatim, like agent_sandbox.llm above.
169
+ if isinstance(data["llm"].get("providers"), dict):
170
+ cfg["llm"]["providers"] = data["llm"]["providers"]
163
171
  return cfg
164
172
 
165
173
 
@@ -47,12 +47,19 @@ CREDENTIALS_FILE = "credentials.json"
47
47
  CURRENT_VERSION = 1
48
48
 
49
49
  # Canonical env vars for the LLM providers the host runtime understands.
50
+ # DERIVED from the provider registry (single source of truth, Endpoint-B).
50
51
  # Env always OVERRIDES the stored credential (explicit/CI intent wins).
51
- LLM_PROVIDER_ENV: dict[str, str] = {
52
- "openai": "OPENAI_API_KEY",
53
- "anthropic": "ANTHROPIC_API_KEY",
54
- "openrouter": "OPENROUTER_API_KEY",
55
- }
52
+ def _derive_llm_provider_env() -> dict[str, str]:
53
+ from agentnode_sdk.llm_providers import KNOWN_PROVIDERS
54
+
55
+ return {
56
+ name: spec["api_key_env"]
57
+ for name, spec in KNOWN_PROVIDERS.items()
58
+ if spec.get("api_key_env")
59
+ }
60
+
61
+
62
+ LLM_PROVIDER_ENV: dict[str, str] = _derive_llm_provider_env()
56
63
 
57
64
  # One keychain item per provider: service "agentnode:<provider>", a fixed
58
65
  # username. (NOT service="agentnode" + username=provider — Windows WinVault
@@ -0,0 +1,116 @@
1
+ """OpenAI-compatible LLM provider registry (Endpoint-A).
2
+
3
+ One shared source of truth for the LLM endpoints AgentNode can bind: built-in
4
+ presets (OpenRouter, DeepSeek, Mistral, Qwen, Ollama — plus the official
5
+ OpenAI/Anthropic entries) and user-defined entries/overrides from
6
+ ``llm.providers.<name>`` in ~/.agentnode/config.json.
7
+
8
+ The runtime (``_auto_detect_llm``) builds standard OpenAI-compatible bindings
9
+ from these specs, so the host LLM broker — and therefore sandboxed agents —
10
+ inherit every provider automatically, with the key staying host-side and no
11
+ broker/sandbox change.
12
+
13
+ Compat presets MUST carry a default model: the broker's bare OpenAI default
14
+ ("gpt-4o-mini") would 404 on most compatible endpoints. ``requires_key:
15
+ False`` providers (Ollama) are bound only on explicit selection — never by
16
+ probing.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ # Built-in presets. base_url=None means the provider SDK's official endpoint.
23
+ # default_model="" lets the broker pick its own official default.
24
+ KNOWN_PROVIDERS: dict[str, dict[str, Any]] = {
25
+ "openai": {
26
+ "base_url": None,
27
+ "api_key_env": "OPENAI_API_KEY",
28
+ "default_model": "",
29
+ "requires_key": True,
30
+ },
31
+ "anthropic": {
32
+ "base_url": None,
33
+ "api_key_env": "ANTHROPIC_API_KEY",
34
+ "default_model": "",
35
+ "requires_key": True,
36
+ },
37
+ "openrouter": {
38
+ "base_url": "https://openrouter.ai/api/v1",
39
+ "api_key_env": "OPENROUTER_API_KEY",
40
+ "default_model": "openai/gpt-4o-mini",
41
+ "requires_key": True,
42
+ },
43
+ "deepseek": {
44
+ "base_url": "https://api.deepseek.com/v1",
45
+ "api_key_env": "DEEPSEEK_API_KEY",
46
+ "default_model": "deepseek-chat",
47
+ "requires_key": True,
48
+ },
49
+ "mistral": {
50
+ "base_url": "https://api.mistral.ai/v1",
51
+ "api_key_env": "MISTRAL_API_KEY",
52
+ "default_model": "mistral-small-latest",
53
+ "requires_key": True,
54
+ },
55
+ "qwen": {
56
+ "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
57
+ "api_key_env": "DASHSCOPE_API_KEY",
58
+ "default_model": "qwen-plus",
59
+ "requires_key": True,
60
+ },
61
+ "gemini": {
62
+ # Google's own OpenAI-compatible endpoint (trailing slash required).
63
+ "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
64
+ "api_key_env": "GEMINI_API_KEY",
65
+ "default_model": "gemini-2.0-flash",
66
+ "requires_key": True,
67
+ },
68
+ "ollama": {
69
+ # key-free local inference: the first agent path with no account and
70
+ # no per-token cost. Bound ONLY when explicitly selected (no probing).
71
+ "base_url": "http://localhost:11434/v1",
72
+ "api_key_env": "OLLAMA_API_KEY",
73
+ "default_model": "llama3.2",
74
+ "requires_key": False,
75
+ },
76
+ }
77
+
78
+ _SPEC_KEYS = ("base_url", "api_key_env", "default_model", "requires_key")
79
+
80
+
81
+ def _config_providers(host_config: dict | None) -> dict:
82
+ llm = (host_config or {}).get("llm") or {}
83
+ providers = llm.get("providers")
84
+ return providers if isinstance(providers, dict) else {}
85
+
86
+
87
+ def resolve_provider_spec(name: str, host_config: dict | None = None) -> dict | None:
88
+ """Effective spec for ``name`` = preset merged with the config override
89
+ (``llm.providers.<name>``), or None for an unknown name without a config
90
+ entry. ``model`` is accepted as a friendlier config alias for
91
+ ``default_model``."""
92
+ name = (name or "").lower().strip()
93
+ preset = KNOWN_PROVIDERS.get(name)
94
+ override = _config_providers(host_config).get(name)
95
+ if preset is None and not isinstance(override, dict):
96
+ return None
97
+
98
+ spec: dict[str, Any] = dict(preset) if preset else {
99
+ "base_url": None, "api_key_env": "", "default_model": "", "requires_key": True,
100
+ }
101
+ if isinstance(override, dict):
102
+ for k in _SPEC_KEYS:
103
+ if k in override:
104
+ spec[k] = override[k]
105
+ if "model" in override:
106
+ spec["default_model"] = override["model"]
107
+ return spec
108
+
109
+
110
+ def known_provider_names(host_config: dict | None = None) -> list[str]:
111
+ """Preset names first (stable order), then custom config-defined names."""
112
+ names = list(KNOWN_PROVIDERS)
113
+ for n in _config_providers(host_config):
114
+ if n not in names:
115
+ names.append(n)
116
+ return names