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>
ROOT CAUSE found in Gitea server logs:
actions/workflows.go:DetectWorkflows() [W] ignore invalid workflow
"publish-runtime.yml": unknown on type:
map["version":{"description":...,"required":true,"type":"string"}]
Gitea 1.22.6's workflow parser flattens workflow_dispatch.inputs.* into
top-level 'on:' event-keys and rejects the workflow when it doesn't
recognize them. Once rejected, the workflow never registers — so NO
event triggers it. publish-runtime.yml has 0 runs in action_run since
the .gitea port for exactly this reason; the runtime-v1.0.0 tag from
yesterday and hongming-pc's runtime-v0.1.130 from tonight both pushed
successfully but went nowhere.
This supersedes the paths-vs-tags hypothesis from #351 (PR #352).
The split is still useful for clarity but was NOT the cause — even
the original tags-only port had this same parse failure.
Fix: drop the inputs block. workflow_dispatch in Gitea 1.22.6 supports
no-input dispatch only. The bash logic for version derivation now uses
just two cases: tag-push (strip prefix) or anything-else (PyPI auto-bump).
Post-merge verification:
- watch for first-ever publish-runtime.yml run in action_run
- check Gitea log no longer emits 'ignore invalid workflow' for this file
- push a runtime-v0.1.130 tag → workflow fires → PyPI 0.1.130
Refs: #351 (root cause), #348 Q3 (the blocker)
publish-runtime.yml has never fired since the .gitea port (0 rows in
action_run.workflow_id='publish-runtime.yml' ever), which is why PyPI
is still at 0.1.129 despite Gitea having a runtime-v1.0.0 tag.
Root cause hypothesis: Gitea Actions evaluates the on.push.paths filter
against tag-push events too (no path diff → workflow skipped). PR #349
made this visible by adding the paths trigger, but the same defect
existed for the originally-ported tags-only trigger on this Gitea version
— hence the runtime-v1.0.0 tag also never published.
Fix: split into two files, each with a single unambiguous trigger shape.
- publish-runtime.yml : on.push.tags only (the publisher)
- publish-runtime-autobump.yml : on.push.branches+paths (NEW; the bumper)
The autobump file computes next version from PyPI latest, pushes
'runtime-v$VERSION' tag via DISPATCH_TOKEN (not GITHUB_TOKEN — needed
to trigger downstream workflows on Gitea), and exits. The tag push
then triggers publish-runtime.yml.
Test plan after merge:
1. Push no-op commit to workspace/. Observe autobump fire, push tag.
2. Observe publish-runtime.yml fire on the tag, publish 0.1.130 to
PyPI, cascade to template repos.
3. Verify 'action_run' shows >0 rows for both workflow_ids.
Adds back the original GitHub workflow's auto-publish trigger that was
dropped during the 2026-05-10 .gitea port (#206). Push to main or
staging filtered by workspace/** falls into the existing PyPI-latest
auto-bump path — no logic changes, just the missing trigger and a
comment correction.
Caveat: the workflow still requires PYPI_TOKEN as a repository secret
(or org-level). Without it the publish step will fail loudly with a
descriptive error. Q2 follow-up tracks setting the secret.
Refs: molecule-core#348
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>
Implements the Claude Design handoff (Molecules AI Mobile.html) as a
viewport-gated React tree under canvas/src/components/mobile/. < 640px
renders the new shell instead of the desktop ReactFlow canvas.
Six screens, all bound to live store data:
- Home (agent list + filter chips + spawn FAB)
- Canvas (mini-graph with pinch-to-zoom + pan + reset)
- Detail (status pills, tabs: Overview / Activity / Config / Memory;
Activity hits /workspaces/:id/activity)
- Chat (textarea composer, IME-safe Enter, sendInFlightRef guard;
bootstraps from agentMessages so the prior thread shows on entry)
- Comms (live A2A feed via /workspaces/:id/activity + ACTIVITY_LOGGED)
- Spawn (bottom sheet; fetches /templates so users pick what's actually
installed on their platform)
Plus a Me tab for mobile theme/accent/density.
Design system (palette.ts + primitives.tsx) ports tokens 1:1 from the
handoff: cream + dark palettes, T1-T4 tier chips, status dots with
halo, JetBrains Mono for IDs/timestamps. Inter + JetBrains Mono are
self-hosted via next/font/google so CSP `font-src 'self'` is honoured.
URL routing: routes sync to ?m=<route>&a=<id>; popstate restores route;
deep links seed initial state. /?m=detail without ?a collapses to home.
Accent override flows through React context (MobileAccentProvider) —
not by mutating the static MOL_LIGHT/MOL_DARK singletons.
SSR flash: isMobile is tri-state; loading spinner stays up until
matchMedia resolves so mobile devices never paint the desktop tree.
Desktop responsiveness fixes (separate but ride along):
- Toolbar: full-width with overflow-x-auto on mobile, logo text + count
hidden < sm, divider/border collapse to sm: only.
- SidePanel: full-screen on mobile via matchMedia, resize handle hidden.
- Canvas: MiniMap hidden < sm (was overlapping the New Workspace FAB).
Tests (51 total, 33 new):
- palette.test.ts (12) - normalizeStatus, tierCode, light/dark parity
- components.test.ts (10) - toMobileAgent field mapping + classifyForFilter
- MobileApp.test.tsx (12) - route stack, deep links, popstate, tab bar
hidden on chat, spawn overlay
- SidePanel.tabs.test.tsx (18) - regression-clean
Verified: tsc --noEmit clean across mobile/, page.tsx, layout.tsx.
Not yet verified: live phone browser (needs CP backend hydrated).
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Cover the canvas image publish workflow with the same `docker info`
guard added to publish-workspace-server-image.yml (commit 5216e781).
publish-canvas-image.yml was the only docker-build workflow still
missing the step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Molecule-AI GitHub org was suspended 2026-05-06; canonical SCM is
now git.moleculesai.app. external_connection.go was still emitting
github.com URLs in operator-facing copy-paste blocks, breaking
external-agent onboarding silently.
Per-site decisions (8 emit sites in 1 file):
- L124 (channel template doc comment): swap source-of-truth comment to
Gitea host.
- L137 /plugin marketplace add Molecule-AI/...: swap to explicit Gitea
HTTPS URL form. End-to-end-verified path per internal#37 § 1.A.
- L138 /plugin install molecule@molecule-mcp-claude-channel: marketplace
name is molecule-channel (per remote .claude-plugin/marketplace.json),
not the repo name. Fix to molecule@molecule-channel.
- L157 --channels plugin:molecule@molecule-mcp-claude-channel: same
marketplace-name fix.
- L179 user-facing GitHub URL: swap to Gitea.
- L261 pip install git+https://github.com/Molecule-AI/molecule-sdk-python:
not on PyPI; swap to git+https://git.moleculesai.app/molecule-ai/...
- L310 hermes-channel doc comment: swap source-of-truth comment.
- L339 pip install git+https://github.com/Molecule-AI/hermes-channel-molecule:
not on PyPI; swap to Gitea.
- L369 issue-tracker URL: swap to Gitea.
Verification:
- molecule-ai-workspace-runtime, codex-channel-molecule are on PyPI (200);
no swap needed for those pip lines (they were already package-name form).
- molecule-mcp-claude-channel, molecule-sdk-python, hermes-channel-molecule
are NOT on PyPI; swapped to git+https://git.moleculesai.app/molecule-ai/
form. All three repos are public on Gitea (default branch main) and
serve git-upload-pack unauthenticated (verified curl 200 against
/info/refs?service=git-upload-pack).
- Third-party github URLs (gin import, openai/codex, NousResearch/
hermes-agent upstream issue trackers, npm @openai/codex) intentionally
preserved.
Adds TestExternalTemplates_NoBrokenMoleculeAIGitHubURLs regression guard
to prevent the same broken URLs from re-emerging on future template
edits.
go vet / go build / existing TestExternal* — all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two surfaces in workspace-server hardcoded `ghcr.io` and silently bypassed
the `MOLECULE_IMAGE_REGISTRY` env override that flips every other image
operation to the configured private mirror (e.g. AWS ECR in production):
1. internal/imagewatch/watch.go — image-auto-refresh polled
`https://ghcr.io/v2/...` and `https://ghcr.io/token` directly. Post-
suspension, with the platform pointed at ECR, the watcher silently
stopped seeing digest changes (every poll either 404'd or hung on a
registry it has no business talking to).
2. internal/handlers/admin_workspace_images.go — Docker Engine auth
payload pinned `serveraddress: "ghcr.io"`, so when the operator sets
`MOLECULE_IMAGE_REGISTRY=…ecr…/molecule-ai` the engine matched the
wrong credential entry on every authenticated pull.
Fix: extract `provisioner.RegistryHost()` returning the host portion of
`RegistryPrefix()` (e.g. `ghcr.io` ← `ghcr.io/molecule-ai`, or
`004947743811.dkr.ecr.us-east-2.amazonaws.com` ← the ECR mirror prefix),
and route both surfaces through it. Default behavior is unchanged for
OSS users on GHCR.
Tests
- New `TestRegistryHost_SplitsHostFromOrgPath` and
`TestRegistryHost_NeverEmpty` pin the helper across GHCR / ECR /
self-hosted Gitea / bare-host edge cases.
- New `TestGHCRAuthHeader_RespectsRegistryEnv` asserts the Docker auth
payload's `serveraddress` follows MOLECULE_IMAGE_REGISTRY (and never
leaks the org-path suffix).
- New `TestRemoteDigest_RegistryHostFollowsEnv` stands up an httptest
server, points MOLECULE_IMAGE_REGISTRY at it, and confirms both the
token endpoint and the manifest HEAD land there — i.e. the full image-
watch loop respects the env override end-to-end.
Both new tests were verified to FAIL on the pre-fix code path before the
helper was wired in, so a future revert can't silently re-introduce the
bug.
Out of scope (followup needed)
ECR uses `aws ecr get-authorization-token` (SigV4 + basic-auth) instead
of GHCR's `/token?service=…&scope=…` flow. This PR makes the URL host-
configurable; the bearer-token negotiation in `fetchPullToken` still
speaks the GHCR flavor. On ECR with `IMAGE_AUTO_REFRESH=true`, the
watcher will now fail loudly at the token fetch (logged per tick) rather
than silently hitting ghcr.io. Operators on ECR should keep
IMAGE_AUTO_REFRESH=false until ECR auth is wired — tracked as a separate
task. Net effect of this PR alone is strictly better than pre-fix:
fail-loud > silent-broken.
Refs: RFC #229 P2-4
tier:low
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in yaml-utils.ts toYaml():
1. tools: [] was only emitted when config.tools.length > 0,
but the test asserts it's always present. Add blank-line
separator + unconditional list("tools", ...) so MINIMAL_CONFIG
with tools: [] renders correctly.
2. Nested list values (e.g. runtime_config.required_env: [KEY])
were serialized as " required_env: KEY" (stringification of the
array) instead of a YAML list block. Fix obj() to detect
Array.isArray(sv) and emit a list block with 4-space indent.
Closes#269.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-delegation deadlocks: the sending turn holds `_run_lock`, the receive
handler waits for the same lock, the A2A request 30s-times-out, and the
whole cycle is wasted (the Dev Lead system prompt warns agents off this by
hand — "Never delegate_task to your own workspace ID … there is no peer who
is also you"). The platform/runtime had no guard. Now both
`tool_delegate_task` and `tool_delegate_task_async` early-return an
actionable error when `workspace_id == effective_source` (`source_workspace_id
or _peer_to_source[target] or WORKSPACE_ID`) — before `discover_peer`, so no
network round-trip is wasted either. A genuinely different target (incl.
another of a multi-workspace agent's own registered workspaces) is
unaffected.
Tests: tests/test_a2a_tools_delegation.py — new TestSelfDelegationGuard (4
cases: rejects own ID; rejects when source_workspace_id explicitly == target;
async path rejects; a different target passes the guard through to
discover_peer). `pytest tests/test_a2a_tools_delegation.py` → 12 passed.
(tests/test_a2a_tools_impl.py's TestToolDelegateTask* suite is red on this
PC2/Windows checkout — same on `main` without this change; httpx-mock infra,
not this PR — CI validates on Linux.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[core-lead-agent] PR #281 merged — handles string-form errors in a2a_tools.delegate_task (was raising AttributeError on every delegation through legacy path), fixes empty-parts dict regression (#279), and drops the dead staging branch trigger from both publish workflows. Replaces the abandoned PR #268 + #277. Integration Tester unblocked for mesh recovery validation.
internal#226 follow-up #1. `molecule_runtime.config` resolves the picked
model as `MOLECULE_MODEL` > `MODEL` > (legacy) `MODEL_PROVIDER` (#280) —
this side of the boundary now matches:
- applyRuntimeModelEnv reads `MOLECULE_MODEL` ahead of `MODEL` /
`MODEL_PROVIDER`, and exports BOTH `MOLECULE_MODEL` and `MODEL`
(the latter kept for back-compat with everything that already reads
`os.environ["MODEL"]`). So a workspace whose secrets carry
`MOLECULE_MODEL` (the unambiguous name) is honoured, and the
`MODEL_PROVIDER` misnomer — which got set to provider slugs
("minimax") and even runtime names ("claude-code") — is the lowest-
priority fallback, exactly as on the runtime side.
- the resolution-order comment is updated to flag MODEL_PROVIDER as the
legacy-and-misleadingly-named var.
Also drops a stray trailing `}` in delegation_test.go (committed in
97768272 "test(delegation): add isDeliveryConfirmedSuccess helper") that
made `internal/handlers` fail to parse — one of the things keeping the
package from compiling for tests.
Tests: TestApplyRuntimeModelEnv_SetsUniversalMODELForAllRuntimes extended
to assert MOLECULE_MODEL mirrors MODEL on every case, plus two new cases
(MOLECULE_MODEL env fallback; MOLECULE_MODEL beats MODEL_PROVIDER). Could
not run `go test ./internal/handlers/` locally — the package is still
blocked behind `internal/plugins` `SourceResolver` redeclaration (the
#248 plugin-router/resolver refactor, Core-BE's lane); CI validates once
that lands. The applyRuntimeModelEnv change is mechanical (same shape as
the existing `MODEL` handling) — reviewer please eyeball.
Companion: molecule-core#280 (runtime config.py side), molecule-ai-workspace-template-claude-code#14 (CLI-stream-error surfacing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>