NotAvailablePanel (12 cases):
- Heading, description text, runtime name display, SVG icon with
aria-hidden, mono font for runtime, Chat tab guidance
- Full-height flex container class names
- h3 heading role, SVG aria-hidden, descriptive paragraph
- Short and complex runtime names
FilesToolbar (17 cases):
- Directory select with aria-label, file count display
- Export and Refresh buttons always visible
- New/Upload/Clear shown only when root="/configs", hidden for
/workspace, /home, /plugins
- setRoot called on directory change
- onNewFile, onDownloadAll, onClearAll, onRefresh called on click
- Hidden file input present with aria-label when on /configs
- All buttons have accessible names
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scope:
- form-inputs.test.tsx (new): 35 cases covering TextInput, NumberInput,
Toggle, TagList, Section. Section coverage includes aria-expanded,
aria-controls, content id, and aria-hidden indicator span.
- form-inputs.tsx (Section): add aria-expanded + aria-controls to the
toggle button and a matching id on the collapsible content region;
aria-hidden on the ▾/▸ indicator so screen readers skip it.
Test isolation fixes (afterEach(cleanup) missing → DOM element accumulation):
- ApprovalBanner.test.tsx
- StatusDot.test.tsx — also adds { hidden: true } to getByRole("img")
since @testing-library/dom v10+ excludes
aria-hidden elements from accessible queries
- ValidationHint.test.tsx — also fixes checkmark test that assumed
✓ + "Valid format" were one text node
- TopBar.test.tsx
- RevealToggle.test.tsx
- StatusBadge.test.tsx
Tooltip.test.tsx:
- Adds vi.useFakeTimers() beforeEach / vi.useRealTimers() afterEach
(tests called vi.advanceTimersByTime without fake timers)
- Fixes aria-describedby test to check the wrapper div, not the button
KeyValueField.tsx:
- Adds role="textbox" to the <input> element so getByRole("textbox")
finds it in @testing-library/dom v10 (password inputs lack implicit
textbox role in jsdom).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a workspace delegates a task via POST /workspaces/:id/a2a, the
proxy records the response via logA2ASuccess which writes
activity_type='a2a_receive'. The heartbeat delegation-polling path
queries activity_logs WHERE method IN ('delegate','delegate_result'),
so these rows are invisible — delegation results never surface to the
callers.
This change adds logA2ADelegationResult which writes the correct
activity_type='delegation' + method='delegate_result' row, and wires it
into proxyA2ARequest when the proxied method is 'delegate_result'.
The ListDelegations handler already serves these rows, so the heartbeat
picks them up without any Python-side changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds resolveInsideRoot inside loadWorkspaceEnv so a malicious
org YAML cannot escape the org root via ../../../etc-style filesDir.
Also fixes pre-existing Go 1.25 + go-sqlmock v1.5.2 build
incompatibility in instructions_test.go:
- Removes unused database/sql import
- Removes unused now := time.Now() variable
- Removes TestScanInstructions_ScanError (broken in Go 1.25;
*sqlmock.Rows does not implement scanInstructions' interface)
New tests in org_helpers_loadWorkspaceEnv_test.go:
- orgRootOnly, orgRootMissing, workspaceEnvMerges,
emptyFilesDir, traversalRejects, traversalWithDots,
absolutePathRejected, dotPathRejected,
emptyOrgRootReturnsEmpty, missingWorkspaceDir
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an optional `stderr` parameter to sanitize_agent_error(). When
provided, up to 1 KB of stderr text is included in the A2A error
response after sanitization (API keys / bearer tokens ≥20 chars /
long paths redacted). The existing generic form is preserved when
stderr is absent. Updates both the main a2a_executor and the google-adk
adapter.
Closes: roadmap item — SDK executor stderr swallowing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers extractToolTrace — the only untested pure function in the file.
Tests are JSON-only, no DB mocking needed:
- Happy path: result.metadata.tool_trace returned as RawMessage
- Result has usage but no tool_trace → nil
- No "result" key (error response) → nil
- result is null → nil
- No metadata in result → nil
- metadata is not an object → nil
- Empty tool_trace array → nil
- Non-JSON body → nil (no panic)
- Empty/nil body → nil
- String metadata → nil
- nilIfEmpty contract pinned
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
force-merge: review-timing race (hongming-pc Five-Axis APPROVED at 07:54Z, sop-tier-check ran at 07:41Z before review landed; gate working, only timing-race per feedback_pull_request_review_no_refire); see audit-force-merge trail
Fixes the second unsanitized exit point flagged in issue #413:
- task_id filter path: sanitize summary + response_preview before returning raw delegation object
- list path (all recent): sanitize both fields in every delegation entry before embedding in JSON
Both are peer-supplied delegation ledger data returned via the JSON polling endpoint.
Sync path (lines 173, 182) was already fixed in #416.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
REGRESSION: Staging commit 8e94c178 (PR #390) added sanitize_a2a_result
calls to _delegate_sync_via_polling but did NOT add the import. Any
delegation completing via the polling path raises NameError at runtime.
One-line fix: add `from _sanitize_a2a import sanitize_a2a_result`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to #369. `resolveInsideRoot` used `filepath.Abs` which does NOT
resolve symlinks — so "workspaces/dev/leaked" where "leaked" is a symlink
to "/etc" would lexically pass the prefix check but resolve outside root.
Fix: call `filepath.EvalSymlinks` before the final prefix check. If the
resolved path points outside root the function returns "path escapes root".
Broken symlinks are also rejected (fail closed).
Also add TestResolveInsideRoot_RejectsSymlinkTraversal covering:
- Symlink pointing outside → rejected (CWE-59)
- Symlink staying inside root → allowed
- Broken symlink → rejected
When GITHUB_APP_ID/INSTALLATION_ID/PRIVATE_KEY_FILE are unset (Gitea-
canonical deployment or suspended GitHub App org), generateAppInstallation
Token() returns "required" — a permanent configuration error, not a
transient one. Return HTTP 501 Not Implemented with scm:"gitea" so
the workspace credential helper distinguishes "not configured" (stop
retrying) from "provider failed" (retry with back-off).
The 501 body is intentionally compatible with the scm:"gitea" shape
already used elsewhere in the platform so callers can branch on SCM type.
Issue #381: agent tick generators producing stale-repo state.
Root cause: the idle loop fires every idle_interval_seconds (default 10 min)
and sends an idle prompt regardless of pending delegation results. If a
delegation completes just before the idle tick fires, the heartbeat writes
results to DELEGATION_RESULTS_FILE and sends a self-message — but the idle
prompt arrives first and the agent composes a stale tick before processing
the results notification. Peers receive repeated identical asks.
Fix: before sending the idle prompt, read DELEGATION_RESULTS_FILE. If it
contains unconsumed results, skip this idle tick. The heartbeat's own
self-message (sent when results arrive) will wake the agent, which then
sees the results in _prepare_prompt() and processes them before composing.
Companion to wsr PR (runtime-runtime mirror).
Changes:
- workspace/main.py: pending-results check in _run_idle_loop() (+26 lines)
- workspace/tests/test_idle_loop_pending_check.py: 6-case unit test
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue: _delegate_sync_via_polling (RFC #2829 PR-5 sync path) returned
unsanitized response_preview and error_detail fields to the agent context.
A malicious peer could inject trust-boundary markers to break the boundary
established by the main sanitization layer.
Changes:
- a2a_tools_delegation.py: sanitize response_preview before returning on
completed; sanitize error_detail/summary before wrapping in _A2A_ERROR_PREFIX
- test_a2a_tools_delegation.py: TestPollingPathSanitization covers both paths
Companion to PR #382 (runtime/offsec-003-executor-sanitize) which covers
the async heartbeat path in executor_helpers.read_delegation_results.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds _sanitize_a2a.py (from PR #346) and integrates sanitize_a2a_result()
into read_delegation_results() so peer-supplied summary and response_preview
fields are escaped before being injected into the agent prompt.
Output is wrapped in [A2A_RESULT_FROM_PEER]...[/A2A_RESULT_FROM_PEER]
boundary markers so content after the block is clearly not from a peer.
Fixes:
- test_a2a_executor.py: correct mock patch path to executor_helpers
- test_executor_helpers.py: fix boundary-injection test assertion to match
_strip_closed_blocks behaviour (closes marker, removes following text)
Follow-up to PR #346 (OFFSEC-003 boundary escape) which noted
"read_delegation_results() path still needs sanitization" as a gap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause: the sop-tier-check.sh script uses jq extensively for all
JSON API parsing (whoami, labels, team IDs, reviews). Gitea Actions
runners (ubuntu-latest label) do not bundle jq — script exits at
line 67 with "jq: command not found", producing "Failing after 1-3s"
status on every staging PR.
Fix: add apt-get install -y jq step before the script run.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Canvas template-deploy path returned HTTP 500 with raw pq error
when a user clicked a template card twice in quick succession. Root
cause: migration 20260506000000 added the partial-unique index
`workspaces_parent_name_uniq` on (COALESCE(parent_id, sentinel), name)
WHERE status != 'removed' to close TOCTOU on /org/import (#2872). The
org-import handler resolves the constraint via ON CONFLICT DO NOTHING
+ idempotent re-select. The Canvas Create handler did not — it
bubbled the pq violation as a generic 500.
Fix: auto-suffix the user-typed name on collision via a small retry
helper that pins on SQLSTATE 23505 + constraint name (so unrelated
unique indexes still fail loud), retries with " (2)", " (3)" up to
N=20, and threads the actually-persisted name back into the response
+ broadcast payload (so the canvas displays what the DB actually
holds). Exhaustion maps to a clean 409 Conflict instead of a 500.
#2872 protection is preserved unchanged — the index stays in place,
and /org/import's ON CONFLICT path is unaffected. The bundle-import
INSERT (handlers/bundle.go) is a separate code path and is not
touched here; if it surfaces the same UX issue a follow-up can adopt
the same helper.
Verification (against running localhost:8080 platform):
Three back-to-back POSTs with name="ManualVerify-1778459812":
POST #1 -> 201, id=db2dacf7-…, persisted name="ManualVerify-1778459812"
POST #2 -> 201, id=f468083d-…, persisted name="ManualVerify-1778459812 (2)"
POST #3 -> 201, id=5f5ae905-…, persisted name="ManualVerify-1778459812 (3)"
Log lines: "name collision auto-suffix \"…\" -> \"… (N)\""
Tests:
- workspace_create_name_test.go — 4 unit tests via sqlmock pin the
retry contract (happy path no-suffix, single-collision -> " (2)",
non-retryable error pass-through, exhaustion -> errWorkspaceNameExhausted).
- workspace_create_name_integration_test.go — 2 real-Postgres tests
(build tag `integration`) confirm the partial-unique index
behaviour AND the WHERE status != 'removed' tombstone exemption.
- Watch-it-fail confirmed: temporarily removing the
`fmt.Sprintf("%s (%d)", baseName, attempt+1)` candidate-naming
line makes TestInsertWorkspaceWithNameRetry_SecondAttemptSuffixed
fail with the expected argument-mismatch from sqlmock.
Pre-existing test failures in handlers/ (TestExecuteDelegation_…,
TestMCPHandler_CommitMemory_GlobalScope_Blocked) reproduce on
unmodified staging and are NOT caused by this change.
Cherry-pick of d79a4bd2 from PR #318 onto fresh main base (PR #318 closed).
Issue #310: platform a2a-proxy logs ~300/hr
`timeout awaiting response headers` because ResponseHeaderTimeout was hardcoded
to 60s. Opus agent turns (big context + internal delegate_task round-trips)
routinely exceed 60s, so the proxy gave up before headers arrived even when
the workspace agent was healthy.
Changes:
- a2a_proxy.go: ResponseHeaderTimeout: 60s hardcoded →
envx.Duration("A2A_PROXY_RESPONSE_HEADER_TIMEOUT", 180s).
180s gives Opus turns comfortable headroom. The X-Timeout caller header
still bounds the absolute request ceiling independently.
- a2a_proxy_test.go: TestA2AClientResponseHeaderTimeout verifies the 180s
default and env-override parsing logic.
Env var: A2A_PROXY_RESPONSE_HEADER_TIMEOUT (e.g. 5m, 300s).
Closes#310.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plugin adapters in molecule-skill-* repos do:
from plugins_registry.builtins import AgentskillsAdaptor as Adaptor
But _load_module_from_path() used exec_module() with a fresh module
namespace that did NOT have plugins_registry or its submodules in sys.modules,
causing:
ModuleNotFoundError: No module named 'plugins_registry'
Fix: before exec_module(), import and register plugins_registry + all three
submodules (builtins, protocol, raw_drop) in sys.modules so adapter imports
resolve correctly. Follows the Option 1 recommendation from issue #296.
Also adds test_resolve_plugin.py verifying the fix for both the
AgentskillsAdaptor import and the full InstallContext/resolve/protocol import.
Closes#296.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports the bounded retry+backoff around each `git clone` in
scripts/clone-manifest.sh onto main, mirroring PR #298 which landed the
same change on staging. CI-infra carve-out: publish-workspace-server-image.yml
fires on `push: branches:[main]`, so the retry mitigation must be on main for
the workflow to be resilient to the OOM-killed-git-mid-clone flake
(`error: git-remote-https died of signal 9`, run 4622) when triggered by a
main push. Same one-file change as #298 (+45/-5), POSIX-sh, sh -n clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Docker daemon health-check fix should not change which branches trigger
the build. Revert accidental addition of 'staging' to branch filters.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>