agentnode-sdk 0.11.3__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.3 → agentnode_sdk-0.12.0}/CHANGELOG.md +78 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/PKG-INFO +1 -1
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/__init__.py +1 -1
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/main.py +2 -1
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/publish.py +39 -0
- {agentnode_sdk-0.11.3 → 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.3 → 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.3 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/backend.py +16 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/container_backend.py +81 -1
- {agentnode_sdk-0.11.3 → 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.3 → 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.3 → agentnode_sdk-0.12.0}/tests/test_publish.py +90 -5
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/.env.example +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/.gitignore +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/README.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_ACTIVATION.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/REGISTRY_SIGNING_SPEC.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/THREAT_MODEL.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/TRUST_STACK.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode.lock +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/_fileutil.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/async_client.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_graph.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/capability_taxonomy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__init__.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/__main__.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/audit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/auth.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/cassette_audit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/commands.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/complements.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/init.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_commands.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_status.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_submit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/mcp_verify.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/output.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/record_cases.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/sandbox_commands.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/serve.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/setup_wizard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/smart_run.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/validate.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/cli/verify_local.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/client.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/compatibility.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/config.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_handle.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_resolver.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/credential_store.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/detect.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/exceptions.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/guard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/input_guard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/installer.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/key_status.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/lock_integrity.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/mcp_server.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/models.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/planner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/policy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/references.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/registry_trust.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/resolve.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/resource_provider.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/risk_profile.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/run_log.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runtime.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/__init__.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/mcp_runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/python_runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/runtimes/remote_runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/__init__.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/policy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/sandbox/types.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/signature.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/signing_key.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/agentnode_sdk/skill.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/sandbox-image/Dockerfile +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/sandbox-image/README.md +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/analyze_scores.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/batch_verify.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/ci_smoke_test.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/generate_compatibility_artifacts.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/verify_toolcalls.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/scripts/weekly_retest.sh +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/__init__.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/conftest.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_async_client.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_audit_ux.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_auto_upgrade_policy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_cli.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_cli_lock.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_cli_run_resolution.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_client.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_client_json_guard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_client_sprint_b.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_config.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_credential_handle.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_credential_integration.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_credential_resolver.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_credential_store.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_detect.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_detect_and_install.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_e2e_runtime.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_edge_cases.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_check.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_config_cache.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_policy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_preview.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_schema.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_set.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_status.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_audit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_tool_override_cli.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_guard_ux.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_input_guard.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_input_guard_escalation.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_install_hardening.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_installer_sprint_b.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_intelligence.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_key_status.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_llm_binding.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_llm_call_runlog.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_lock_integrity.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_lock_runtime.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_mcp_audit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_mcp_doctor.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_mcp_sandbox.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_mcp_server.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_observability.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_planner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_policy.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_policy_integration.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_prompt_specs.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_provider_matrix.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_references.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_registry_trust.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_remote_hardening.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_remote_runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_resource_provider.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_resource_specs.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_risk_profile.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_run_log.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_runner.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_runtime.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_runtime_audit.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_sandbox_backend.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_sandbox_doctor.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_sandbox_e2e.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_sandbox_gate.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_security_hardening.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_signature.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_signing_key.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_skill.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_smart.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_stability.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_toolpack_sandbox.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_v02.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_validate.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/test_validate_skill.py +0 -0
- {agentnode_sdk-0.11.3 → agentnode_sdk-0.12.0}/tests/validation_lockfile.json +0 -0
|
@@ -1,5 +1,83 @@
|
|
|
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
|
+
|
|
63
|
+
## 0.11.4 — Publish confirm gate
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **`agentnode publish` now asks for confirmation before publishing.** After the
|
|
68
|
+
preview, the command prompts `Publish <pkg>@<version> to <registry>? [y/N]`
|
|
69
|
+
(default No) and only uploads on explicit `y`. A new `--yes`/`-y` flag skips
|
|
70
|
+
the prompt for CI/automation. `--dry-run` is unchanged (never prompts, never
|
|
71
|
+
publishes). Prevents accidental publishes of the wrong package/version/folder.
|
|
72
|
+
|
|
73
|
+
### BREAKING / Upgrade Notes
|
|
74
|
+
|
|
75
|
+
- **Non-interactive publish now requires `--yes`.** Previously `agentnode publish`
|
|
76
|
+
in a non-interactive context (CI, piped stdin, or `AGENTNODE_NON_INTERACTIVE=1`)
|
|
77
|
+
uploaded silently. It now **refuses** without `--yes` and exits non-zero.
|
|
78
|
+
Automation that publishes must add `--yes`. Interactive use is unaffected
|
|
79
|
+
beyond the new prompt.
|
|
80
|
+
|
|
3
81
|
## 0.11.3 — Test hygiene + multi-tool run guidance
|
|
4
82
|
|
|
5
83
|
### Fixed
|
|
@@ -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
|
|
@@ -154,6 +154,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
154
154
|
publish_parser.add_argument("--dry-run", action="store_true", help="Validate and build without uploading")
|
|
155
155
|
publish_parser.add_argument("--skip-validate", action="store_true", help="Continue despite validation errors")
|
|
156
156
|
publish_parser.add_argument("--token", default=None, help="API key (default: AGENTNODE_API_KEY env)")
|
|
157
|
+
publish_parser.add_argument("--yes", "-y", action="store_true", help="Skip the confirmation prompt (required for non-interactive/CI publish)")
|
|
157
158
|
|
|
158
159
|
# record-cases
|
|
159
160
|
record_parser = sub.add_parser("record-cases", help="Record VCR cassettes for API verification cases")
|
|
@@ -338,7 +339,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
338
339
|
return commands.cmd_verify_local(args.path)
|
|
339
340
|
if args.command == "publish":
|
|
340
341
|
from agentnode_sdk.cli.publish import cmd_publish
|
|
341
|
-
return cmd_publish(args.path, dry_run=args.dry_run, skip_validate=args.skip_validate, token=args.token)
|
|
342
|
+
return cmd_publish(args.path, dry_run=args.dry_run, skip_validate=args.skip_validate, token=args.token, yes=args.yes)
|
|
342
343
|
if args.command == "record-cases":
|
|
343
344
|
return commands.cmd_record_cases(args.path, strict=args.strict)
|
|
344
345
|
if args.command == "inspect":
|
|
@@ -286,12 +286,38 @@ def _post_publish(
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
def _confirm_publish(
|
|
290
|
+
pkg_id: str, version: str, registry: str, *, yes: bool, interactive: bool
|
|
291
|
+
) -> bool:
|
|
292
|
+
"""Final consent gate before an external publish. Returns True to proceed.
|
|
293
|
+
|
|
294
|
+
- ``yes``: explicit bypass for CI/automation → proceed.
|
|
295
|
+
- not ``interactive``: refuse — never publish non-interactively without --yes.
|
|
296
|
+
- interactive: prompt ``[y/N]``, default No.
|
|
297
|
+
"""
|
|
298
|
+
if yes:
|
|
299
|
+
return True
|
|
300
|
+
if not interactive:
|
|
301
|
+
print(
|
|
302
|
+
" Refusing to publish non-interactively. "
|
|
303
|
+
"Re-run with --yes to confirm in CI/automation.",
|
|
304
|
+
file=sys.stderr,
|
|
305
|
+
)
|
|
306
|
+
return False
|
|
307
|
+
answer = input(f" Publish {pkg_id}@{version} to {registry}? [y/N] ").strip().lower()
|
|
308
|
+
if answer in ("y", "yes"):
|
|
309
|
+
return True
|
|
310
|
+
print(" Publish cancelled.")
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
289
314
|
def cmd_publish(
|
|
290
315
|
path_str: str,
|
|
291
316
|
*,
|
|
292
317
|
dry_run: bool = False,
|
|
293
318
|
skip_validate: bool = False,
|
|
294
319
|
token: str | None = None,
|
|
320
|
+
yes: bool = False,
|
|
295
321
|
) -> int:
|
|
296
322
|
"""Publish a package to the AgentNode registry."""
|
|
297
323
|
from agentnode_sdk.cli.validate import validate_package_dir
|
|
@@ -389,6 +415,19 @@ def cmd_publish(
|
|
|
389
415
|
)
|
|
390
416
|
return 1
|
|
391
417
|
|
|
418
|
+
# Publish confirm gate — publish is an external, hard-to-undo action.
|
|
419
|
+
# Detect interactivity robustly (CI/pipes have a non-TTY stdin even without
|
|
420
|
+
# AGENTNODE_NON_INTERACTIVE set); non-interactive requires explicit --yes.
|
|
421
|
+
registry = _resolve_api_base().replace("https://", "").replace("http://", "")
|
|
422
|
+
interactive = sys.stdin.isatty() and os.environ.get(
|
|
423
|
+
"AGENTNODE_NON_INTERACTIVE", ""
|
|
424
|
+
).lower() not in ("true", "1")
|
|
425
|
+
if not _confirm_publish(pkg_id, version, registry, yes=yes, interactive=interactive):
|
|
426
|
+
# interactive decline = clean cancel (0); non-interactive without --yes
|
|
427
|
+
# = refusal (1). When yes=True the gate always proceeds, so we never land
|
|
428
|
+
# here in that case.
|
|
429
|
+
return 0 if interactive else 1
|
|
430
|
+
|
|
392
431
|
try:
|
|
393
432
|
sig_block = _sign_for_publish(pkg_id, manifest, artifact_bytes)
|
|
394
433
|
manifest["_signatures"] = {"publisher": [sig_block]}
|
|
@@ -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,
|