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.
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/CHANGELOG.md +60 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/PKG-INFO +1 -1
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/__init__.py +1 -1
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/templates.py +15 -0
- agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_llm_broker.py +83 -0
- agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_llm_policy.py +184 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/agent_runner.py +7 -0
- agentnode_sdk-0.12.0/agentnode_sdk/runtimes/agent_sandbox.py +218 -0
- agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_container_wrapper.py +83 -0
- agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_rpc.py +156 -0
- agentnode_sdk-0.12.0/agentnode_sdk/sandbox/agent_session.py +68 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/backend.py +16 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/container_backend.py +81 -1
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/pyproject.toml +1 -1
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/README.md +111 -0
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/__init__.py +9 -0
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/container_agent_wrapper.py +73 -0
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/fake_llm.py +14 -0
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/host_driver.py +145 -0
- agentnode_sdk-0.12.0/spikes/agent_sandbox_routing/trivial_agent.py +38 -0
- agentnode_sdk-0.12.0/tests/test_agent_llm_broker.py +179 -0
- agentnode_sdk-0.12.0/tests/test_agent_llm_policy.py +279 -0
- agentnode_sdk-0.12.0/tests/test_agent_rpc.py +186 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_agent_runner.py +84 -0
- agentnode_sdk-0.12.0/tests/test_agent_sandbox_e2e.py +175 -0
- agentnode_sdk-0.12.0/tests/test_agent_sandbox_routing.py +420 -0
- agentnode_sdk-0.12.0/tests/test_agent_sandbox_spike.py +87 -0
- agentnode_sdk-0.12.0/tests/test_agent_session_container.py +173 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/.env.example +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/.gitignore +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/README.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_ACTIVATION.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_SPEC.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/THREAT_MODEL.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/TRUST_STACK.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode.lock +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/_fileutil.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/async_client.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_graph.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_taxonomy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__init__.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__main__.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/audit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/auth.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/cassette_audit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/commands.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/complements.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/init.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/main.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_commands.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_status.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_submit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_verify.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/output.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/publish.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/record_cases.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/sandbox_commands.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/serve.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/setup_wizard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/smart_run.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/validate.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/verify_local.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/client.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/compatibility.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/config.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_handle.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_resolver.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_store.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/detect.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/exceptions.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/guard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/input_guard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/installer.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/key_status.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/lock_integrity.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/mcp_server.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/models.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/planner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/policy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/references.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/registry_trust.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/resolve.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/resource_provider.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/risk_profile.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/run_log.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtime.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/__init__.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/mcp_runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/python_runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/remote_runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/__init__.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/policy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/types.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/signature.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/signing_key.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/agentnode_sdk/skill.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/sandbox-image/Dockerfile +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/sandbox-image/README.md +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/analyze_scores.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/batch_verify.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/ci_smoke_test.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/generate_compatibility_artifacts.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/verify_toolcalls.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/scripts/weekly_retest.sh +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/__init__.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/conftest.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_async_client.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_audit_ux.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_auto_upgrade_policy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli_lock.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_cli_run_resolution.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client_json_guard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_client_sprint_b.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_config.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_handle.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_integration.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_resolver.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_credential_store.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_detect.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_detect_and_install.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_e2e_runtime.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_edge_cases.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_check.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_config_cache.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_policy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_preview.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_schema.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_set.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_status.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_audit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_cli.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_guard_ux.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_input_guard.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_input_guard_escalation.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_install_hardening.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_installer_sprint_b.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_intelligence.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_key_status.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_llm_binding.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_llm_call_runlog.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_lock_integrity.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_lock_runtime.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_audit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_doctor.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_sandbox.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_mcp_server.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_observability.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_planner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_policy.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_policy_integration.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_prompt_specs.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_provider_matrix.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_publish.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_references.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_registry_trust.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_remote_hardening.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_remote_runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_resource_provider.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_resource_specs.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_risk_profile.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_run_log.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runner.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runtime.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_runtime_audit.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_backend.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_doctor.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_e2e.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_sandbox_gate.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_security_hardening.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_signature.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_signing_key.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_skill.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_smart.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_stability.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_toolpack_sandbox.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_v02.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_validate.py +0 -0
- {agentnode_sdk-0.11.4 → agentnode_sdk-0.12.0}/tests/test_validate_skill.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
+
)
|