agentnode-sdk 0.11.4__tar.gz → 0.12.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 (184) hide show
  1. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/CHANGELOG.md +60 -0
  2. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/PKG-INFO +1 -1
  3. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/__init__.py +1 -1
  4. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/templates.py +15 -0
  5. agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_llm_broker.py +83 -0
  6. agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_llm_policy.py +184 -0
  7. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/agent_runner.py +7 -0
  8. agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_sandbox.py +218 -0
  9. agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_container_wrapper.py +83 -0
  10. agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_rpc.py +156 -0
  11. agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_session.py +68 -0
  12. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/backend.py +16 -0
  13. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/container_backend.py +81 -1
  14. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/pyproject.toml +1 -1
  15. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/README.md +111 -0
  16. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/__init__.py +9 -0
  17. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/container_agent_wrapper.py +73 -0
  18. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/fake_llm.py +14 -0
  19. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/host_driver.py +145 -0
  20. agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/trivial_agent.py +38 -0
  21. agentnode_sdk-0.12.0/tests/test_agent_llm_broker.py +179 -0
  22. agentnode_sdk-0.12.0/tests/test_agent_llm_policy.py +279 -0
  23. agentnode_sdk-0.12.0/tests/test_agent_rpc.py +186 -0
  24. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_agent_runner.py +84 -0
  25. agentnode_sdk-0.12.0/tests/test_agent_sandbox_e2e.py +175 -0
  26. agentnode_sdk-0.12.0/tests/test_agent_sandbox_routing.py +420 -0
  27. agentnode_sdk-0.12.0/tests/test_agent_sandbox_spike.py +87 -0
  28. agentnode_sdk-0.12.0/tests/test_agent_session_container.py +173 -0
  29. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/.env.example +0 -0
  30. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/.gitignore +0 -0
  31. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/README.md +0 -0
  32. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_ACTIVATION.md +0 -0
  33. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_SPEC.md +0 -0
  34. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/THREAT_MODEL.md +0 -0
  35. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/TRUST_STACK.md +0 -0
  36. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode.lock +0 -0
  37. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/_fileutil.py +0 -0
  38. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/async_client.py +0 -0
  39. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_graph.py +0 -0
  40. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_taxonomy.py +0 -0
  41. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__init__.py +0 -0
  42. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__main__.py +0 -0
  43. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/audit.py +0 -0
  44. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/auth.py +0 -0
  45. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/cassette_audit.py +0 -0
  46. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/commands.py +0 -0
  47. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/complements.py +0 -0
  48. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/init.py +0 -0
  49. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/main.py +0 -0
  50. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_commands.py +0 -0
  51. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_status.py +0 -0
  52. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_submit.py +0 -0
  53. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_verify.py +0 -0
  54. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/output.py +0 -0
  55. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/publish.py +0 -0
  56. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/record_cases.py +0 -0
  57. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/sandbox_commands.py +0 -0
  58. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/serve.py +0 -0
  59. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/setup_wizard.py +0 -0
  60. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/smart_run.py +0 -0
  61. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/validate.py +0 -0
  62. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/verify_local.py +0 -0
  63. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/client.py +0 -0
  64. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/compatibility.py +0 -0
  65. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/config.py +0 -0
  66. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_handle.py +0 -0
  67. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_resolver.py +0 -0
  68. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_store.py +0 -0
  69. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/detect.py +0 -0
  70. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/exceptions.py +0 -0
  71. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/guard.py +0 -0
  72. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/input_guard.py +0 -0
  73. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/installer.py +0 -0
  74. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/key_status.py +0 -0
  75. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/lock_integrity.py +0 -0
  76. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/mcp_server.py +0 -0
  77. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/models.py +0 -0
  78. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/planner.py +0 -0
  79. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/policy.py +0 -0
  80. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/references.py +0 -0
  81. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/registry_trust.py +0 -0
  82. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/resolve.py +0 -0
  83. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/resource_provider.py +0 -0
  84. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/risk_profile.py +0 -0
  85. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/run_log.py +0 -0
  86. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runner.py +0 -0
  87. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtime.py +0 -0
  88. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/__init__.py +0 -0
  89. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/mcp_runner.py +0 -0
  90. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/python_runner.py +0 -0
  91. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/remote_runner.py +0 -0
  92. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/__init__.py +0 -0
  93. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/policy.py +0 -0
  94. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/types.py +0 -0
  95. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/signature.py +0 -0
  96. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/signing_key.py +0 -0
  97. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/skill.py +0 -0
  98. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/sandbox-image/Dockerfile +0 -0
  99. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/sandbox-image/README.md +0 -0
  100. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/analyze_scores.py +0 -0
  101. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/batch_verify.py +0 -0
  102. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/ci_smoke_test.py +0 -0
  103. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/generate_compatibility_artifacts.py +0 -0
  104. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/verify_toolcalls.py +0 -0
  105. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/weekly_retest.sh +0 -0
  106. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/__init__.py +0 -0
  107. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/conftest.py +0 -0
  108. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_async_client.py +0 -0
  109. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_audit_ux.py +0 -0
  110. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_auto_upgrade_policy.py +0 -0
  111. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli.py +0 -0
  112. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli_lock.py +0 -0
  113. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli_run_resolution.py +0 -0
  114. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client.py +0 -0
  115. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client_json_guard.py +0 -0
  116. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client_sprint_b.py +0 -0
  117. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_config.py +0 -0
  118. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_handle.py +0 -0
  119. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_integration.py +0 -0
  120. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_resolver.py +0 -0
  121. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_store.py +0 -0
  122. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_detect.py +0 -0
  123. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_detect_and_install.py +0 -0
  124. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_e2e_runtime.py +0 -0
  125. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_edge_cases.py +0 -0
  126. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard.py +0 -0
  127. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_check.py +0 -0
  128. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_config_cache.py +0 -0
  129. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_policy.py +0 -0
  130. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_preview.py +0 -0
  131. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_schema.py +0 -0
  132. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_set.py +0 -0
  133. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_status.py +0 -0
  134. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override.py +0 -0
  135. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_audit.py +0 -0
  136. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_cli.py +0 -0
  137. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_ux.py +0 -0
  138. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_input_guard.py +0 -0
  139. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_input_guard_escalation.py +0 -0
  140. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_install_hardening.py +0 -0
  141. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_installer_sprint_b.py +0 -0
  142. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_intelligence.py +0 -0
  143. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_key_status.py +0 -0
  144. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_llm_binding.py +0 -0
  145. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_llm_call_runlog.py +0 -0
  146. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_lock_integrity.py +0 -0
  147. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_lock_runtime.py +0 -0
  148. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_audit.py +0 -0
  149. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_doctor.py +0 -0
  150. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_sandbox.py +0 -0
  151. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_server.py +0 -0
  152. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_observability.py +0 -0
  153. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_planner.py +0 -0
  154. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_policy.py +0 -0
  155. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_policy_integration.py +0 -0
  156. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_prompt_specs.py +0 -0
  157. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_provider_matrix.py +0 -0
  158. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_publish.py +0 -0
  159. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_references.py +0 -0
  160. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_registry_trust.py +0 -0
  161. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_remote_hardening.py +0 -0
  162. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_remote_runner.py +0 -0
  163. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_resource_provider.py +0 -0
  164. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_resource_specs.py +0 -0
  165. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_risk_profile.py +0 -0
  166. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_run_log.py +0 -0
  167. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runner.py +0 -0
  168. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runtime.py +0 -0
  169. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runtime_audit.py +0 -0
  170. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_backend.py +0 -0
  171. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_doctor.py +0 -0
  172. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_e2e.py +0 -0
  173. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_gate.py +0 -0
  174. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_security_hardening.py +0 -0
  175. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_signature.py +0 -0
  176. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_signing_key.py +0 -0
  177. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_skill.py +0 -0
  178. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_smart.py +0 -0
  179. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_stability.py +0 -0
  180. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_toolpack_sandbox.py +0 -0
  181. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_v02.py +0 -0
  182. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_validate.py +0 -0
  183. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_validate_skill.py +0 -0
  184. {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/validation_lockfile.json +0 -0
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 — Sandboxed community agents (flag-gated)
4
+
5
+ ### Added
6
+
7
+ - **Agent sandbox (default OFF):** with `AGENTNODE_AGENT_SANDBOX=1` (or config
8
+ `agent_sandbox.enabled: true`), `verified`/`unverified` community agents run
9
+ **sandbox-or-fail-closed** in the pinned container image — never on the host,
10
+ with **no host fallback** anywhere on the path. Tool calls cross an
11
+ allowlisted RPC back to the host's gated runner (the host owns allowlist and
12
+ limits); `trusted`/`curated` agents are unchanged. With the flag OFF
13
+ (default), community agents remain refused exactly as in 0.11.4.
14
+ - **Host-side LLM broker:** sandboxed agents request completions via RPC; the
15
+ provider call runs host-side and **provider API keys never enter the
16
+ container** (the container env is only `PYTHONPATH=/pack`).
17
+ - **`llm_access` manifest block (default-deny):** a sandboxed agent gets NO
18
+ host LLM unless its manifest declares `llm_access.enabled: true` — analogous
19
+ to `tool_access`. Caps: `max_calls`, `max_input_chars`, `max_output_chars`;
20
+ optional `allowed_models` checks the HOST-chosen model (the agent never picks
21
+ a model; absent = unrestricted, `[]` = refuse-all, manifest+host = both must
22
+ allow). The host ceiling (`agent_sandbox.llm` in `~/.agentnode/config.json`)
23
+ always wins — it can lower caps, restrict models, or disable access entirely.
24
+ Refused/failed LLM calls return **graceful per-call errors** the agent can
25
+ catch; they never crash the run and never fall back to the host.
26
+ - **Audit:** every sandboxed run writes ONE aggregated, sanitized record to
27
+ `~/.agentnode/audit.jsonl` (`event: agent_run`, `source: agent_sandbox`) —
28
+ counters, caps, and fixed reason codes only; **never prompts, keys, raw
29
+ provider errors, or agent-authored error text**. Fail-closed refusals
30
+ (missing volume/runtime, session-start failure) are audited too.
31
+ - Agent manifest template (`agentnode init`) now documents the opt-in
32
+ `llm_access` block with the caps and `allowed_models`.
33
+
34
+ ### Changed
35
+
36
+ - `agentnode init` agent template includes the `llm_access` example (newly
37
+ scaffolded packages only; existing manifests are unaffected — an absent
38
+ `llm_access` simply means deny).
39
+
40
+ ### Hardened
41
+
42
+ - The sandbox path is fail-closed end to end: missing/stale volume, missing
43
+ container runtime or pinned image, sandbox-start failure, or a host-loop
44
+ error all return a clean `sandbox_unavailable`/error result — community
45
+ agent code never executes on the host.
46
+ - LLM broker errors are generic and leak-free (no key, no provider internals,
47
+ no prompt echo). A model-allowlist refusal never calls the provider (no
48
+ charge), and the host-side model name is never sent into the sandbox.
49
+
50
+ ### BREAKING / Upgrade Notes
51
+
52
+ - **None.** With the flag OFF (default), behavior is identical to 0.11.4.
53
+ There are no flag-ON users yet (the flag ships first in this release).
54
+ - Enabling the agent sandbox requires a container runtime plus the pinned
55
+ public sandbox image — `agentnode sandbox pull` to fetch it,
56
+ `agentnode sandbox doctor` for diagnosis.
57
+ - Operational note for managed hosts (e.g. Coolify): automatic image pruning
58
+ can remove the pinned sandbox image, degrading community execution to
59
+ fail-closed until it is re-pulled. Keep the image pinned (e.g. a minimal
60
+ keep-alive holder container referencing the digest) or re-pull on a
61
+ schedule.
62
+
3
63
  ## 0.11.4 — Publish confirm gate
4
64
 
5
65
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentnode-sdk
3
- Version: 0.11.4
3
+ Version: 0.12.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.11.4"
41
+ __version__ = "0.12.0"
42
42
  __all__ = [
43
43
  "AgentNode",
44
44
  "AsyncAgentNode",
@@ -655,6 +655,21 @@ agent:
655
655
  tier: "llm_only"
656
656
  llm:
657
657
  required: true
658
+ llm_access:
659
+ # Host-brokered LLM access for SANDBOXED community runs (opt-in).
660
+ # When your agent runs sandboxed on someone else's machine, it can only
661
+ # reach their LLM credentials through the host broker, and only if you
662
+ # set enabled: true here. Default false = no access (calls fail gracefully).
663
+ # The host's own ceiling (agent_sandbox.llm in the host config) ALWAYS
664
+ # wins: it can lower these caps or disable access entirely.
665
+ enabled: false
666
+ max_calls: 20
667
+ max_input_chars: 24000
668
+ max_output_chars: 24000
669
+ # Optionally restrict which host-chosen models may serve this agent
670
+ # (the agent cannot pick a model; the host does). Omit = any host model.
671
+ # An explicit empty list means no model is acceptable.
672
+ # allowed_models: ["gpt-4o-mini", "claude-3-5-haiku-latest"]
658
673
  system_prompt: |
659
674
  You are a helpful agent that accomplishes goals step by step.
660
675
  Think carefully, use available tools, and return a clear result.
@@ -0,0 +1,83 @@
1
+ """Host-side LLM broker for sandboxed agents (B2b-1).
2
+
3
+ A sandboxed community agent requests an LLM completion via the ``call_llm`` RPC;
4
+ the HOST runs the actual provider call here. The provider API key stays host-side
5
+ and NEVER enters the sandbox — the broker receives only ``messages`` and returns
6
+ only a structured completion. A PURE RELAY: it never interprets messages as host
7
+ commands. Errors are GENERIC — the raw provider exception (which can contain the
8
+ key, request URLs, or internal details) is never surfaced to the agent.
9
+
10
+ Credential policy (caps, default-deny, audit) lives in ``agent_llm_policy`` —
11
+ this module is only the secure provider plumbing. C2 adds one policy hook here:
12
+ an optional ``allowed_models`` check, because the effective model (incl. the
13
+ default fallback) is only known at this point.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+
19
+ logger = logging.getLogger("agentnode.agent_sandbox")
20
+
21
+ # Used only when the host LLM binding does not specify a model.
22
+ _DEFAULT_MODELS = {
23
+ "openai": "gpt-4o-mini",
24
+ "anthropic": "claude-3-5-haiku-latest",
25
+ }
26
+
27
+
28
+ class LlmBrokerError(RuntimeError):
29
+ """Generic, leak-free LLM broker failure (no key, no provider internals)."""
30
+
31
+
32
+ class LlmModelNotAllowedError(LlmBrokerError):
33
+ """C2: the host-chosen model is outside the effective ``allowed_models`` set.
34
+
35
+ The MESSAGE stays generic (it crosses into the sandbox); the host-side model
36
+ name travels only on the ``model`` attribute, for audit.
37
+ """
38
+
39
+ def __init__(self, model: str = ""):
40
+ super().__init__("the host-configured LLM model is not allowed for this agent")
41
+ self.model = model
42
+
43
+
44
+ def host_llm_broker(messages: list, *, allowed_models=None) -> dict:
45
+ """Run one LLM completion HOST-side and return ``{"role","content"}``.
46
+
47
+ ``allowed_models`` (optional, C2 defense-in-depth): a set of model ids the
48
+ host-chosen model must be in — the agent never picks a model, this only
49
+ checks the one the host resolved (incl. the default fallback). Not allowed →
50
+ :class:`LlmModelNotAllowedError` WITHOUT calling the provider.
51
+
52
+ Raises :class:`LlmBrokerError` (generic) on any failure — no provider /
53
+ missing SDK / provider exception — never leaking the key or the raw provider
54
+ exception text.
55
+ """
56
+ from agentnode_sdk.runtimes.agent_runner import _auto_detect_llm
57
+
58
+ binding = _auto_detect_llm()
59
+ if not binding:
60
+ raise LlmBrokerError("no LLM provider configured on the host")
61
+
62
+ client = binding.get("client")
63
+ provider = binding.get("provider")
64
+ model = binding.get("model") or _DEFAULT_MODELS.get(provider or "", "")
65
+ if client is None or provider not in _DEFAULT_MODELS:
66
+ raise LlmBrokerError("unsupported or unavailable LLM provider")
67
+
68
+ if allowed_models is not None and model not in allowed_models:
69
+ logger.warning("LLM broker refused a model outside allowed_models")
70
+ raise LlmModelNotAllowedError(model)
71
+
72
+ try:
73
+ if provider == "openai":
74
+ resp = client.chat.completions.create(model=model, messages=list(messages or []))
75
+ content = resp.choices[0].message.content
76
+ else: # anthropic
77
+ resp = client.messages.create(model=model, max_tokens=1024, messages=list(messages or []))
78
+ content = resp.content[0].text
79
+ except Exception as exc: # never surface the raw provider error (may carry the key)
80
+ logger.warning("LLM broker provider call failed: %s", type(exc).__name__)
81
+ raise LlmBrokerError("LLM provider call failed")
82
+
83
+ return {"role": "assistant", "content": content or ""}
@@ -0,0 +1,184 @@
1
+ """C1 — host-credential LLM broker policy for sandboxed community agents.
2
+
3
+ This protects the HOST USER's LLM credentials/wallet from UNTRUSTED
4
+ (verified/unverified) sandboxed community code that borrows the host key via the
5
+ broker. It is NOT a limit on agents in general: trusted/curated/self-run agents
6
+ (own key, host path) are unaffected — they keep "run until done".
7
+
8
+ DEFAULT-DENY: like ``tool_access``, an agent reaches the host LLM only if it
9
+ explicitly declares ``llm_access.enabled: true`` in its manifest. The host-config
10
+ ceiling (``agent_sandbox.llm`` in ~/.agentnode/config.json) ALWAYS wins — it can
11
+ lower the caps or force-disable, and a higher manifest value is clamped to it.
12
+
13
+ Refusals/errors come back as a STRUCTURED ``{"ok": False, "error": ...}`` so the
14
+ RPC host can return them as graceful per-call errors the agent can catch — never
15
+ a whole-run crash, never a host fallback. Errors are sanitized: no key, no prompt,
16
+ no raw provider internals.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+
22
+ from agentnode_sdk.runtimes.agent_llm_broker import LlmBrokerError, LlmModelNotAllowedError
23
+
24
+ # Conservative built-in ceilings: the fallback when the host config does not set a
25
+ # field, and the default the manifest request is clamped against.
26
+ _DEFAULT_MAX_CALLS = 20
27
+ _DEFAULT_MAX_INPUT_CHARS = 24_000
28
+ _DEFAULT_MAX_OUTPUT_CHARS = 24_000
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class LlmAccessPolicy:
33
+ """Resolved per-run LLM access policy for one sandboxed community agent.
34
+
35
+ ``allowed_models`` (C2): ``None`` = unrestricted (the host picks the model
36
+ anyway); a frozenset = only those host-chosen models may serve the agent
37
+ (an empty set refuses all — same convention as tool_access.allowed_packages).
38
+ """
39
+
40
+ enabled: bool = False
41
+ max_calls: int = 0
42
+ max_input_chars: int = 0
43
+ max_output_chars: int = 0
44
+ allowed_models: frozenset | None = None
45
+
46
+
47
+ def _ceiling(host_llm: dict, key: str, default: int) -> int:
48
+ """The host-config ceiling for ``key`` (a positive int), else the built-in."""
49
+ try:
50
+ v = int(host_llm[key])
51
+ except (KeyError, TypeError, ValueError):
52
+ return int(default)
53
+ return v if v > 0 else int(default)
54
+
55
+
56
+ def _request(manifest: dict, key: str, ceiling: int) -> int:
57
+ """The manifest's requested value for ``key`` (positive int), else the ceiling."""
58
+ try:
59
+ v = int(manifest[key])
60
+ except (KeyError, TypeError, ValueError):
61
+ return ceiling
62
+ return v if v > 0 else ceiling
63
+
64
+
65
+ def _models_set(section: dict) -> frozenset | None:
66
+ """``allowed_models`` as a frozenset, or ``None`` when absent / not a list.
67
+
68
+ Convention (mirrors ``tool_access.allowed_packages``): absent → ``None`` =
69
+ unrestricted; an explicit ``[]`` → empty set = no model is acceptable.
70
+ """
71
+ raw = section.get("allowed_models")
72
+ if not isinstance(raw, list):
73
+ return None
74
+ return frozenset(s for s in (str(x).strip() for x in raw) if s)
75
+
76
+
77
+ def resolve_llm_policy(agent_config: dict | None, host_config: dict | None = None) -> LlmAccessPolicy:
78
+ """Resolve the effective policy = min(manifest request, host ceiling).
79
+
80
+ Default-deny: a missing ``llm_access`` or ``enabled != true`` → disabled. The
81
+ host config can also force-disable (``agent_sandbox.llm.enabled: false``).
82
+ ``allowed_models``: both sides set → intersection (both must allow); one side
83
+ set → that side; neither → unrestricted.
84
+ """
85
+ manifest = ((agent_config or {}).get("llm_access")) or {}
86
+ host_llm = ((((host_config or {}).get("agent_sandbox")) or {}).get("llm")) or {}
87
+
88
+ manifest_enabled = manifest.get("enabled") is True
89
+ host_enabled = host_llm.get("enabled", True) # host omits → defer to manifest
90
+ if not (manifest_enabled and host_enabled is not False):
91
+ return LlmAccessPolicy(enabled=False)
92
+
93
+ mc = _ceiling(host_llm, "max_calls", _DEFAULT_MAX_CALLS)
94
+ ic = _ceiling(host_llm, "max_input_chars", _DEFAULT_MAX_INPUT_CHARS)
95
+ oc = _ceiling(host_llm, "max_output_chars", _DEFAULT_MAX_OUTPUT_CHARS)
96
+ m, h = _models_set(manifest), _models_set(host_llm)
97
+ models = (m & h) if (m is not None and h is not None) else (m if m is not None else h)
98
+ return LlmAccessPolicy(
99
+ enabled=True,
100
+ max_calls=min(_request(manifest, "max_calls", mc), mc),
101
+ max_input_chars=min(_request(manifest, "max_input_chars", ic), ic),
102
+ max_output_chars=min(_request(manifest, "max_output_chars", oc), oc),
103
+ allowed_models=models,
104
+ )
105
+
106
+
107
+ def _input_chars(messages) -> int:
108
+ return sum(len(str((m or {}).get("content", "") or "")) for m in (messages or []))
109
+
110
+
111
+ def make_policy_broker(policy: LlmAccessPolicy, base_broker):
112
+ """Wrap ``base_broker`` (the host LLM broker) with policy enforcement.
113
+
114
+ Returns a callable ``(messages) -> {"ok": bool, "completion"?|"error"?}``.
115
+ Per-run state (the call counter) lives in this closure, so one run gets one
116
+ fresh budget. Never raises — all failures are structured + sanitized so the
117
+ RPC host turns them into graceful per-call errors (no host fallback).
118
+
119
+ C2: the callable exposes ``broker.usage`` (live per-run counters for the
120
+ aggregated audit record — counts, reason codes, and at most the HOST-side
121
+ model name; never message content) and ``broker.policy`` (the resolved
122
+ policy). ``allowed_models`` is forwarded to ``base_broker`` ONLY when the
123
+ policy sets it, so plain single-arg brokers/fakes keep working unchanged.
124
+ """
125
+ state = {
126
+ "requests": 0,
127
+ "calls": 0,
128
+ "ok": 0,
129
+ "refused_disabled": 0,
130
+ "refused_limit": 0,
131
+ "refused_input": 0,
132
+ "refused_output": 0,
133
+ "refused_model": 0,
134
+ "provider_errors": 0,
135
+ "model": None,
136
+ }
137
+
138
+ def broker(messages):
139
+ state["requests"] += 1
140
+ if not policy.enabled:
141
+ state["refused_disabled"] += 1
142
+ return {"ok": False, "error":
143
+ "LLM access not granted: this agent did not declare llm_access.enabled"}
144
+
145
+ state["calls"] += 1
146
+ if state["calls"] > policy.max_calls:
147
+ state["refused_limit"] += 1
148
+ return {"ok": False, "error": "LLM call limit reached for this run"}
149
+
150
+ if _input_chars(messages) > policy.max_input_chars:
151
+ state["refused_input"] += 1
152
+ return {"ok": False, "error": "LLM request exceeds the allowed input size"}
153
+
154
+ try:
155
+ if policy.allowed_models is not None:
156
+ completion = base_broker(messages, allowed_models=policy.allowed_models)
157
+ else:
158
+ completion = base_broker(messages)
159
+ except LlmModelNotAllowedError as exc:
160
+ # Generic message to the agent; the host-side model name goes only
161
+ # into the usage counters (for audit), never into the sandbox.
162
+ state["refused_model"] += 1
163
+ state["model"] = getattr(exc, "model", "") or None
164
+ return {"ok": False, "error": str(exc)}
165
+ except LlmBrokerError as exc:
166
+ # LlmBrokerError is our own already-sanitized type (no key/internals).
167
+ state["provider_errors"] += 1
168
+ return {"ok": False, "error": str(exc)}
169
+ except Exception:
170
+ # Never surface a raw provider/host exception (may carry secrets).
171
+ state["provider_errors"] += 1
172
+ return {"ok": False, "error": "LLM call failed"}
173
+
174
+ content = str(completion.get("content", "") or "") if isinstance(completion, dict) else ""
175
+ if len(content) > policy.max_output_chars:
176
+ state["refused_output"] += 1
177
+ return {"ok": False, "error": "LLM response exceeds the allowed output size"}
178
+
179
+ state["ok"] += 1
180
+ return {"ok": True, "completion": completion}
181
+
182
+ broker.usage = state
183
+ broker.policy = policy
184
+ return broker
@@ -1252,6 +1252,13 @@ def run_agent(
1252
1252
  # or community code runs unsandboxed (reintroducing the RCE class the sandbox
1253
1253
  # bow closed). Locked by test_agent_runner's execution-vector regression test.
1254
1254
  trust_level = entry.get("trust_level", "unverified")
1255
+ # B2a: with the agent-sandbox flag ON, community (verified/unverified) agents
1256
+ # run sandboxed-or-fail-closed (never host) instead of being refused. Flag OFF
1257
+ # (default) ⇒ this branch is skipped ⇒ behaviour is unchanged (the gate below
1258
+ # refuses them). trusted/curated always fall through to the host path.
1259
+ from agentnode_sdk.runtimes.agent_sandbox import _agent_sandbox_enabled, run_agent_sandboxed
1260
+ if _agent_sandbox_enabled() and trust_level in ("verified", "unverified"):
1261
+ return run_agent_sandboxed(slug, entry, agent_config, goal=goal, run_id=run_id, **kwargs)
1255
1262
  if not _trust_meets_minimum(trust_level, "trusted"):
1256
1263
  _audit_agent_run(
1257
1264
  slug, success=False,
@@ -0,0 +1,218 @@
1
+ """B2a: route community (verified/unverified) agents through a sandboxed session,
2
+ behind a default-OFF feature flag.
3
+
4
+ Policy (only when the flag is ON):
5
+ verified / unverified -> sandbox-or-fail-closed (this module); NEVER host.
6
+ trusted / curated -> host (handled in run_agent, unchanged).
7
+ unknown / missing -> refused (run_agent's existing gate).
8
+
9
+ This is the FIRST behaviour change of the agent-sandbox bow. With the flag OFF
10
+ (default) nothing here runs and run_agent behaves exactly as before. No host
11
+ fallback: if no backend/volume is available, fail-closed. Tool-calls go host-side
12
+ through the real ``runner.run_tool`` (via AgentRpcHost); LLM calls go through the
13
+ host-side broker (B2b-1) behind a default-DENY credential policy (C1), and every
14
+ sandboxed run leaves ONE aggregated, sanitized audit record (C2). See
15
+ docs/design/agent-sandbox-architecture.md.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import os
21
+ import re
22
+ import subprocess
23
+ import uuid
24
+
25
+ logger = logging.getLogger("agentnode.agent_sandbox")
26
+
27
+
28
+ def _agent_sandbox_enabled() -> bool:
29
+ """Default OFF. Env ``AGENTNODE_AGENT_SANDBOX`` wins, else config
30
+ ``agent_sandbox.enabled`` in ~/.agentnode/config.json."""
31
+ if os.environ.get("AGENTNODE_AGENT_SANDBOX", "").strip().lower() in ("1", "true"):
32
+ return True
33
+ try:
34
+ from agentnode_sdk.config import load_config
35
+ section = (load_config() or {}).get("agent_sandbox") or {}
36
+ return bool(section.get("enabled", False))
37
+ except Exception:
38
+ return False
39
+
40
+
41
+ def _audit_sandbox_run(slug, *, trust_level, run_id, success, reason,
42
+ policy=None, usage=None, events=None) -> None:
43
+ """C2: ONE aggregated audit record per sandboxed agent run (reuses
44
+ ``policy.audit_decision``; no new storage). Sanitized by construction:
45
+ fixed reason codes, counters, and caps only — never prompts, keys, raw
46
+ provider errors, or agent-authored error text. Never crashes the caller."""
47
+ try:
48
+ from agentnode_sdk.policy import PolicyResult, audit_decision
49
+
50
+ llm = None
51
+ if policy is not None:
52
+ llm = {
53
+ "enabled": policy.enabled,
54
+ "max_calls": policy.max_calls,
55
+ "max_input_chars": policy.max_input_chars,
56
+ "max_output_chars": policy.max_output_chars,
57
+ "allowed_models": (sorted(policy.allowed_models)
58
+ if policy.allowed_models is not None else None),
59
+ }
60
+ llm.update(usage or {})
61
+ ev = events or []
62
+ audit_decision(
63
+ PolicyResult(action="allow" if success else "deny",
64
+ reason=reason, source="agent_sandbox"),
65
+ "agent_run",
66
+ slug,
67
+ trust_level=trust_level,
68
+ run_id=run_id,
69
+ extra={
70
+ "sandbox": True,
71
+ "llm": llm,
72
+ "tool_calls": sum(1 for e in ev if e and e[0] == "run_tool"),
73
+ "tool_refusals": sum(
74
+ 1 for e in ev if e and e[0] in ("refused_allowlist", "refused_limit")),
75
+ },
76
+ )
77
+ except Exception:
78
+ logger.debug("Failed to audit sandboxed agent run: %s", slug, exc_info=True)
79
+
80
+
81
+ def run_agent_sandboxed(slug, entry, agent_config, *, goal=None, run_id=None, **kwargs):
82
+ """Run a community agent's entrypoint inside the sandbox. Returns a
83
+ RunToolResult. Fail-closed on a missing/stale volume or no runtime — never
84
+ runs the agent on the host."""
85
+ from agentnode_sdk.models import RunToolResult
86
+ from agentnode_sdk.sandbox import get_default_backend, sandbox_volume_name
87
+ from agentnode_sdk.sandbox.agent_container_wrapper import WRAPPER_SOURCE
88
+ from agentnode_sdk.sandbox.agent_rpc import AgentRpcHost
89
+ from agentnode_sdk.sandbox.types import MountSpec, ProcessSpec
90
+
91
+ def _fail(error: str, mode: str = "sandbox_unavailable",
92
+ reason_code: str = "fail_closed") -> "RunToolResult":
93
+ # C2: fail-closed refusals get an audit record too (deny + fixed code).
94
+ _audit_sandbox_run(slug, trust_level=(entry or {}).get("trust_level"),
95
+ run_id=run_id, success=False, reason=reason_code)
96
+ return RunToolResult(success=False, error=error, mode_used=mode, run_id=run_id)
97
+
98
+ agent_config = agent_config or {}
99
+ entrypoint = agent_config.get("entrypoint", "")
100
+ if not entrypoint:
101
+ return _fail(f"Agent '{slug}' has no entrypoint defined.",
102
+ mode="agent_sandbox", reason_code="no_entrypoint")
103
+
104
+ limits = agent_config.get("limits") or {}
105
+ max_tool_calls = limits.get("max_tool_calls", 40)
106
+ timeout = float(limits.get("max_runtime_seconds", 180))
107
+ allowed = (agent_config.get("tool_access") or {}).get("allowed_packages")
108
+ effective_goal = goal or agent_config.get("goal", "")
109
+
110
+ # Volume gate — mirror python_runner._run_container; never trust the lockfile
111
+ # blindly. The community agent's code was built into this volume at install.
112
+ expected_vol = sandbox_volume_name(slug, entry.get("version"), entry.get("artifact_hash"))
113
+ if not entry.get("sandboxed") or entry.get("sandbox_volume") != expected_vol:
114
+ return _fail(
115
+ f"Agent sandbox volume missing or stale — reinstall '{slug}' "
116
+ f"(run: agentnode install {slug}).",
117
+ reason_code="volume_missing",
118
+ )
119
+
120
+ backend = get_default_backend()
121
+ avail = backend.check_available()
122
+ if not avail.available:
123
+ return _fail(
124
+ "Agent execution requires a container runtime + the pinned image. "
125
+ f"{avail.reason or 'none available'} — refusing to run community agent "
126
+ "code on the host.",
127
+ reason_code="backend_unavailable",
128
+ )
129
+
130
+ runtime = avail.backend or "docker"
131
+ try:
132
+ insp = subprocess.run(
133
+ [runtime, "volume", "inspect", expected_vol],
134
+ capture_output=True, timeout=10,
135
+ )
136
+ except Exception as exc:
137
+ return _fail(f"Could not verify sandbox volume: {exc}",
138
+ reason_code="volume_inspect_failed")
139
+ if insp.returncode != 0:
140
+ return _fail(f"Agent sandbox volume missing — reinstall '{slug}'.",
141
+ reason_code="volume_missing")
142
+
143
+ safe = re.sub(r"[^a-zA-Z0-9_.-]", "-", slug)[:40] or "agent"
144
+ spec = ProcessSpec(
145
+ command=["python", "-c", WRAPPER_SOURCE],
146
+ network="none",
147
+ env={"PYTHONPATH": "/pack"},
148
+ mounts=[MountSpec(src=expected_vol, dst="/pack", read_only=True)],
149
+ clean_home=True,
150
+ interactive=True,
151
+ name=f"agentnode-agent-{safe}-{uuid.uuid4().hex[:8]}",
152
+ )
153
+
154
+ # B2a: STRICT host-side allowlist (the agent's declared tool_access). A
155
+ # community agent with no declared allowlist gets no tool access — broadening
156
+ # "unrestricted" community agents is a later decision.
157
+ # B2b-1: LLM calls go through the host-side broker (the provider key stays on
158
+ # the host, never in the container).
159
+ # C1: that broker is wrapped by a default-DENY credential policy — the agent
160
+ # reaches the host LLM key only if it declared llm_access.enabled, and the
161
+ # host-config ceiling (agent_sandbox.llm) always wins. Refusals come back as
162
+ # graceful per-call errors (the agent can catch them), never a host fallback.
163
+ from agentnode_sdk.runtimes.agent_llm_broker import host_llm_broker
164
+ from agentnode_sdk.runtimes.agent_llm_policy import make_policy_broker, resolve_llm_policy
165
+
166
+ try:
167
+ from agentnode_sdk.config import load_config
168
+ host_cfg = load_config() or {}
169
+ except Exception:
170
+ host_cfg = {}
171
+ policy = resolve_llm_policy(agent_config, host_cfg)
172
+ policy_broker = make_policy_broker(policy, host_llm_broker)
173
+
174
+ # Fail-closed: a sandbox-START failure (e.g. the runtime vanished between the
175
+ # availability check and the launch) returns a clean sandbox_unavailable —
176
+ # it never raises out and never falls back to the host.
177
+ try:
178
+ session = backend.open_agent_session(spec)
179
+ except Exception as exc:
180
+ return _fail(f"Could not start the agent sandbox: {exc}",
181
+ reason_code="session_start_failed")
182
+
183
+ host = AgentRpcHost(
184
+ allowed_packages=allowed or [],
185
+ max_tool_calls=max_tool_calls,
186
+ llm_broker=policy_broker,
187
+ )
188
+ try:
189
+ out = host.run(
190
+ session,
191
+ init={"entrypoint": entrypoint, "goal": effective_goal, "kwargs": kwargs},
192
+ timeout=timeout,
193
+ )
194
+ except Exception as exc:
195
+ _audit_sandbox_run(slug, trust_level=entry.get("trust_level"), run_id=run_id,
196
+ success=False, reason=f"host_loop_error: {type(exc).__name__}",
197
+ policy=policy, usage=policy_broker.usage, events=host.events)
198
+ return RunToolResult(
199
+ success=False, error=f"Sandboxed agent failed: {exc}",
200
+ mode_used="agent_sandbox", run_id=run_id,
201
+ )
202
+ finally:
203
+ session.close()
204
+
205
+ res = out.get("result") or {}
206
+ events = out.get("events") or []
207
+ if res.get("ok"):
208
+ _audit_sandbox_run(slug, trust_level=entry.get("trust_level"), run_id=run_id,
209
+ success=True, reason="agent_completed",
210
+ policy=policy, usage=policy_broker.usage, events=events)
211
+ return RunToolResult(success=True, result=res.get("value"), mode_used="agent_sandbox", run_id=run_id)
212
+ _audit_sandbox_run(slug, trust_level=entry.get("trust_level"), run_id=run_id,
213
+ success=False, reason="agent_error",
214
+ policy=policy, usage=policy_broker.usage, events=events)
215
+ return RunToolResult(
216
+ success=False, error=res.get("error", "sandboxed agent failed"),
217
+ mode_used="agent_sandbox", run_id=run_id,
218
+ )