Implement sandbox_boundary(r) function that walks up containment edges
in the resource DAG to the nearest sandboxable ancestor, enabling
resources sharing a boundary to share one sandbox instance.
Changes:
- Add boundary.py: is_sandbox_boundary(), sandbox_boundary(),
compute_sandbox_domains(), BoundaryCache (thread-safe, per-execution)
- Update SandboxManager: resolve_sandbox_key() and
get_or_create_sandbox_for_resource() key by (plan_id, boundary_id)
instead of (plan_id, resource_id); boundary cache lifecycle methods
- Define "sandboxable" via ResourceCapabilities.sandboxable + non-none
sandbox_strategy as per specification section 24659-24674
- Export new symbols from sandbox __init__.py
- Add vulture whitelist entries for new public API
Tests:
- 26 Behave BDD scenarios (features/sandbox_boundary_algebra.feature)
- 5 Robot Framework integration tests (robot/sandbox_boundary_algebra.robot)
- ASV benchmarks for boundary walk, domain grouping, and cache performance
ISSUES CLOSED: #548