Two critical gaps in a2a_tools.py let any tenant workspace poison org-wide
(GLOBAL) memory and bypass all RBAC enforcement:
1. tool_commit_memory had no RBAC check — any agent could write any scope.
2. tool_commit_memory had no root-workspace enforcement for GLOBAL scope —
Tenant A could POST scope=GLOBAL and pollute the shared memory store
that Tenant B's agent reads as trusted context.
Fix adds:
- _ROLE_PERMISSIONS table (mirrors builtin_tools/audit.py) so a2a_tools
has isolated RBAC logic without depending on memory.py.
- _check_memory_write_permission() / _check_memory_read_permission() helpers:
evaluate RBAC roles from WorkspaceConfig; fail closed (deny) on errors.
- _is_root_workspace() / _get_workspace_tier(): read WorkspaceConfig.tier
(0 = root/org, 1+ = tenant) from config.yaml; fall back to
WORKSPACE_TIER env var.
- tool_commit_memory now (a) checks memory.write RBAC, (b) rejects
GLOBAL scope for non-root workspaces, (c) embeds workspace_id in the
POST body so the platform can namespace-isolate and audit cross-workspace
writes.
- tool_recall_memory now checks memory.read RBAC before any HTTP call,
and always sends workspace_id as a GET param for platform cross-validation.
Security regression tests added:
- GLOBAL scope denied for non-root (tier>0) workspaces.
- RBAC denial blocks all scope levels (including LOCAL) on write.
- RBAC denial blocks recall entirely.
- workspace_id present in POST body and GET params.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two latent bugs the self-hosted Mac mini had been hiding. Both caught
by the newer toolchain on ubuntu-latest runners after PR #1626.
1. workspace-server/internal/handlers/terminal.go:442
`fmt.Sprintf("%s:%d", host, port)` flagged by go vet as unsafe
for IPv6 (it omits the required [::] brackets). Replaced with
`net.JoinHostPort(host, strconv.Itoa(port))` which handles both
IPv4 and IPv6 correctly. No runtime behaviour change — the only
call site passes "127.0.0.1", so the bug would never trigger in
practice, but vet is right to flag it as a latent correctness
issue.
2. workspace/tests/test_a2a_executor.py::test_set_current_task_updates_heartbeat
`MagicMock()` auto-creates attributes on first access, so
`getattr(heartbeat, "active_tasks", 0)` in shared_runtime.py
returned a MagicMock rather than the default 0. Adding 1 to a
MagicMock returns another MagicMock, so the assertion
`heartbeat.active_tasks == 1` never held. Seeding
`heartbeat.active_tasks = 0` before the first call makes
getattr() return a real int, matching how the real HeartbeatLoop
class initialises itself.
Both pre-existed on main and were hidden by the older Python / Go
toolchains on the Mac mini runner. Verified locally (venv pytest
pass, `go vet ./...` + `go build ./...` clean on workspace-server).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ci.yml: replace if/else BASE assignment with GITHUB_BASE_REF default
+ pull_request base.sha override pattern. Prevents push events from
overwriting the correct PR base SHA when both events fire together.
- conftest.py: catch RuntimeError in addition to ImportError when
importing coordinator.py, which raises RuntimeError at import time
when WORKSPACE_ID is not set (before the ImportError guard).
Co-authored-by: Molecule AI Release Manager <release-manager@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a pre-stop hook that captures agent state before container exit and
writes a scrubbed snapshot to /configs/.agent_snapshot.json. On restart,
the snapshot is loaded and the adapter's restore_state() is called before
the A2A server starts.
- New lib/pre_stop.py: build_snapshot / write_snapshot / read_snapshot /
delete_snapshot + _scrub_value deep-scrubber (uses lib.snapshot_scrub
to redact API keys, tokens, and sandbox output before persisting)
- BaseAdapter.pre_stop_state(): captures _executor._session_id and recent
transcript_lines; overridden by adapters with richer in-memory state
- BaseAdapter.restore_state(): stores snapshot fields as adapter attrs
for create_executor() to pick up
- main.py: calls pre_stop serialization in finally block (after server
serves) and restore_state() after adapter setup, before server starts
- Added 12 unit tests covering scrub, read/write, adapter integration
Co-authored-by: Molecule AI Infra-Runtime-BE <infra-runtime-be@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mock get_hma_instructions in exact-match tests so they don't break
when HMA content is appended. Add a dedicated test for HMA inclusion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove compiled workspace-server/server binary from git
- Fix .gitignore, .gitattributes, .githooks/pre-commit for renamed dirs
- Fix CI workflow path filters (workspace-template → workspace)
- Replace real EC2 IP and personal slug in test_saas_tenant.sh
- Scrub molecule-controlplane references in docs
- Fix stale workspace-template/ paths in provisioner, handlers, tests
- Clean tracked Python cache files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>