Commit Graph

30 Commits

Author SHA1 Message Date
f92147abdd fix: v0.4.0-gitea.3 — claude/channel capability + notify echo filter + README (closes Reno-Stars P0+P1+P2) (#8)
All checks were successful
Test / bun test (push) Successful in 8s
Reno-Stars (airenostars) verified install end-to-end against v0.4.0-gitea.2 ONLY after applying a local patch. This ships the same patch upstream so fresh self-hosters get a working install out-of-the-box.

P0: declare experimental.claude/channel + claude/channel/permission on the Server constructor.
P1: skip outbound method=notify rows in pollWorkspace (prevents reply echo as fake user turn).
P2: README accuracy — drop broken one-liner; document allowedChannelPlugins object-shape + managed-settings location.

Version bump 0.4.0-gitea.2 → 0.4.0-gitea.3. Tests: 27 pass / 0 fail (+9 new). Force-merged: same Gitea CI flake as other repos today; verified locally.
2026-05-07 18:02:00 +00:00
00799b45c6 fix: v0.4.0-gitea.3 — claude/channel capability + notify echo filter + README accuracy
All checks were successful
Test / bun test (pull_request) Successful in 23s
Reno-Stars (airenostars) verified install end-to-end against v0.4.0-gitea.2
ONLY after applying a local patch. This ships the same patch upstream so
fresh self-hosters get a working install out-of-the-box, no per-host
workaround required.

P0 (CRITICAL) — declare experimental.claude/channel capability
The Server constructor previously declared `{ capabilities: { tools: {} } }`.
Without `experimental['claude/channel']` (and the companion
`claude/channel/permission` flag), Claude Code's MCP host treats the
server as tool-only and silently drops every `notifications/claude/channel`
event we emit. Symptom: poll loop runs cleanly, cursor advances, stderr
says "delivered", message never reaches the conversation. Mirrors the
shape used by the official telegram channel plugin's MCP server.

P1 — filter outbound `method=notify` rows in pollWorkspace
The activity feed under `?type=a2a_receive` ALSO returns the agent's own
outbound /notify calls (recorded with method='notify' and source_id=null
on the same workspace_id). emitNotification would classify them as
canvas_user inbound and the reply would echo back as a fake user turn one
poll later — the model would see its own answer as a new user prompt and
try to "respond" to it. Filtered at the per-row layer via a new pure
helper `shouldEmitActivity` so the cursor still advances past the
skipped rows.

P2 — README accuracy
- Drop the `claude --channels plugin:…` one-liner instruction (silently
  no-ops on Claude Code 2.1.129; only the marketplace flow works).
- Document `allowedChannelPlugins` schema: it's an array of OBJECTS
  `{ plugin, marketplace }`, not strings — the host's Zod validator
  silently ignores string entries, which is the most common cause of
  "plugin installed but no notifications" reports.
- Document `allowedChannelPlugins` LOCATION: only takes effect from the
  managed-settings file (/Library/Application Support/ClaudeCode/
  managed-settings.json on macOS, /etc/claude-code/managed-settings.json
  on Linux), NOT from `~/.claude/settings.json`. Most self-hosters try
  user settings first.

Tests
Added channel-capabilities-and-filter.test.ts (9 cases) that pin both
regressions via two small exported surfaces (`SERVER_CAPABILITIES`,
`shouldEmitActivity`). Verified the new tests fail when each fix is
reverted: removing the experimental block makes 2 tests fail; removing
the notify-method filter makes 2 tests fail. 27 pass / 0 fail (was 18).

Version bump (all four manifests + the Server() literal):
0.4.0-gitea.2 → 0.4.0-gitea.3.

Closes Reno-Stars feedback P0+P1+P2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:01:33 -07:00
73367764f9 Merge pull request 'fix(presence): POST /registry/heartbeat keepalive (closes #6, closes molecule-core#24)' (#7) from fix/6-presence-heartbeat into main
All checks were successful
Test / bun test (push) Successful in 9s
2026-05-07 15:28:54 +00:00
b3b79a5efc fix(presence): POST /registry/heartbeat ticker so canvas badge stays online (closes #6, closes molecule-core#24)
All checks were successful
Test / bun test (pull_request) Successful in 20s
v0.4.0-gitea.1 polled /workspaces/:id/activity but never sent
/registry/heartbeat. The platform's healthsweep
(workspace-server/internal/registry/healthsweep.go) flipped any
runtime='external' workspace whose last_heartbeat_at was older than
90s back to status='awaiting_agent', so the canvas presence badge
stuck on awaiting_agent within 90s of plugin start even while A2A
traffic flowed fine over the long-poll loop.

Fix: per-workspace heartbeat ticker (default 30s, three ticks inside
the 90s stale window) POSTs the minimal HeartbeatPayload shape
(workspace_id only) — same path the Python runtime in
workspace/heartbeat.py uses when it has nothing else to report.

heartbeat.ts pure module + Bun.serve fixture test pin the wire
shape (POST + bearer + Origin + workspace_id body) so a future
refactor that drops any of those silently re-breaks the badge.

Bump 0.4.0-gitea.1 → 0.4.0-gitea.2 and document
MOLECULE_HEARTBEAT_INTERVAL_MS in README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 08:28:24 -07:00
ed5cbe02b5 Merge pull request 'fix(test): preload env-mock so server.ts can be imported in tests' (#5) from fix/3-mock-env-for-tests into main
All checks were successful
Test / bun test (push) Successful in 8s
2026-05-07 12:04:47 +00:00
devops-engineer
1a988eba1a fix(test): preload env-mock so server.ts can be imported in tests (closes #3 phase 2)
All checks were successful
Test / bun test (pull_request) Successful in 21s
server.ts:92 has a required-config guard that calls process.exit(1)
when MOLECULE_PLATFORM_URL / MOLECULE_WORKSPACE_IDS /
MOLECULE_WORKSPACE_TOKENS are unset. Test files import pure helpers
from server.ts (formatRemovedWorkspaceError + others), and the mere
act of importing executes server.ts top-level — including that guard —
which kills the test runner before any test runs.

Fix: add tests/setup.ts that sets fake values via ??= (only when unset,
so a dev running 'bun test' with a populated .env locally isn't
overridden), and bunfig.toml [test].preload that runs setup.ts before
any test file imports.

Verified locally: 13 pass / 0 fail. The server.ts boot path still runs
during tests (watchers spin up, fail to connect to localhost:18080,
log a warning) but doesnt affect the pure-helper tests.

Phase 2 of mcp-claude-channel#3 — Phase 1 was the bun-version pin
in PR #4 (one CI hop earlier; setup-bun is no longer the failure
point). This PR addresses the *test setup* failure that became
visible once setup-bun stopped masking it.
2026-05-07 05:02:40 -07:00
security-auditor
25e1cc2770 ci: re-trigger after runner-config v2 (CONFIG_FILE fix)
Some checks failed
Test / bun test (push) Failing after 20s
Verify whether failure was setup-python toolcache class (now fixed via
orchestrator's runners-1-8 recreate) or real CODE class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:57:25 -07:00
9d43fb096a Merge pull request 'fix(marketplace): switch source from '.' to url-form (#37 Phase 4 verify caught the §1 weakest spot)' (#2) from fix/marketplace-source-url-form into main
Some checks failed
Test / bun test (push) Failing after 17s
2026-05-07 09:23:45 +00:00
documentation-specialist
a6a61b78d0 fix(marketplace): switch source from '.' to url-form (#37 Phase 4 verify)
Some checks failed
Test / bun test (pull_request) Failing after 17s
Phase 4 E2E install verify of #37 (clean Docker container,
node:20-bookworm, Claude Code 2.1.132 latest stable):

Step 1: claude plugin marketplace add — SUCCEEDED (marketplace.json
recognized, validated, registered as 'molecule-channel').

Step 2: claude plugin install molecule@molecule-channel — FAILED with:
'This plugin uses a source type your Claude Code version does not
support. Update Claude Code and try again.'

Root cause: source: '.' (relative-path form) is not supported in
Claude Code 2.1.132. The url-form is supported and works
identically:

  source:
    source: url
    url: https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel.git

Re-tested with the url-form: claude plugin install succeeded; plugin
appears in 'claude plugin list' as 'molecule@molecule-channel'
v0.4.0-gitea.1, status: enabled.

This is exactly the §1 weakest spot in molecule-mcp-claude-channel#1's
hostile self-review:
  '"source: "."" in marketplace.json is a guess at semantics. The
  Claude Code docs confirm source accepts a local path or git URL
  but don't give an example for "the plugin is at the marketplace
  root". "." is the conventional same-dir-as-marketplace.json
  semantic; if it's wrong, claude plugin install fails fast in
  Phase 4 with a clear error and the fix is one line.'

The fix IS one line. Phase 4 forecast was correct.

Refs: molecule-ai/internal#37 (Phase 4)
2026-05-07 02:22:18 -07:00
9dd22e3b3e Merge pull request 'fix(install): make Gitea repo install-ready + canonical marketplace flow (#37)' (#1) from fix/install-path-gitea-marketplace into main
Some checks failed
Test / bun test (push) Failing after 26s
2026-05-07 06:20:52 +00:00
documentation-specialist
ebeb7d9f55 fix(install): make Gitea repo install-ready + canonical marketplace flow (#37)
Some checks failed
Test / bun test (pull_request) Failing after 20s
Adds the bits needed for `claude plugin marketplace add
https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel.git`
to actually work, and rewrites the README install section to use the
canonical Claude Code marketplace flow (replacing the suspended-GitHub
install command).

Changes:
- New `.claude-plugin/marketplace.json` describing the single-plugin
  marketplace `molecule-channel` with one plugin entry `molecule`.
  Required for `claude plugin marketplace add` to recognize this repo.
- Bump `.claude-plugin/plugin.json` version `0.1.0` → `0.4.0-gitea.1`.
- Bump `package.json` version `0.3.0` → `0.4.0-gitea.1`. (Both versions
  were already drifted; this reconciles them.)
- README install section rewritten:
  - Old: `claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel`
    (suspended-GitHub URL, no longer resolves)
  - New: `claude plugin marketplace add https://git.moleculesai.app/...`
    + `claude plugin install molecule@molecule-channel` (canonical
    marketplace flow per Claude Code docs)
- README "Contributing" issues link migrated github.com → Gitea.
- Migration note for users on the old install path: GitHub
  Molecule-AI org permanently gone 2026-05-06; new path is direct
  replacement, behavior unchanged.

Version `0.4.0-gitea.1` admits the lineage gap with the
never-recovered-from-GitHub `v0.4.x` work; preserves user expectation
of a 0.4.x release. Annotated tag to follow this PR (re-tag is cheap if
Hongming prefers a different version).

Refs: molecule-ai/internal#37, molecule-ai/internal#38
2026-05-06 23:14:42 -07:00
Hongming Wang
9b2a3c03a4
Merge pull request #22 from Molecule-AI/feat/410-removed-workspace-error
Some checks failed
Test / bun test (push) Failing after 21s
feat(channel): surface 410 Gone with re-onboard hint (#2429)
2026-04-30 22:13:38 -07:00
Hongming Wang
53e4ac329d feat(channel): surface 410 Gone with re-onboard hint instead of HTTP-410 (#2429)
Follow-up to molecule-core#2449 (which taught the platform to return
410 Gone for status='removed'). Without this branch the operator sees
`get_workspace_info failed: HTTP 410 — workspace removed` and has to
guess what to do — exactly the 2026-04-30 silent-fail UX hit on the
hongmingwang tenant.

The new code path:
  1. Detect resp.status === 410 explicitly
  2. Best-effort parse the body for id / removed_at / hint
  3. Throw `Workspace <id> was deleted on the platform at <ts>. <hint>`

The 410-message-formatting is extracted into a pure
`formatRemovedWorkspaceError` helper so it can be unit-tested
without mocking fetch + resolveWatching. Four new bun:test cases:

  - prefers platform-supplied id, removed_at, hint
  - falls back to local workspaceId + default hint when body is empty
  - tolerates null/undefined body (unparseable response)
  - omits ' at <ts>' clause when removed_at is missing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:10:00 -07:00
Hongming Wang
96a753dcf1
Merge pull request #21 from Molecule-AI/test/bun-test-bootstrap-extract-text
test(extractText): bootstrap bun:test + pin v0/v1 part discriminator
2026-04-30 20:15:57 -07:00
Hongming Wang
68d8ae981f test(extractText): bootstrap bun:test + pin v0/v1 part discriminator
Extracts the extractText helper + ActivityEntry type into their own
module so unit tests can import them without dragging server.ts's
top-level boot side-effects (cursor load, MCP transport connect, poll
loop) into the test runner. server.ts re-imports both — the wire
behavior is unchanged.

Tests cover the v0/v1 part-discriminator regression that landed on
2026-04-30 (every canvas peer message returning act.summary because
parts had `kind` instead of `type`):
- v1 `kind: text` parts (current production shape)
- v0 `type: text` back-compat
- multi-part text join, ignore non-text parts
- body-shape priority: params.message.parts > params.parts > body.parts
- empty-text-part recovery: skips empty candidate, tries next
- summary fallback when no shape matches
- `(empty A2A message)` fallback when summary is null

Adds:
- extract-text.ts (helper + type, no side-effects)
- extract-text.test.ts (9 tests)
- .github/workflows/test.yml (bun test on push/PR)
- "test": "bun test" script in package.json

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:15:31 -07:00
Hongming Wang
69a21a0ea9
Merge pull request #20 from Molecule-AI/fix/extract-text-kind-discriminator
fix(extractText): accept both kind and type part discriminators
2026-04-30 18:28:56 -07:00
Hongming Wang
7402011d80 fix(extractText): accept both kind and type part discriminators
a2a-sdk v0 used `type`, v1 (current production) uses `kind`. The real
platform sends `kind === 'text'` for text parts, so the v0-only filter
silently dropped every part on every inbound A2A message — the user
saw act.summary instead of the actual message body.

Same v0/v1 part-shape bug that was fixed in workspace/inbox.py
(molecule-core PR #2415); this is the channel-bridge port.

Reproduced live on hongmingwang tenant 2026-04-30: canvas peer
messages were arriving in /workspaces/:id/activity but Claude saw
only the summary line, not the actual message text. Same fix as
the universal-MCP path: filter on `kind === 'text' || type === 'text'`.

No tests added — channel-bridge repo has no test runner configured.
The equivalent fix in inbox.py has full v0/v1 part-shape tests
(test_inbox.py: test_extract_text_accepts_kind_text_v1,
test_extract_text_accepts_type_text_v0_compat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:28:31 -07:00
Hongming Wang
40fa2d3810
Merge pull request #19 from Molecule-AI/feat/v0.3-canvas-user-reply-and-peer-discovery
feat(v0.3): mirror universal-MCP surface for external workspaces
2026-04-30 15:05:52 -07:00
Hongming Wang
bcc7db5c86 feat(v0.3): mirror universal-MCP surface for external workspaces
Brings the channel's tool surface up to parity with the in-container
universal MCP (workspace/platform_tools/registry.py). External agents
driven through this channel now get the same 9 tools that an in-container
agent gets — same names, same input shapes, same semantics — so a hermes
or codex session bridged via this channel can do anything a containerized
claude-code can.

Tools:
- reply_to_workspace — smart-routed (canvas_user → /notify, peer_agent → /a2a).
  Was peer-only; now handles canvas chat replies too. Works for the most
  common case (user types in My Chat, agent replies in My Chat) which the
  v0.2 channel silently dropped.
- delegate_task / delegate_task_async / check_task_status — proactive A2A.
  Previously only inbound peer messages could trigger an outbound a2a;
  the agent could not initiate.
- list_peers — peer discovery. Backed by GET /registry/:id/peers.
- get_workspace_info — self-introspection. Needed for memory scope checks
  (only tier-0 roots can write GLOBAL).
- send_message_to_user — canvas push with attachments. Multipart upload via
  /chat/uploads then /notify, mirrors the universal tool's two-phase shape.
- commit_memory / recall_memory — persistent memory across sessions, with
  LOCAL/TEAM/GLOBAL scopes. Platform enforces RBAC + scope.

Inbound delivery:
- Drop seed-then-skip on first run. The previous policy assumed events
  before session start were "out of context"; in practice operators
  restart Claude Code mid-conversation and EXPECT the queued messages.
  Cold start now delivers everything in POLL_WINDOW_SECS and advances
  the cursor past it.
- emitNotification adds meta.kind ('canvas_user' | 'peer_agent') so
  Claude can pick the right reply form without re-parsing the row.

Channel-specific:
- New tools take an optional _as_workspace param to disambiguate when
  watching multiple workspaces. Underscore-prefixed so it can't collide
  with the universal contract (which is bound to a single workspace).
2026-04-30 14:57:46 -07:00
Hongming Wang
58beccc06c
Merge pull request #18 from Molecule-AI/fix/v0.2.2-probe-order-and-pid-cleanup
fix: probe ordering + PID file cleanup on exit(2) (v0.2.2)
2026-04-30 00:06:21 -07:00
Hongming Wang
fef6a6210e fix: probe ordering + PID file cleanup on exit(2) (v0.2.2)
Independent five-axis review of v0.2.1 surfaced three issues. All
three only manifest after the upgrade-or-die path actually fires,
which is rare in normal operation but exactly the failure mode the
probe was added for — so quiet correctness matters.

1. **CRITICAL: process.exit(2) orphans PID file → cross-process-kill
   hazard.** v0.2.1 added a fatal exit on too_old probe response, but
   left the PID file pointing at this (about-to-die) process. Next
   launch reads PID_FILE, calls `process.kill(stale, 'SIGTERM')` —
   the dead PID is now likely owned by an unrelated process, so the
   plugin SIGTERMs whatever happens to be there. This is exactly the
   bug the singleton lock dance was designed to prevent. Fix: install
   a `process.on('exit')` listener that unlinks PID_FILE iff this
   process still owns it (handles all exit paths: clean shutdown,
   probe failure, unhandledException, etc.). Listener uses sync I/O
   (only path 'exit' permits) and is no-op-safe if another process
   has already taken ownership of the file.

2. **Probe ran AFTER mcp.connect(transport).** Claude Code had
   already finished the MCP initialize handshake when the probe
   detected too_old and exited; from Claude's perspective this looked
   like "MCP server crashed mid-session" — the carefully-written
   stderr explanation isn't surfaced. Probe must run BEFORE
   mcp.connect so the failure mode is "server failed to start" with
   stderr visible to the user.

3. **Probe ran AFTER registerAsPoll().** If the platform predates
   delivery_mode=poll AND since_id (consistent — both shipped under
   #2339), registerAsPoll() may have already mutated the workspace's
   delivery_mode on the platform. Then the probe fails and we exit,
   leaving the workspace in a broken half-configured state. Probe
   must run BEFORE registerAsPoll for the same reason it must run
   before mcp.connect — fail without side effects.

Reordered boot sequence:
  loadCursors → probeCursorSupport (parallel via allSettled) →
  mcp.connect → registerAsPoll → poll loop.

Also parallelized the probe with Promise.allSettled — sequentially
it was up to N × 15s on a hung platform, which adds up for
multi-workspace channels.

README updated to document that 401/403/404/5xx from the probe are
treated as inconclusive (orthogonal to cursor support — usually
auth or transient) and the plugin continues; this avoids users
debugging a phantom probe failure when the real issue is their
token.

Bumps to 0.2.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:05:47 -07:00
Hongming Wang
e7a2dce756
Merge pull request #17 from Molecule-AI/fix/v0.2.1-probe-cursor-support
fix: probe cursor support at startup, fail loudly on pre-#2354 platform (v0.2.1)
2026-04-29 23:36:42 -07:00
Hongming Wang
8da0c47955 feat: probe cursor support at startup (v0.2.1)
Plugin v0.2 polls /activity?since_id=<cursor>. Older platforms (pre
molecule-core#2354) silently ignore the param and return whatever the
default time window covers — every poll then re-delivers the same
activities to Claude as fresh turns. That's a worse failure mode than
any v0.1 bug.

Add a startup probe: send since_id=00000000-…-000000000000 (the all-zero
UUID, which gen_random_uuid() can never produce per RFC 4122 §4.4 so a
valid 410 is unambiguous). #2354+ answers 410 Gone for any unknown
cursor; pre-#2354 answers 200 OK. On 200 the plugin exits with code 2
and a clear "upgrade your platform or downgrade the plugin" message.

401/403/404/5xx are inconclusive (orthogonal to cursor support) — log a
warning, let the normal poll loop surface the real failure.

Bumps package.json + the MCP server-name version string to 0.2.1 (the
0.1.0 in the Server constructor was already stale, fixed at the same
time). README's compat section documents the probe + exit code so users
debugging an immediate exit-code-2 know what's going on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:34:22 -07:00
Hongming Wang
d862aa6c2c
Merge pull request #16 from Molecule-AI/feat/v0.2-cursor-poll-mode
feat: v0.2 — cursor-based polling + auto-register as poll-mode
2026-04-29 22:35:58 -07:00
Hongming Wang
18657ebf59 feat: v0.2 — cursor-based polling + auto-register as poll-mode
Switches from the v0.1 since_secs+seenIds time-window scheme to a
Telegram-shaped since_id cursor (Molecule-AI/molecule-core#2354), and
adds a startup self-register that sets the workspace's delivery_mode
to poll (Molecule-AI/molecule-core#2353) so the platform's a2a_proxy
short-circuits inbound A2A into activity_logs without requiring a
public URL.

What changed:
- Cursor persistence: ~/.claude/channels/molecule/cursor.json, atomic
  temp+rename writes, chmod 600. Survives restarts; no replay window,
  no in-memory dedup growth.
- pollWorkspace: first run seeds cursor from most-recent without
  delivering (events that predate the session are out of context).
  Steady state uses ?since_id=<cursor>; ASC-ordered rows strictly
  after cursor get delivered in recorded order. 410 → drop cursor +
  re-seed next tick.
- registerAsPoll: POST /registry/register {delivery_mode:"poll",
  agent_card:{name,description}} for each watched workspace on boot.
  Idempotent (upsert). Skipped when MOLECULE_AUTO_REGISTER_POLL=false
  for users who configured the workspace another way.
- New env vars: MOLECULE_AGENT_NAME, MOLECULE_AGENT_DESC,
  MOLECULE_AUTO_REGISTER_POLL.
- README: cursor section + compat bumped to require molecule-core
  PRs #2348/#2353/#2354 (issue #2339).

Compat: requires platform to ship #2339. Plugin works against older
platforms only if you set MOLECULE_AUTO_REGISTER_POLL=false and the
workspace already has delivery_mode=poll set another way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:34:42 -07:00
Hongming Wang
c7b9e5500c
Merge pull request #15 from Molecule-AI/fix/origin-header-for-saas-waf
fix: send Origin header so SaaS edge WAF accepts /workspaces/* fetches
2026-04-29 21:08:57 -07:00
Hongming Wang
2f08478edb fix: send Origin header so SaaS edge WAF accepts /workspaces/* fetches
Production tenants front the workspace-server with a Cloudflare WAF that
enforces same-origin on /workspaces/* paths. Without an Origin header the
WAF silently re-routes the request to the canvas Next.js (which has no
/workspaces page), so polls returned empty 404s and replies failed with
an opaque error.

Browsers set Origin automatically for cross-origin POSTs; Node/Bun fetch
does not (it's a browser-only concern). Both fetch sites in this plugin
hit /workspaces/* with a workspace bearer that's only valid against
PLATFORM_URL anyway, so we set Origin: PLATFORM_URL explicitly — no risk
of leaking the bearer to a different origin.

Verified against hongmingwang.moleculesai.app:
  - Pre-fix: GET /workspaces/:id/activity → empty 404 (WAF re-route)
  - Post-fix: GET /workspaces/:id/activity → 200 [] (correct)

The reply path gets the same fix; e2e verification of replies is blocked
upstream (the platform's outbound forward to a registered URL needs the
agent reachable from the platform's network), unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:08:04 -07:00
Hongming Wang
f3402e48ff
Merge pull request #1 from Molecule-AI/fix/v0.1-jsonrpc-envelope-handling
fix: walk JSON-RPC envelope in extractText + send proper envelope on reply
2026-04-29 12:23:21 -07:00
Hongming Wang
8684a47989 fix: walk JSON-RPC envelope in extractText + send proper envelope on reply
Two-workspace E2E test (2026-04-29) against a real molecule-core platform
caught two mirror-image bugs in v0.1's A2A wire-shape handling:

1. **extractText looked at the wrong path.** The plugin assumed
   `request_body.parts[].text` but real activity_log rows from the
   platform's a2a_proxy use the proper JSON-RPC shape:
   `request_body.params.message.parts[].text`. With shorthand bodies the
   platform was stripping params, so the plugin was falling back to
   `summary` (auto-generated " → workspace-name" string) instead of the
   actual peer message.

2. **reply_to_workspace was sending shorthand `{parts:[...]}`.** The
   platform accepted the request but the a2a_proxy stripped params before
   forwarding to the peer's URL — peer received an envelope with
   `params:null`, no message text. Wrapping in proper JSON-RPC 2.0
   (`{jsonrpc, id, method:"message/send", params:{message:{messageId, parts}}}`)
   preserves the message all the way through.

Verified by E2E: real A2A from workspace B → A surfaces correctly in the
notification; reply from A via the plugin lands at B with intact text.

The extractText helper now walks 3 shapes in priority order so backward-
compat with shorthand-sending callers (canvas-direct-sends) is preserved.

Known limitation flagged but NOT fixed here: a2a_proxy doesn't populate
activity_log.source_id from the X-Source-Workspace-Id request header, so
notifications surface with `peer_id: ""`. That's a platform-side fix
(workspace-server/internal/handlers/a2a_proxy_helpers.go logA2ASuccess);
will file separately in molecule-core.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:11:12 -07:00
Hongming Wang
d07363cbe5 feat: initial scaffold — Molecule channel plugin for Claude Code
Bridges Molecule A2A traffic into a Claude Code session via MCP. Inbound
A2A messages from watched workspaces surface as conversation turns
(notifications/claude/channel); replies route back through the existing
POST /workspaces/:id/a2a endpoint via the reply_to_workspace MCP tool.

Architecture:
- Polling-based inbound (uses /activity?since_secs= shipped in molecule-core
  PR #2300). Works through every NAT/firewall, no tunnel required —
  optimized for laptop-launched Claude Code sessions vs the existing
  push-based external-agent flow that needs ngrok.
- Per-workspace bearer auth (MOLECULE_WORKSPACE_TOKENS, comma-separated to
  match MOLECULE_WORKSPACE_IDS). Same token covers /activity (read) and
  /a2a (write).
- Singleton lock at ~/.claude/channels/molecule/bot.pid prevents two
  channel servers racing the dedup state.
- Dedup by activity.id; 30s overlap window over a 5s poll interval
  protects against missed ticks (laptop sleep, transient network blips).

v0.1 ships:
- .claude-plugin/plugin.json, .mcp.json, package.json, LICENSE (Apache-2.0)
- server.ts: MCP server with notification emission + reply_to_workspace tool
- README: install + .env config + architecture notes + v0.2 roadmap

v0.1 explicit non-goals (tracked in README):
- No push-mode inbound (requires tunnel; deferred to v0.2)
- No pairing flow (manual .env tokens; canvas pairing in v0.2)
- No file-attachment download (URLs surface in meta; host fetches on-demand)
- No outbound channel-init (only replies; start_workspace_chat in v0.2)

Mirrors the architecture of @claude-plugins-official/telegram v0.0.6
(MCP notification contract: notifications/claude/channel with
{content, meta}) so the host's existing channel-handling logic works
without custom adapters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 11:30:23 -07:00