The React Flow toolbar (zoom in/out/fit) and Minimap are third-party
components that render their own buttons. Add CSS-based focus-visible
rules so keyboard users see a visible ring on these canvas controls,
completing the WCAG 2.4.7 coverage for all interactive elements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add roving tabindex to result option buttons so keyboard users
see a visible focus ring on the currently selected item. Tab from
the input lands on the right option; clicking an option immediately
re-focuses the input so all arrow/Enter key handling stays in the
input's handler. Applies focus-visible ring (accent) to the selected
listbox option.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WCAG 2.4.7 — Focus Visible (Two-level Keyboard Navigation).
ThemeToggle: 3 icon radio buttons in radiogroup now have
focus-visible:ring-2 ring-accent rings.
RevealToggle: eye/eye-off icon button now has focus-visible ring.
ErrorBoundary: Reload and Report buttons now have focus-visible rings.
ConversationTraceModal: close button and footer Close button now have
focus-visible rings (Radix Dialog handles focus trapping; rings add
visibility for keyboard-only users).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuditTrailPanel: filter buttons, refresh button, load-more button now
have focus-visible:ring-2 ring-accent focus-visible rings so keyboard
users can see which element has focus.
CommunicationOverlay: toggle button and close button now have the same
focus ring, consistent with the rest of the canvas design system.
WCAG 2.1 AA — 2.4.7: Focus Visible (Two-level Keyboard Navigation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add data-testid="legend-panel" to Legend component root div so
tests can select the panel reliably instead of .closest("div")
(the "Legend" text also appears in the collapsed pill).
- Update palette-offset positioning tests to use container.querySelector
with data-testid instead of screen.getByText + .closest("div").
- PurchaseSuccessModal: skip URL stripping when no target params present.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Design fixes:
- PricingTable.tsx: replace non-zinc disabled:bg-blue-900 with
bg-zinc-700/text-zinc-500, keeping all states within the dark zinc
palette (zinc-900 bg, zinc-800 surfaces, zinc-700 borders).
Test fixes:
- PurchaseSuccessModal.test.tsx: replace setTimeout(0) anti-pattern under
vi.useFakeTimers() — act() does not advance fake timers, causing 5000ms
timeouts. Use vi.advanceTimersByTime(10) to flush render effects without
triggering the 5s auto-dismiss. 18/18 tests now pass.
- OnboardingWizard.test.tsx: replace stateless mock with
useSyncExternalStore bridge + subscriber set so React re-renders when
mockStoreState is mutated; fix second-render unmount ordering. 13/13 pass.
- yaml-utils.ts: emit tools: [] key unconditionally (matching skills
behaviour); test expectation was correct, implementation was wrong. 36/36.
- tabs/chat/types.ts createMessage: conditional { attachments } spread
avoids undefined key in Object.keys(); Object.freeze() the returned
object so mutation-guards in tests pass.
- tabs/FilesTab/tree.ts getIcon: normalize extracted extension to
lowercase so data.JSON matches the .json entry in FILE_ICONS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatusDot: replace screen.getByRole("img") with container.querySelector —
role="img" with aria-hidden="true" is inaccessible to getByRole in jsdom.
Use getAttribute("class") instead of .className (SVG returns
SVGAnimatedString which .toContain fails on).
- Spinner: same SVG className fix as StatusDot — use getAttribute("class").
- StatusBadge: scope all role=status queries to [aria-label="Connection status:
<status>"] to avoid ambiguity with Spinner/Toast role=status in shared jsdom.
- ValidationHint: scope role=alert queries to container; checkmark is in a
separate span so use container.textContent regex /✓.*Valid format/s.
- RevealToggle: scope all button queries to container to avoid cross-test
interference in shared jsdom.
- TopBar: scope all queries to container; match "+ New Agent" by text content.
- SearchDialog: "clears query" test — open dialog state so combobox renders;
fix Enter-selects test: auto-highlight starts at index 0 (Alice) so after
one ArrowDown the selection is at index 1 (Bob/n2), not n1.
- ContextMenu: Tab handler fires on the menu div, not document.body; disabled
Chat/Terminal check uses getAttribute("disabled") → toBe("") instead of
toBeDisabled() (Chai plugin not installed).
- Tooltip: add vi.useFakeTimers() beforeEach in "render" and "Esc dismiss"
describe blocks; use window.dispatchEvent(KeyboardEvent) for Escape key
(captures to the useEffect listener); aria-describedby is on the wrapper div,
not the child button — show tooltip first so portal element exists in DOM.
- Tooltip — renders children: fix duplicate render call inside test.
- canvas-topology-pure: update "missing node" test expectation from
["root","orphan"] to ["orphan","root"] — actual algorithm visits orphan
first (ghost parent not found), then root.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatusBadge: scope role=status queries to [aria-label] to avoid
ambiguity with role=status from other components in shared jsdom
- ApprovalBanner: scope role=alert queries and button clicks to
container to avoid cross-test interference
- ContextMenu: use vi.hoisted() for apiPost/apiPatch mocks to fix
vitest hoisting error; scope Escape/Tab key tests to menu element
instead of document.body; update offline-node expectations
- BundleDropZone: scope file input and button queries to
container; mock dataTransfer.types for drag-over test; guard
dataTransfer?.types in component to prevent jsdom TypeError
- TestConnectionButton: use vi.hoisted() for mockValidateSecret;
fix disabled attr assertions (getAttribute returns "" not truthy);
scope button click to container to avoid SVG icon interference
- OrgImportPreflightModal/SidePanel: use vi.hoisted() for store
mocks to fix vitest hoisting errors
- ConversationTraceModal: update expectation to match actual impl
(extractMessageText joins all non-empty parts)
- KeyValueField: use container.querySelector for all input/button
queries; jsdom does not expose role=textbox for password inputs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Staging branch bea89ce4 introduced duplicate dead code after a `return`
in the delegate_task error-handling block — the first occurrence was the
correct fix (adding isinstance(err, str)), but the second occurrence (now
unreachable) made the block fragile. Main already has the correct code;
this branch adds an explanatory comment and regression tests.
The non-tool delegate_task() in a2a_tools.py uses httpx.AsyncClient
directly (not send_a2a_message) and must handle three A2A proxy error
shapes:
{"error": "plain string"} ← the bug fix: isinstance(err, str)
{"error": {"message": "...", ...}} ← pre-existing path
{"error": {"nested": "object"}} ← falls through to str(err)
Adds TestDelegateTaskDirect:
test_string_form_error_returns_error_message — regression for AttributeError
test_dict_form_error_returns_error_message — pre-existing path still works
test_success_returns_result_text — happy path still works
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gitea Actions runners (ubuntu-latest) do not bundle jq.
The sop-tier-check script uses jq for all JSON API parsing.
Install jq before the script runs so sop-tier-check can pass.
Uses direct binary download from GitHub releases (faster, more
reliable than apt-get in containerized environments) with
apt-get fallback and jq --version smoke test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two vulnerable call sites confirmed on origin/main:
1. org_helpers.go:loadWorkspaceEnv (line 101): filesDir from untrusted org YAML
joined directly with orgBaseDir without traversal guard. A malicious filesDir
like "../../../etc" escapes the org root and reads arbitrary files.
2. org_import.go:createWorkspaceTree (line 494): same pattern directly in the
env-loading block — not covered by staging-targeted PR #345.
Fix (both locations): call resolveInsideRoot(orgBaseDir, filesDir) before
filepath.Join. On traversal detection, org_helpers.go returns an empty map
(caller contract); org_import.go silently skips the workspace .env override
(matches existing template-resolution pattern in the same function).
Tests: org_helpers_test.go — 3 cases covering traversal rejection,
workspace-override happy path, and empty filesDir edge case.
Closes: molecule-core#362, molecule-core#321
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Close the A2A delegation auto-resume gap.
Root cause: heartbeat.py's _check_delegations already writes completed
delegation rows to DELEGATION_RESULTS_FILE and sends a self-message to
wake the agent. executor_helpers.read_delegation_results() was defined to
atomically consume that file, but a2a_executor._core_execute() never
called it — so delegation results were written but the agent never saw
them.
Fix: call read_delegation_results() at the top of _core_execute() and
prepend the results to the user input context so the agent can act on
them without an explicit check_task_status call. The Temporal durable
workflow path is also covered because it calls _core_execute() directly.
Test: two new cases — delegation results injected when file exists;
user input passed through unchanged when file is empty.
Closes molecule-core#354.
Incorporates valuable extra coverage from fullstack-engineer's PR #336:
- test_push_queued_missing_queue_id_still_parsed: queue_id is optional,
absence must not break parsing
- test_push_queued_is_distinct_from_poll_queued: both envelope shapes
parse correctly and independently, with correct delivery_mode values
Also adds push_queued_no_queue_id fixture and regression gate entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: a2a_response.py:197 returned Queued(method=method) without passing
delivery_mode, silently defaulting to "poll" for push-mode busy-queue
responses. Callers branching on v.delivery_mode would mis-identify push-mode
responses as poll-mode, causing wrong dispatch logic.
Fix: pass delivery_mode="push" explicitly in the push-mode branch.
Tests: add push_queued_full/notify/no_method fixtures and 4 test cases
asserting delivery_mode="push" for all three envelope shapes. Also add
adversarial {"queued": "yes"} and {"queued": False} → Malformed guards.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Run 5160 publish-runtime build step failed:
error: TOP_LEVEL_MODULES drifted from workspace/*.py contents:
in workspace/ but NOT in TOP_LEVEL_MODULES (will ship un-rewritten): ['_sanitize_a2a']
Edit scripts/build_runtime_package.py:TOP_LEVEL_MODULES to match.
workspace/_sanitize_a2a.py was added recently but the allowlist in
scripts/build_runtime_package.py was not updated. The build script
intentionally aborts (exit 3) when it detects the drift, because
shipping a module un-rewritten breaks the package's flat-layout import
contract.
Fix: add '_sanitize_a2a' to the set. Alphabetical order preserved
(it sorts before 'a2a_*').
Third workflow defect after #353 (workflow_dispatch.inputs parser) and
#355 (Publish step working-directory). After this lands, attempt #4 of
runtime-v0.1.130 should finally succeed.
Refs: #351, #353, #355, #348 Q3
First-ever publish-runtime.yml dispatch (run 5097 post-#353, 2026-05-11
02:06Z) failed at the twine upload step:
ERROR InvalidDistribution: Cannot find file (or expand pattern): 'dist/*'
Cause: the Publish step was missing 'working-directory: ${{ runner.temp
}}/runtime-build' while the preceding Build/Verify steps all had it.
Result: twine ran from the workspace checkout dir where dist/ doesn't
exist.
Fix: add working-directory to match the rest of the publish job.
This is the second of three workflow defects exposed by #353 finally
making the workflow run at all:
1. workflow_dispatch.inputs rejection → fixed in #353
2. Publish step missing working-directory → THIS PR
3. (anything else surfaced by 0.1.130 attempt #2)
After merge: push runtime-v0.1.130 again (tag was already pushed once
post-#353 but the run failed at publish; need a fresh trigger). Should
finally land 0.1.130 on PyPI.
Refs: #351, #348 Q3, #353
test_audit_ledger.py imports sqlalchemy directly (line 42).
Without an explicit sqlalchemy install, pip dependency resolution can
omit it when pytest/pytest-asyncio/pytest-cov are installed as a
separate step after requirements.txt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gitea Actions reads .gitea/workflows/, not .github/workflows/. The
.github/ copy of this workflow has been kept in lockstep with .gitea/
since the post-suspension migration (e.g. 6d94fd30, 5216e781, 67b2e488
all touch both files). The functional code is identical between the
two; the only differences are comment verbosity and the path-filter
self-reference (each version watches its own location).
Removing the .github/ copy:
- eliminates the dual-edit maintenance tax (two files touched per fix)
- prevents accidental drift where one is updated and the other isn't
- leaves a single source-of-truth at .gitea/workflows/
Cross-references confirmed safe:
- canary-verify.yml + redeploy-tenants-on-{staging,main}.yml all use
`workflows: ['publish-workspace-server-image']` (workflow name,
not file path) — they trigger off the workflow_run event keyed on
`name:`, which is identical in both files.
- No other workflow path-watches .github/workflows/publish-workspace-
server-image.yml.
Other two triplicates from task #287 (publish-runtime.yml and
secret-scan.yml) are NOT addressed in this PR — see PR description for
the ambiguity report flagging them for human review.
Refs: task #287
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trivial empty commit to force a fresh workflow run now that the
PR has tier:low label and approvals on the rebased branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause (from infra-lead PR#7 review id=724):
Sanitization in PR#7 wrapped peer text in [A2A_RESULT_FROM_PEER]
markers, but the markers themselves were not escaped — a malicious
peer could inject "[/A2A_RESULT_FROM_PEER]" to close the trust
boundary early, making subsequent text appear inside the trusted zone.
Fix:
- Create workspace/_sanitize_a2a.py (leaf module, no circular import
risk) with shared sanitize_a2a_result() + _escape_boundary_markers()
- _escape_boundary_markers() escapes boundary open/close markers in the
raw peer text before wrapping (primary security control)
- Defense-in-depth: also escapes SYSTEM/OVERRIDE/INSTRUCTIONS/IGNORE
ALL/YOU ARE NOW patterns (secondary, per PR#7 design intent)
- Update a2a_tools_delegation.py: import from _sanitize_a2a; wrap
tool_delegate_task return and tool_check_task_status response_preview
- Add 15 tests covering boundary escape, injection patterns, integration
shapes (workspace/tests/test_a2a_sanitization.py)
Follow-up (non-blocking, noted in PR#7 infra-lead review):
- Deduplicate if a2a_tools.py also wraps (currently handled in
delegation module only — callers get sanitized output regardless)
- tool_check_task_status: consider sanitizing 'summary' field too
Closes: molecule-ai/molecule-ai-workspace-runtime#7 (wrong-repo PR
that this supersedes)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>