agentnode-sdk 0.11.0__tar.gz → 0.11.2__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.0 → agentnode_sdk-0.11.2}/CHANGELOG.md +44 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/PKG-INFO +1 -1
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/__init__.py +1 -1
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/commands.py +9 -2
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/installer.py +30 -3
- agentnode_sdk-0.11.2/agentnode_sdk/references.py +21 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtimes/agent_runner.py +17 -9
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtimes/python_runner.py +2 -2
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/pyproject.toml +1 -1
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_agent_runner.py +27 -0
- agentnode_sdk-0.11.2/tests/test_cli_run_resolution.py +36 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_install_hardening.py +10 -0
- agentnode_sdk-0.11.2/tests/test_references.py +16 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_sandbox_gate.py +14 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_toolpack_sandbox.py +15 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_v02.py +42 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/.env.example +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/.gitignore +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/README.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/REGISTRY_SIGNING_ACTIVATION.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/REGISTRY_SIGNING_SPEC.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/THREAT_MODEL.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/TRUST_STACK.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode.lock +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/_fileutil.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/async_client.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/capability_graph.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/capability_taxonomy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/__init__.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/__main__.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/audit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/auth.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/cassette_audit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/complements.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/init.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/main.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/mcp_commands.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/mcp_status.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/mcp_submit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/mcp_verify.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/output.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/publish.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/record_cases.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/sandbox_commands.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/serve.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/setup_wizard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/smart_run.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/templates.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/validate.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/cli/verify_local.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/client.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/compatibility.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/config.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/credential_handle.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/credential_resolver.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/credential_store.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/detect.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/exceptions.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/guard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/input_guard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/key_status.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/lock_integrity.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/mcp_server.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/models.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/planner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/policy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/registry_trust.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/resolve.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/resource_provider.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/risk_profile.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/run_log.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtime.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtimes/__init__.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtimes/mcp_runner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/runtimes/remote_runner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/sandbox/__init__.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/sandbox/backend.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/sandbox/container_backend.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/sandbox/policy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/sandbox/types.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/signature.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/signing_key.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/agentnode_sdk/skill.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/sandbox-image/Dockerfile +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/sandbox-image/README.md +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/analyze_scores.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/batch_verify.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/ci_smoke_test.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/generate_compatibility_artifacts.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/verify_toolcalls.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/scripts/weekly_retest.sh +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/__init__.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/conftest.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_async_client.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_audit_ux.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_auto_upgrade_policy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_cli.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_cli_lock.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_client.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_client_json_guard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_client_sprint_b.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_config.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_credential_handle.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_credential_integration.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_credential_resolver.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_credential_store.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_detect.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_detect_and_install.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_e2e_runtime.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_edge_cases.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_check.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_config_cache.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_policy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_preview.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_schema.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_set.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_status.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_tool_override.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_tool_override_audit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_tool_override_cli.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_guard_ux.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_input_guard.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_input_guard_escalation.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_installer_sprint_b.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_intelligence.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_key_status.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_llm_binding.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_llm_call_runlog.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_lock_integrity.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_lock_runtime.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_mcp_audit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_mcp_doctor.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_mcp_sandbox.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_mcp_server.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_observability.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_planner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_policy.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_policy_integration.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_prompt_specs.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_provider_matrix.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_publish.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_registry_trust.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_remote_hardening.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_remote_runner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_resource_provider.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_resource_specs.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_risk_profile.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_run_log.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_runner.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_runtime.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_runtime_audit.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_sandbox_backend.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_sandbox_doctor.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_sandbox_e2e.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_security_hardening.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_signature.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_signing_key.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_skill.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_smart.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_stability.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_validate.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/test_validate_skill.py +0 -0
- {agentnode_sdk-0.11.0 → agentnode_sdk-0.11.2}/tests/validation_lockfile.json +0 -0
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.2 — CLI run resolution fixes
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`agentnode run <slug>` auto-selects a single-tool pack's tool.** When a
|
|
8
|
+
toolpack declares exactly one tool with an entrypoint, `run <slug>` (no tool
|
|
9
|
+
name) now resolves that tool instead of a non-existent default `run` function
|
|
10
|
+
(which raised `ImportError: Function 'run' not found`). Multi-tool packs are
|
|
11
|
+
unchanged — the runner does not guess among several tools. Applied
|
|
12
|
+
consistently to the host (`load_tool`) and container (`_resolve_container_target`)
|
|
13
|
+
paths via a shared `_default_tool_entrypoint` helper.
|
|
14
|
+
- **`agentnode run <slug>:<tool>` resolves the real package trust/lockfile.**
|
|
15
|
+
The `slug:tool` form is now split at the CLI boundary, so the lockfile entry
|
|
16
|
+
and trust level come from the real slug instead of being treated as an unknown
|
|
17
|
+
(and therefore `unverified`) package. The sandbox/trust gate is unchanged and
|
|
18
|
+
still keys on the real slug — community/unverified packages stay
|
|
19
|
+
sandbox-mandatory or fail-closed. Slug/tool parsing is now a shared
|
|
20
|
+
`references.parse_tool_reference` helper used by both the CLI and the agent
|
|
21
|
+
runtime.
|
|
22
|
+
|
|
23
|
+
## 0.11.1 — Bugfix + hardening
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Install targets the running interpreter.** `installer.resolve_python()` now
|
|
28
|
+
returns `sys.executable` first, so the host build (`agentnode install` of a
|
|
29
|
+
trusted/curated pack) and `agentnode run` use the **same** Python. Previously
|
|
30
|
+
it resolved `$VIRTUAL_ENV → ./.venv → PATH python3 → PATH python` and never
|
|
31
|
+
the interpreter actually running AgentNode, so under **pipx** or an
|
|
32
|
+
**unactivated venv** the pack installed into a different environment and the
|
|
33
|
+
run could not import it. The existing fallbacks are kept for the rare case of
|
|
34
|
+
an empty `sys.executable`. Host build path only — the community/sandbox path
|
|
35
|
+
builds with `python -m pip` inside the container and was never affected.
|
|
36
|
+
|
|
37
|
+
### Hardened
|
|
38
|
+
|
|
39
|
+
- **Agent execution-vector invariant documented + regression-tested.** An
|
|
40
|
+
agent's own entrypoint code runs on the host (not via `SandboxBackend`), so
|
|
41
|
+
the `trust >= trusted` gate in `run_agent` is a security invariant. Added an
|
|
42
|
+
audit comment at the gate (no logic change) and a named regression test
|
|
43
|
+
asserting that `None`/unknown/`unverified`/`verified`/`preview` agents are
|
|
44
|
+
refused while `trusted`/`curated` pass — locking the gate against silent
|
|
45
|
+
lowering that would run community code unsandboxed.
|
|
46
|
+
|
|
3
47
|
## 0.11.0 — Execution Sandbox (isolated or not at all)
|
|
4
48
|
|
|
5
49
|
The execution plane is now sandboxed. Community/unverified code (toolpack
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentnode-sdk
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
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
|
|
@@ -837,6 +837,13 @@ def cmd_run(
|
|
|
837
837
|
if " " in capability.strip():
|
|
838
838
|
return _cmd_run_smart(capability, raw=raw, explain=explain, dry_run=dry_run, json_output=json_output)
|
|
839
839
|
|
|
840
|
+
# Slug mode: support `slug:tool`. Split at the CLI boundary so trust and the
|
|
841
|
+
# lockfile entry resolve on the REAL slug (slugs are kebab-case and never
|
|
842
|
+
# contain ':'); the tool name is forwarded to run_tool. The sandbox/trust
|
|
843
|
+
# gate is unchanged — it keys on the real slug's entry just like a bare slug.
|
|
844
|
+
from agentnode_sdk.references import parse_tool_reference
|
|
845
|
+
slug, tool_name = parse_tool_reference(capability)
|
|
846
|
+
|
|
840
847
|
if input_data and file_path:
|
|
841
848
|
print("--input and --file are mutually exclusive.", file=sys.stderr)
|
|
842
849
|
return 1
|
|
@@ -875,10 +882,10 @@ def cmd_run(
|
|
|
875
882
|
is_interactive = os.environ.get("AGENTNODE_NON_INTERACTIVE", "").lower() not in ("true", "1")
|
|
876
883
|
callback = cli_confirmation_callback if is_interactive else None
|
|
877
884
|
|
|
878
|
-
result = run_tool(
|
|
885
|
+
result = run_tool(slug, tool_name=tool_name, confirmation_callback=callback, **data)
|
|
879
886
|
|
|
880
887
|
if result.mode_used == "policy_denied" and result.policy:
|
|
881
|
-
_print_guard_block(
|
|
888
|
+
_print_guard_block(slug, tool_name, result)
|
|
882
889
|
return 1
|
|
883
890
|
|
|
884
891
|
if explain and hasattr(result, "policy") and result.policy:
|
|
@@ -40,9 +40,15 @@ PIP_TIMEOUT = 120
|
|
|
40
40
|
# ---------------------------------------------------------------------------
|
|
41
41
|
|
|
42
42
|
def resolve_python() -> str:
|
|
43
|
-
"""Find a usable Python 3 interpreter.
|
|
43
|
+
"""Find a usable Python 3 interpreter for the host package build.
|
|
44
44
|
|
|
45
45
|
Resolution order:
|
|
46
|
+
0. ``sys.executable`` — the interpreter actually running AgentNode, so a host
|
|
47
|
+
build (``agentnode install``) installs into the SAME environment that
|
|
48
|
+
``agentnode run`` later imports from. This fixes pipx / unactivated-venv
|
|
49
|
+
installs, where ``$VIRTUAL_ENV`` is unset and ``python`` on PATH is a
|
|
50
|
+
DIFFERENT interpreter (the package would install into the wrong env and
|
|
51
|
+
``agentnode run`` would fail to import it).
|
|
46
52
|
1. $VIRTUAL_ENV/bin/python (or Scripts/python.exe on Windows)
|
|
47
53
|
2. .venv/bin/python in cwd
|
|
48
54
|
3. python3 on PATH
|
|
@@ -50,6 +56,11 @@ def resolve_python() -> str:
|
|
|
50
56
|
"""
|
|
51
57
|
is_windows = sys.platform == "win32"
|
|
52
58
|
|
|
59
|
+
# 0. The interpreter actually running AgentNode — guarantees install and run
|
|
60
|
+
# use the same Python. Most reliable; prefer it over PATH/venv heuristics.
|
|
61
|
+
if sys.executable and os.path.isfile(sys.executable):
|
|
62
|
+
return sys.executable
|
|
63
|
+
|
|
53
64
|
# 1. Active virtual environment
|
|
54
65
|
venv = os.environ.get("VIRTUAL_ENV")
|
|
55
66
|
if venv:
|
|
@@ -1025,6 +1036,21 @@ def _resolve_entrypoint(entrypoint: str) -> tuple[str, str]:
|
|
|
1025
1036
|
return entrypoint, "run"
|
|
1026
1037
|
|
|
1027
1038
|
|
|
1039
|
+
def _default_tool_entrypoint(entry: dict) -> str | None:
|
|
1040
|
+
"""Entrypoint to use when no explicit ``tool_name`` is given.
|
|
1041
|
+
|
|
1042
|
+
Auto-selects the sole tool when the pack declares EXACTLY one tool with an
|
|
1043
|
+
entrypoint, so ``agentnode run <slug>`` works for single-tool packs without
|
|
1044
|
+
the caller knowing the tool name. Otherwise falls back to the package-level
|
|
1045
|
+
entrypoint — multi-tool packs are unchanged (the runner must not guess
|
|
1046
|
+
among several tools). Returns ``None`` when neither is available.
|
|
1047
|
+
"""
|
|
1048
|
+
tools = entry.get("tools") or []
|
|
1049
|
+
if len(tools) == 1 and tools[0].get("entrypoint"):
|
|
1050
|
+
return tools[0]["entrypoint"]
|
|
1051
|
+
return entry.get("entrypoint")
|
|
1052
|
+
|
|
1053
|
+
|
|
1028
1054
|
def load_tool(slug: str, tool_name: str | None = None, *, _internal: bool = False) -> Any:
|
|
1029
1055
|
"""Load an installed package's tool function.
|
|
1030
1056
|
|
|
@@ -1090,8 +1116,9 @@ def load_tool(slug: str, tool_name: str | None = None, *, _internal: bool = Fals
|
|
|
1090
1116
|
f"Available tools: {[t.get('name') for t in pkg.get('tools', [])]}"
|
|
1091
1117
|
)
|
|
1092
1118
|
|
|
1093
|
-
#
|
|
1094
|
-
entrypoint
|
|
1119
|
+
# No tool_name: auto-select the sole tool when exactly one is declared,
|
|
1120
|
+
# else fall back to the package-level entrypoint (multi-tool unchanged).
|
|
1121
|
+
entrypoint = _default_tool_entrypoint(pkg)
|
|
1095
1122
|
if not entrypoint:
|
|
1096
1123
|
raise ImportError(f"Package '{slug}' has no entrypoint in lockfile.")
|
|
1097
1124
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared parsing for package/tool references.
|
|
2
|
+
|
|
3
|
+
A reference is either a bare package slug (``"word-counter-pack"``) or a
|
|
4
|
+
``slug:tool`` pair (``"word-counter-pack:count_words"``). Package slugs are
|
|
5
|
+
kebab-case (``[a-z0-9-]``) and never contain ``':'``, so the first ``':'``
|
|
6
|
+
unambiguously separates the slug from an optional tool name.
|
|
7
|
+
|
|
8
|
+
Used by both the CLI run path (``cli.commands.cmd_run``) and the agent runtime
|
|
9
|
+
(``runtimes.agent_runner``) so the two stay in lock-step.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_tool_reference(ref: str) -> tuple[str, str | None]:
|
|
15
|
+
"""Parse ``'slug:tool'`` or ``'slug'`` into ``(slug, tool_name)``.
|
|
16
|
+
|
|
17
|
+
Returns ``(slug, None)`` when no tool is given (or the tool part is empty,
|
|
18
|
+
e.g. ``"slug:"``), so callers can treat it as "no specific tool".
|
|
19
|
+
"""
|
|
20
|
+
slug, _, tool = ref.partition(":")
|
|
21
|
+
return slug, (tool or None)
|
|
@@ -559,10 +559,7 @@ class AgentContext:
|
|
|
559
559
|
tc_args = {}
|
|
560
560
|
|
|
561
561
|
# Parse slug:tool_name from the tool call name
|
|
562
|
-
|
|
563
|
-
slug, tool_name = tc_name.split(":", 1)
|
|
564
|
-
else:
|
|
565
|
-
slug, tool_name = tc_name, None
|
|
562
|
+
slug, tool_name = _parse_tool_reference(tc_name)
|
|
566
563
|
|
|
567
564
|
tool_result = self.try_tool(slug, tool_name, **tc_args)
|
|
568
565
|
|
|
@@ -1245,6 +1242,15 @@ def run_agent(
|
|
|
1245
1242
|
)
|
|
1246
1243
|
|
|
1247
1244
|
# --- 2. Agent-specific policy: trust >= trusted ---
|
|
1245
|
+
# SECURITY INVARIANT (audit 2026-06): an agent's OWN entrypoint code runs on
|
|
1246
|
+
# the HOST — in a thread (_execute_with_timeout) or a child process
|
|
1247
|
+
# (_execute_with_process) — NOT inside SandboxBackend. The exec-sandbox bow
|
|
1248
|
+
# (P0.0-P0.3) isolates the agent's *tool calls* (they re-enter run_tool's
|
|
1249
|
+
# gate), but NOT the agent's own orchestration code. So `trust >= trusted` is
|
|
1250
|
+
# the ONLY thing keeping community/unverified agent code off the host. Do NOT
|
|
1251
|
+
# lower this gate without first routing agent execution through SandboxBackend,
|
|
1252
|
+
# or community code runs unsandboxed (reintroducing the RCE class the sandbox
|
|
1253
|
+
# bow closed). Locked by test_agent_runner's execution-vector regression test.
|
|
1248
1254
|
trust_level = entry.get("trust_level", "unverified")
|
|
1249
1255
|
if not _trust_meets_minimum(trust_level, "trusted"):
|
|
1250
1256
|
_audit_agent_run(
|
|
@@ -2194,11 +2200,13 @@ def _parse_literal(value: str) -> Any:
|
|
|
2194
2200
|
# ---------------------------------------------------------------------------
|
|
2195
2201
|
|
|
2196
2202
|
def _parse_tool_reference(ref: str) -> tuple[str, str | None]:
|
|
2197
|
-
"""Parse 'slug:tool_name' or 'slug' into (slug, tool_name).
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2203
|
+
"""Parse 'slug:tool_name' or 'slug' into (slug, tool_name).
|
|
2204
|
+
|
|
2205
|
+
Thin wrapper over the shared :func:`agentnode_sdk.references.parse_tool_reference`
|
|
2206
|
+
so the agent runtime and the CLI run path stay in lock-step.
|
|
2207
|
+
"""
|
|
2208
|
+
from agentnode_sdk.references import parse_tool_reference
|
|
2209
|
+
return parse_tool_reference(ref)
|
|
2202
2210
|
|
|
2203
2211
|
|
|
2204
2212
|
def _resolve_input_mapping(
|
|
@@ -445,7 +445,7 @@ def _resolve_container_target(entry: dict, tool_name: str | None) -> tuple[str,
|
|
|
445
445
|
first then the default function (``run``), matching load_tool's getattr order;
|
|
446
446
|
- no tool_name → package entrypoint module + its function.
|
|
447
447
|
"""
|
|
448
|
-
from agentnode_sdk.installer import _resolve_entrypoint
|
|
448
|
+
from agentnode_sdk.installer import _resolve_entrypoint, _default_tool_entrypoint
|
|
449
449
|
|
|
450
450
|
tools = entry.get("tools") or []
|
|
451
451
|
if tool_name:
|
|
@@ -463,7 +463,7 @@ def _resolve_container_target(entry: dict, tool_name: str | None) -> tuple[str,
|
|
|
463
463
|
tool_name=tool_name,
|
|
464
464
|
)
|
|
465
465
|
|
|
466
|
-
ep = entry
|
|
466
|
+
ep = _default_tool_entrypoint(entry)
|
|
467
467
|
if not ep:
|
|
468
468
|
raise AgentNodeToolError(
|
|
469
469
|
"Package has no entrypoint in the lockfile.", tool_name=None,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentnode-sdk"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.2"
|
|
8
8
|
description = "Python SDK for AgentNode — the open upgrade and discovery infrastructure for AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -319,6 +319,33 @@ class TestRunAgentTrustPolicy:
|
|
|
319
319
|
assert "trust level" not in (result.error or "")
|
|
320
320
|
|
|
321
321
|
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# run_agent() — execution-vector invariant (0.11.1 hardening, audit 2026-06)
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# An agent's OWN entrypoint code runs on the HOST (thread _execute_with_timeout /
|
|
326
|
+
# process _execute_with_process), NOT through SandboxBackend. The exec-sandbox
|
|
327
|
+
# bow does NOT cover the agent's own code. So `trust >= trusted` at the gate
|
|
328
|
+
# (agent_runner.py ~1248) is a SECURITY INVARIANT: lowering it would run
|
|
329
|
+
# community/unverified agent code unsandboxed on the host. These tests lock the
|
|
330
|
+
# gate against silent regressions — any non-(trusted|curated) level, including an
|
|
331
|
+
# unrecognized or missing one, must be refused with a trust-level error.
|
|
332
|
+
|
|
333
|
+
class TestRunAgentExecutionVectorInvariant:
|
|
334
|
+
@pytest.mark.parametrize("trust_level", [None, "unknown", "unverified", "verified", "preview"])
|
|
335
|
+
def test_community_or_unknown_agent_refused(self, trust_level):
|
|
336
|
+
entry = _agent_entry(trust_level=trust_level)
|
|
337
|
+
result = run_agent("test-agent", entry=entry)
|
|
338
|
+
assert result.success is False
|
|
339
|
+
assert "trust level" in (result.error or "")
|
|
340
|
+
|
|
341
|
+
@pytest.mark.parametrize("trust_level", ["trusted", "curated"])
|
|
342
|
+
def test_trusted_or_curated_agent_not_gated(self, trust_level):
|
|
343
|
+
entry = _agent_entry(trust_level=trust_level)
|
|
344
|
+
result = run_agent("test-agent", entry=entry)
|
|
345
|
+
# Passes the trust gate (may still fail later at entrypoint loading).
|
|
346
|
+
assert "trust level" not in (result.error or "")
|
|
347
|
+
|
|
348
|
+
|
|
322
349
|
# ---------------------------------------------------------------------------
|
|
323
350
|
# run_agent() — execution
|
|
324
351
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""CLI `run` resolution (0.11.2): `slug:tool` is split at the CLI boundary so the
|
|
2
|
+
real slug feeds trust/lockfile resolution, and the tool name is forwarded."""
|
|
3
|
+
from agentnode_sdk.cli import commands
|
|
4
|
+
from agentnode_sdk.models import RunToolResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _spy_run_tool(captured):
|
|
8
|
+
def fake(slug, tool_name=None, confirmation_callback=None, **kwargs):
|
|
9
|
+
captured["slug"] = slug
|
|
10
|
+
captured["tool_name"] = tool_name
|
|
11
|
+
captured["kwargs"] = kwargs
|
|
12
|
+
return RunToolResult(success=True, result={"ok": True}, mode_used="subprocess")
|
|
13
|
+
return fake
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cmd_run_splits_slug_tool(monkeypatch):
|
|
17
|
+
"""`run <slug>:<tool>` → run_tool(real_slug, tool_name=tool). The real slug
|
|
18
|
+
is what reaches run_tool, so trust/lockfile resolve on it (not the colon
|
|
19
|
+
string treated as an unknown unverified package)."""
|
|
20
|
+
captured = {}
|
|
21
|
+
monkeypatch.setattr("agentnode_sdk.runner.run_tool", _spy_run_tool(captured))
|
|
22
|
+
rc = commands.cmd_run("word-counter-pack:count_words", input_data='{"text":"a b"}', json_output=True)
|
|
23
|
+
assert rc == 0
|
|
24
|
+
assert captured["slug"] == "word-counter-pack"
|
|
25
|
+
assert captured["tool_name"] == "count_words"
|
|
26
|
+
assert captured["kwargs"] == {"text": "a b"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_cmd_run_bare_slug_has_no_tool(monkeypatch):
|
|
30
|
+
"""Bare `run <slug>` still forwards tool_name=None (unchanged)."""
|
|
31
|
+
captured = {}
|
|
32
|
+
monkeypatch.setattr("agentnode_sdk.runner.run_tool", _spy_run_tool(captured))
|
|
33
|
+
rc = commands.cmd_run("word-counter-pack", input_data='{"text":"a"}', json_output=True)
|
|
34
|
+
assert rc == 0
|
|
35
|
+
assert captured["slug"] == "word-counter-pack"
|
|
36
|
+
assert captured["tool_name"] is None
|
|
@@ -93,3 +93,13 @@ def test_missing_artifact_hash_blocked(monkeypatch, tmp_path):
|
|
|
93
93
|
artifact_hash=None, entrypoint="pk.tool", trust_level="trusted",
|
|
94
94
|
)
|
|
95
95
|
assert pip_calls == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_resolve_python_prefers_sys_executable():
|
|
99
|
+
"""0.11.1 bugfix: the host build must target the interpreter actually running
|
|
100
|
+
AgentNode, so `agentnode install` + `agentnode run` use the SAME Python. In a
|
|
101
|
+
pipx / unactivated venv, $VIRTUAL_ENV is unset and `python` on PATH is a
|
|
102
|
+
DIFFERENT interpreter — the pack would install into the wrong env and the run
|
|
103
|
+
couldn't import it. resolve_python() must return sys.executable."""
|
|
104
|
+
import sys
|
|
105
|
+
assert installer.resolve_python() == sys.executable
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared slug/tool reference parsing (0.11.2)."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from agentnode_sdk.references import parse_tool_reference
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize("ref,expected", [
|
|
8
|
+
("word-counter-pack", ("word-counter-pack", None)),
|
|
9
|
+
("word-counter-pack:count_words", ("word-counter-pack", "count_words")),
|
|
10
|
+
("a-b", ("a-b", None)),
|
|
11
|
+
("a-b:c", ("a-b", "c")),
|
|
12
|
+
("a-b:", ("a-b", None)), # empty tool part → None
|
|
13
|
+
("a-b:c:d", ("a-b", "c:d")), # only the first colon splits
|
|
14
|
+
])
|
|
15
|
+
def test_parse_tool_reference(ref, expected):
|
|
16
|
+
assert parse_tool_reference(ref) == expected
|
|
@@ -82,6 +82,20 @@ def test_other_nontrusted_blocked_when_unavailable(tmp_path, trust):
|
|
|
82
82
|
assert res.mode_used == "sandbox_unavailable"
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_cli_community_slug_tool_still_failclosed(tmp_path, monkeypatch, capsys):
|
|
86
|
+
"""0.11.2: splitting `slug:tool` at the CLI must NOT bypass the sandbox gate.
|
|
87
|
+
A community (unverified) pack run as `slug:tool` with no container runtime
|
|
88
|
+
stays fail-closed — cmd_run forwards the REAL slug, whose trust the gate reads."""
|
|
89
|
+
from agentnode_sdk.cli import commands
|
|
90
|
+
set_default_backend(_FakeBackend(available=False))
|
|
91
|
+
lf = _write_lockfile(tmp_path, "evil-pack", "unverified")
|
|
92
|
+
monkeypatch.setattr(installer, "_lockfile_path", lambda: lf)
|
|
93
|
+
rc = commands.cmd_run("evil-pack:dotool", input_data='{"x":1}', json_output=True)
|
|
94
|
+
out = capsys.readouterr().out
|
|
95
|
+
assert rc == 1
|
|
96
|
+
assert "sandbox_unavailable" in out
|
|
97
|
+
|
|
98
|
+
|
|
85
99
|
def test_curated_not_blocked_when_unavailable(tmp_path, monkeypatch):
|
|
86
100
|
set_default_backend(_FakeBackend(available=False))
|
|
87
101
|
_stub_check_run_deny(monkeypatch)
|
|
@@ -339,6 +339,21 @@ def test_resolve_target_v01_package_default():
|
|
|
339
339
|
assert _resolve_container_target(entry, None) == ("mod.tool", ["run"])
|
|
340
340
|
|
|
341
341
|
|
|
342
|
+
def test_resolve_target_single_tool_autoselect():
|
|
343
|
+
# 0.11.2: single-tool pack, no tool_name → auto-select the sole tool (not run)
|
|
344
|
+
entry = {"entrypoint": "wc.tool",
|
|
345
|
+
"tools": [{"name": "count_words", "entrypoint": "wc.tool:count_words"}]}
|
|
346
|
+
assert _resolve_container_target(entry, None) == ("wc.tool", ["count_words"])
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_resolve_target_multi_tool_keeps_package_default():
|
|
350
|
+
# 0.11.2: multi-tool, no tool_name → package-level entrypoint (unchanged)
|
|
351
|
+
entry = {"entrypoint": "mod.tool:dispatch",
|
|
352
|
+
"tools": [{"name": "a", "entrypoint": "mod.tool:a"},
|
|
353
|
+
{"name": "b", "entrypoint": "mod.tool:b"}]}
|
|
354
|
+
assert _resolve_container_target(entry, None) == ("mod.tool", ["dispatch"])
|
|
355
|
+
|
|
356
|
+
|
|
342
357
|
def test_resolve_target_v02_colon_function():
|
|
343
358
|
entry = {"entrypoint": "mod.tool:describe", "tools": []}
|
|
344
359
|
assert _resolve_container_target(entry, None) == ("mod.tool", ["describe"])
|
|
@@ -69,6 +69,48 @@ class TestLoadTool:
|
|
|
69
69
|
func = load_tool("my-pack")
|
|
70
70
|
assert func == mock_module.run
|
|
71
71
|
|
|
72
|
+
def test_load_single_tool_autoselect(self, tmp_path):
|
|
73
|
+
"""0.11.2: single-tool pack — load_tool(slug) with no tool_name
|
|
74
|
+
auto-selects the sole tool's entrypoint instead of the default `run`."""
|
|
75
|
+
self._write_lockfile(tmp_path, {
|
|
76
|
+
"word-counter-pack": {
|
|
77
|
+
"version": "1.0.0",
|
|
78
|
+
"entrypoint": "wc.tool", # colon-less → old code defaulted to run()
|
|
79
|
+
"tools": [
|
|
80
|
+
{"name": "count_words", "entrypoint": "wc.tool:count_words"},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
# spec=[...] so the module has NO `run` attr — old behavior would raise.
|
|
85
|
+
mock_module = MagicMock(spec=["count_words"])
|
|
86
|
+
mock_module.count_words = lambda text="": {"words": 0}
|
|
87
|
+
|
|
88
|
+
with patch("agentnode_sdk.installer._lockfile_path", return_value=tmp_path / "agentnode.lock"):
|
|
89
|
+
with patch("agentnode_sdk.installer.importlib.import_module", return_value=mock_module):
|
|
90
|
+
func = load_tool("word-counter-pack")
|
|
91
|
+
assert func == mock_module.count_words
|
|
92
|
+
|
|
93
|
+
def test_load_multi_tool_uses_package_entrypoint(self, tmp_path):
|
|
94
|
+
"""0.11.2: multi-tool pack — load_tool(slug) with no tool_name still uses
|
|
95
|
+
the package-level entrypoint (no guessing among tools). Unchanged."""
|
|
96
|
+
self._write_lockfile(tmp_path, {
|
|
97
|
+
"multi-pack": {
|
|
98
|
+
"version": "1.0.0",
|
|
99
|
+
"entrypoint": "multi.tool:dispatch",
|
|
100
|
+
"tools": [
|
|
101
|
+
{"name": "a", "entrypoint": "multi.tool:a"},
|
|
102
|
+
{"name": "b", "entrypoint": "multi.tool:b"},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
mock_module = MagicMock(spec=["dispatch", "a", "b"])
|
|
107
|
+
mock_module.dispatch = lambda **k: "dispatched"
|
|
108
|
+
|
|
109
|
+
with patch("agentnode_sdk.installer._lockfile_path", return_value=tmp_path / "agentnode.lock"):
|
|
110
|
+
with patch("agentnode_sdk.installer.importlib.import_module", return_value=mock_module):
|
|
111
|
+
func = load_tool("multi-pack")
|
|
112
|
+
assert func == mock_module.dispatch
|
|
113
|
+
|
|
72
114
|
def test_load_v02_with_tool_name(self, tmp_path):
|
|
73
115
|
"""v0.2 pack — load_tool(slug, tool_name) returns specific function."""
|
|
74
116
|
self._write_lockfile(tmp_path, {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|