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>
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>
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>
- 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 (fef664d)
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>
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.
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.
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.
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 (9542348). 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>