The replace directive `=> /plugin` breaks CI builds where go build runs
natively (no /plugin directory). Move the replace to Dockerfile RUN so
it only applies during Docker builds where the plugin is COPYed.
Fixes: "replacement directory /plugin does not exist" on CI runner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows Docker Desktop copies host files with CRLF even when
.gitattributes says eol=lf. The entrypoint now strips \r from all
hook .sh/.py files before dropping to agent user. Permanent fix for
the #507 CRLF regression that reappeared after every restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds callback_query to AllowedUpdates in Telegram polling. When CEO
clicks Yes/No inline keyboard buttons:
1. Acknowledges press (removes loading spinner)
2. Updates message with 'CEO approved/rejected'
3. Routes 'CEO_DECISION: approve:xyz' as inbound to the agent
Only one workspace polls per bot token (Triage Operator) — other
workspaces with Telegram use outbound-only via direct API.
Fixed: duplicate pollers causing 'terminated by other getUpdates'
errors — removed PM/DevLead/ResearchLead Telegram channel rows
(they send outbound via direct Telegram API calls, not channel manager).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HermesA2AExecutor now supports sending system context as ordered, separate
role=system messages instead of a single concatenated string — the model
format recommended by NousResearch.
Changes:
- HermesA2AExecutor.__init__: new system_blocks kwarg (list[str|None]|None)
stored as an independent copy; None blocks and empty strings silently skipped
- _build_messages(): when system_blocks is not None, emits each non-empty
block as a separate {"role": "system"} entry in Hermes-recommended order
(persona → tools context → reasoning policy); falls through to legacy
system_prompt path when system_blocks is None (backward compatible)
Backward compatibility: existing callers that pass a single system_prompt
string continue to work identically — no changes required.
Tests (12 new, 47 total):
- system_blocks stored as independent copy (mutation safe)
- three-block stacked ordering preserved
- empty / None blocks silently skipped
- all-empty list → zero system messages
- system_blocks overrides system_prompt when both provided
- legacy system_prompt path unchanged
- stacked blocks appear in the live API call kwargs
Closes#499
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Dockerfile COPY for molecule-ai-plugin-github-app-auth was lost
during a rebase earlier this session. Without it, the platform binary
compiled without the TokenProvider interface implementation, causing
/admin/github-installation-token to return 'no token provider registered'.
This forced hourly rolling restarts to refresh GH_TOKEN (the env var
from provision time expires after ~60 min). Each restart also required
re-applying 6 manual patches and caused ~2 min of A2A downtime where
agents reported peers as 'unresponsive'.
With this fix, the gh-wrapper in each container auto-refreshes tokens
via the platform endpoint on every gh call. Zero restarts needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a concise action advisory for engineering leads summarising the 9 open
findings from the full SAFE-MCP audit, with immediate remediation steps for
NEW-003 (unpinned npm packages in .mcp.json — HIGH), a Phase 35 scoping
recommendation for plugin supply-chain hardening (VULN-003, VULN-004), and
medium-term GLOBAL memory scope controls (VULN-002, VULN-005).
Pairs with: monorepo PR #808, docs PR #18
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per UIUX Cycle 5 spec, Dialog.Content should carry an explicit
aria-label="Conversation trace" in addition to the aria-labelledby
automatically wired by Radix Dialog via Dialog.Title. This provides
a fallback accessible name directly on the dialog container element.
All 732 tests pass, build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
molecule-medo now lives at Molecule-AI/molecule-ai-plugin-molecule-medo
(same pattern as all other plugins). Removed the gitignore exception
that kept it in the monorepo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Corrects the source-input aria-label wording to match the UIUX Cycle 4
spec exactly. Previous commit used "Install plugin from source URL";
spec says "Install from source URL" (matches the visible "Install from
source" section heading). Updates the corresponding test assertions.
No functional change. All 736 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _redact_secrets() in builtin_tools/security.py and apply it at every
commit_memory call site before content reaches the memories table.
Patterns scrubbed (replaced with [REDACTED]):
- sk-[A-Za-z0-9_-]{20,} OpenAI/Anthropic keys (sk-, sk-ant-, sk-proj-)
- ghp_[A-Za-z0-9]{36} GitHub classic PAT
- ghs_[A-Za-z0-9]{36} GitHub server-to-server token
- github_pat_[A-Za-z0-9_]{82} GitHub fine-grained PAT
- AKIA[0-9A-Z]{16} AWS access key ID
- key/token/secret/password/api_key=<40+ chars> Generic contextual (value replaced,
keyword preserved: "api_key=[REDACTED]" not "[REDACTED]")
Call sites wired:
- builtin_tools/memory.py::commit_memory() — LangChain tool (LangGraph path)
- a2a_tools.py::tool_commit_memory() — MCP server path
- executor_helpers.py::commit_memory() — CLI/SDK executor path
Implementation guarantees:
- Pure function (no side effects, no I/O)
- Idempotent: [REDACTED] does not match any pattern
- No false positives on normal prose (all patterns require ≥20-char prefix
or ≥40-char value after known keyword)
Tests (36 passing):
- Per-pattern unit tests for all 6 secret types
- Idempotency tests
- Normal prose non-regression tests
- Integration: a2a_tools.tool_commit_memory scrubs ghp_ tokens before HTTP POST
- Integration: executor_helpers.commit_memory scrubs AWS keys and OpenAI keys
- Source inspection: memory.py imports and applies _redact_secrets before
build_awareness_client() (i.e. before any storage operation)
conftest.py updated to load the real builtin_tools/security.py so that
executor_helpers and a2a_tools can import _redact_secrets during test collection.
Closes#834
Sub-issue of #725
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkspaceNode.eject.test.tsx: add draggable/selectable/deletable to
NodeProps render call (TS2739); add `as WorkspaceNodeData` cast on
makeNodeData return to silence Partial<> spread widening (TS2322)
The cherry-picked fix/canvas-test-fixture-budgetlimit commit (9e0aa61)
also lands here — it resolves latent test-fixture drift in 7 test files
that the incremental tsc cache had masked on main but that became visible
once the new WorkspaceNode.eject.test.tsx file invalidated the cache.
tsc --noEmit: 0 errors | npm test: 726 passed | npm run build: clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The budget PR (#541) added budgetLimit: number | null as a required field
on WorkspaceNodeData and budget_limit: number | null on WorkspaceData.
Seven test fixture factories were not updated, causing tsc --noEmit to
produce 34 TS2322/TS2345 errors (runtime tests still passed because
Vitest transpiles via esbuild which strips types).
Fixes:
- canvas-events.test.ts: makeNode factory +budgetLimit: null
- canvas-events-pan.test.ts: makeNode factory +budgetLimit: null
- canvas-capabilities.test.ts: makeNodeData factory +budgetLimit: null
- canvas-topology.test.ts: makeWS factory +budget_limit: null
- canvas.test.ts: makeWS factory +budget_limit: null; two inline
summarizeWorkspaceCapabilities args +budgetLimit: null; context-menu
fixture +budgetLimit: null
- ProvisioningTimeout.test.tsx: makeWS factory +budget_limit: null
Also fixes 3 TS2348 errors in AuthGate.test.tsx: newer Vitest type defs
resolve ReturnType<typeof vi.fn> to Mock<Procedure|Constructable> which
TypeScript no longer considers directly callable in a vi.mock factory.
Fix: intersect the mock variables with a plain function type so both the
call expression and the mock API (mockReturnValue etc.) type-check.
tsc --noEmit: 0 errors. npm test: 722/722.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR#842 merged the docs/opencode.json to main with the correct MCP URL path.
PR#840 branch had an older version — sync to main's content to resolve conflict.
PR#842 merged the docs/opencode.json to main with the correct MCP URL path.
PR#840 branch had an older version — sync to main's content to resolve conflict.
Security Auditor pre-merge conditions for PR#840:
C5: toolCommitMemory passes content directly to DB insert without secret
redaction. Gap is tracked to #838 (platform-wide _redactSecrets pass).
Adds inline TODO(#838) comment at the insert site so the gap is visible
in-code, not only in the issue tracker.
C6: toolDelegateTask sets X-Workspace-ID but no bearer token on the
outbound A2A call. The /workspaces/:id/a2a route is intentionally outside
WorkspaceAuth (by design in router.go). CanCommunicate is enforced before
the request is constructed, and callerID was authenticated by WorkspaceAuth
on the MCP bridge entry point. Documents this trust assumption at the call
site.
The inline JSON example still showed the bare ${MOLECULE_MCP_URL} without
the /workspaces/${WORKSPACE_ID}/mcp path. Updated to match opencode.json fix
in previous commit (bf80f15). Added WORKSPACE_ID to the env section.
Tables: Slack has no table syntax. Converter now detects markdown tables
and renders them as monospace code blocks with aligned columns.
Dividers: replaced unicode em-dash (caused encoding artifacts) with
plain ASCII dashes.
Strikethrough: ~~text~~ converts to ~text~ (Slack native).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The function was defined on a feature branch, referenced by manager.go
and slack_test.go, but never made it to main after the rebase. This
caused go build to fail with 'undefined: FetchChannelHistory', which
Docker masked by using a cached binary from the last successful build.
That cached binary had neither the mrkdwn blocks nor the Level 3
context injection — explaining why Slack messages showed raw markdown
despite the source having the converter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- EjectIcon now accepts React.SVGProps<SVGSVGElement> so aria-hidden can be passed
- Eject button: aria-label and title both use `Extract ${data.name} from team`
(previously title was static 'Extract from team'; aria-label was absent)
- <EjectIcon aria-hidden="true"> prevents assistive tech from double-announcing
the icon content inside the already-labelled button
- Added WorkspaceNode.eject.test.tsx (4 tests) covering aria-label, title,
label==title invariant, and aria-hidden on the SVG
Closes#854
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace denylist approach with strict allowlist: only PATH, HOME, LANG,
PYTHONPATH, WORKSPACE_ID, WORKSPACE_NAME, PLATFORM_URL (and a small set
of locale/Python runtime vars) pass through to agent-executed code. Every
other env var — including ANTHROPIC_API_KEY, GH_TOKEN, DATABASE_URL,
REDIS_URL, *_SECRET, *_PASSWORD — is stripped from os.environ for the
duration of SafeLocalPythonExecutor.__call__ and restored on exit.
- make_safe_env() is a pure read (never mutates os.environ)
- _ENV_PATCH_LOCK serialises concurrent calls for thread safety
- os.environ fully restored even on exception (try/finally)
- 38 unit tests covering all secret categories, thread safety, import
restrictions, and env-restore guarantees
Closes#826
Sub-issue of #804
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Slack's chat.postMessage renders the text field as plain text when
username override is used. Switching to blocks with type=mrkdwn
forces rich formatting (bold, links, code, dividers).
Also restores FetchWorkspaceChannelContext that was lost in rebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both were lost during the PR #844 rebase — the converter was in the
source but the binary couldn't compile because FetchWorkspaceChannelContext
was missing from manager.go (interface mismatch). Previous deploys
silently used the cached old binary without the converter.
Also removed unused 'log' import that blocked compilation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The budget PR (#541) added budgetLimit: number | null as a required field
on WorkspaceNodeData and budget_limit: number | null on WorkspaceData.
Seven test fixture factories were not updated, causing tsc --noEmit to
produce 34 TS2322/TS2345 errors (runtime tests still passed because
Vitest transpiles via esbuild which strips types).
Fixes:
- canvas-events.test.ts: makeNode factory +budgetLimit: null
- canvas-events-pan.test.ts: makeNode factory +budgetLimit: null
- canvas-capabilities.test.ts: makeNodeData factory +budgetLimit: null
- canvas-topology.test.ts: makeWS factory +budget_limit: null
- canvas.test.ts: makeWS factory +budget_limit: null; two inline
summarizeWorkspaceCapabilities args +budgetLimit: null; context-menu
fixture +budgetLimit: null
- ProvisioningTimeout.test.tsx: makeWS factory +budget_limit: null
Also fixes 3 TS2348 errors in AuthGate.test.tsx: newer Vitest type defs
resolve ReturnType<typeof vi.fn> to Mock<Procedure|Constructable> which
TypeScript no longer considers directly callable in a vi.mock factory.
Fix: intersect the mock variables with a plain function type so both the
call expression and the mock API (mockReturnValue etc.) type-check.
tsc --noEmit: 0 errors. npm test: 722/722.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agents output standard Markdown (Claude Code default) but Slack uses
its own mrkdwn format. Without conversion:
**bold** shows as literal **bold**
### heading shows as literal ###
[text](url) shows as raw markdown link
Converter handles:
**bold** → *bold* (Slack bold is single asterisk)
### heading → *heading* (bold text, no headings in Slack)
[text](url) → <url|text> (Slack link format)
--- → ——— (visual separator)
`code` and ```blocks``` pass through unchanged
6 new tests: bold, heading, link, hr, code block, mixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two WCAG violations in the Danger Zone delete flow:
1. WCAG 4.1.3 (Status Messages): the confirmation UI that appears when
the user clicks "Delete Workspace" had no ARIA live region, so screen
readers never announced the confirmation prompt. Adding role="alert"
to the confirmation container makes it an implicit assertive live
region that is announced immediately.
2. WCAG 2.4.3 (Focus Order): pressing Cancel left focus wherever the
browser placed it (often body). Keyboard users had to re-navigate to
find the Delete Workspace button. The Cancel handler now calls
deleteButtonRef.current?.focus() to return focus to the trigger
button, matching the expected modal/disclosure focus-management pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the onboarding card had no landmark role and no
live region, so screen readers had no way to know the card exists or
that the step changed.
- Add role="complementary" aria-label="Onboarding guide" to the card
container so it appears as a named landmark in assistive technology.
- Add a role="status" aria-live="polite" aria-atomic="true" sr-only div
that holds the current step label. When the step state changes React
updates the div content, which the live region broadcasts to the AT
without pulling focus away from the user's current position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NVDA and other screen readers ignore the title attribute on interactive
elements and non-interactive divs. Add aria-label alongside title on:
- Stop All button (dynamic label reflects active task count)
- Restart All button (dynamic label reflects pending workspace count)
- StatusPill component (online/offline/failed/provisioning counts)
- WsStatusPill component (connected/connecting/disconnected variants)
Inner dot and text spans get aria-hidden="true" so the screen reader
reads the single aria-label rather than individual child nodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the error div that appears after a failed workspace
deploy or blank-workspace create had no ARIA live region, so screen
readers never announced it. Adding role="alert" makes the message an
implicit aria-live="assertive" region so assistive technology surfaces
the error immediately without requiring the user to navigate to it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire WCAG 1.3.1 label associations: 6 bare <label>+control pairs in
ConfigTab (Description, Tier, Runtime, Effort, Task Budget, Backend) now
use stable useId() IDs with matching htmlFor/id. Field helper in
DetailsTab updated to generate its own fieldId via useId() and inject it
into the child element via cloneElement, so every Name/Role/Tier field in
edit mode is correctly associated without requiring call-site changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>