mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
main
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 review nits)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|
||
|
|
5d79696fb5 | MUL-3794: rewrite comment routing cascade | ||
|
|
906f70a3e2 |
Add comment trigger preview suppression (#3792)
* Add comment trigger preview suppression Co-authored-by: multica-agent <github@multica.ai> * Use TanStack Query for trigger preview Co-authored-by: multica-agent <github@multica.ai> * Test note comments skip create triggers Co-authored-by: multica-agent <github@multica.ai> * feat(issues): redesign comment trigger chips as avatar chips Single agent renders as avatar + presence dot + full sentence; several agents collapse to an overlapping stack + active count, mirroring the header working chip. Per-agent skip moves into a click-opened popover (hover layers stay read-only tooltips); suppression reads as brightness, not a ban glyph. Loading and preview errors render nothing. Also: share one tooltip body across chip and popover rows, invalidate cached previews after a comment lands (the enqueued task changes the dedup answer), move the preview query key into issueKeys, and drop the now-unconsumed status field from useCommentTriggerPreview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(server): drop comment trigger wrappers kept only for tests enqueueMentionedAgentTasks and shouldEnqueueSquadLeaderOnComment had no production callers after the compute/enqueue split — the comment path goes through computeCommentAgentTriggers. Tests now exercise the compute functions directly via package-local helpers, so the legacy adapters cannot drift from the real path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): sync mentioning/squads source maps with shared trigger computation The squads source map still pointed the comment-trigger contract at the pre-refactor call chain (comment.go:940 -> shouldEnqueueSquadLeaderOnComment), and the mentioning skill referenced the deleted wrapper. Re-anchor both to computeCommentAgentTriggers / computeAssignedSquadLeaderCommentTrigger / computeMentionedAgentCommentTriggers with current line numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dfc159e1aa |
feat: skip agent triggering on /note-prefixed comments (MUL-3115, #3649) (#3885)
* feat(comments): skip agent triggering on /note-prefixed comments A comment whose first token is the reserved /note prefix (case-insensitive) is stored like any other comment but never wakes an agent. The guard sits at the top of triggerTasksForComment, the single chokepoint, so it covers all three trigger paths — assignee, squad leader, and @mentioned agents. Gating only shouldEnqueueOnComment (as originally proposed) would still let "/note @agent ..." through the mention path. Lets members leave human-only tips/notes on agent-assigned issues without burning an agent run. MUL-3115, closes #3649. Co-authored-by: multica-agent <github@multica.ai> * feat(editor): add /note built-in slash command to comment composer Enable the `/` menu in the issue comment and reply composers in a new "command" mode that lists fixed built-in commands instead of the chat skill picker. Currently one command, /note, which marks a comment as a human-only note that won't trigger the assigned agent. Selecting it inserts the plain-text "/note " prefix (not a rich node), so a menu pick and a hand-typed command are byte-identical and the backend detects either with a simple prefix match. The command menu renders nothing on a non-matching `/` (hideOnEmpty) so typing a date like 6/8 isn't noisy. The chat skill picker is unchanged. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> * refactor(editor): match /note by label prefix and localize its description Address PR review feedback: - buildBuiltinCommandItems now matches the command label as a prefix only, dropping the description substring match copied from the skill picker. With one command this keeps the menu predictable (/no surfaces note; /deploy or a description word like /agent shows nothing) and avoids Enter selecting note unexpectedly. - The command description is now a localized UI string: added slash_command.commands.note to all four editor locales (en/ja/ko/zh-Hans) and the menu renders it via the typed translator. The /label itself stays literal since it's the typed token the backend matches. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): shorten /note command description to avoid truncation The slash menu item is single-line (truncate, w-72), so the longer copy was cut off. Shorten to "won't trigger any agents" across all four locales — also more accurate, since /note skips assignee, squad leader, and @mentioned agents, not just the assigned one. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9ccaf18479 |
fix(comment): don't inherit parent @mentions from agent-authored roots (#1833)
* fix(comment): don't inherit parent @mentions when parent author is an agent When an agent posts a comment that @mentions another agent (typically a one-shot delegation, e.g. a PR-completion comment that asks a reviewer agent to review), member follow-up replies in the same thread were auto-inheriting that mention and re-triggering the reviewer on every plain question. Same root cause: the inheritance branch only required the reply to have no mentions, not that the parent was member-authored. Tighten the guard: only inherit when the parent (thread root) is authored by a member. Member-rooted threads still inherit so a member who started by @mentioning an agent can keep replying without re-typing. Agent-authored roots are treated as one-shot — explicit @mentions in later comments still trigger normally. Extracted the decision into shouldInheritParentMentions for direct unit testing, and added an end-to-end regression (TestMemberReplyToAgentRootDoesNotInheritParentMentions) that reproduces MUL-1535: J posts a PR completion @mentioning Reviewer; a member's plain follow-up must not re-enqueue Reviewer. * chore(comment): gofmt trigger_test.go |
||
|
|
bf71802451 |
fix(server): trigger agent on reply in thread where agent already participated (#981)
When a member replies in a member-started thread without @mentioning the assigned agent, the on_comment trigger was suppressed — even if the agent had already replied in that thread. This meant the common flow of "member posts → agent replies → member follows up" would not re-trigger the agent on the follow-up. Add HasAgentRepliedInThread SQL query and check it in isReplyToMemberThread so that agent participation in a thread is treated as an ongoing conversation. |
||
|
|
7c79611309 |
refactor: remove agent triggers config field (#469)
* refactor: remove agent triggers config field Remove the triggers field from agent configuration. The on_assign, on_comment, and on_mention behaviors are now always enabled (hardcoded), as decided in the Agentflow design discussion (MUL-372). Changes: - Database: migration 032 drops triggers column from agent table - Backend: remove triggers from create/update agent APIs and response - Backend: simplify trigger-checking logic to always-enabled - Frontend: remove TriggersTab UI and AgentTrigger types - Tests: remove trigger config unit tests (no longer configurable) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: also remove agent tools config field Remove the tools field from agent configuration alongside triggers. The tools field was a placeholder — stored in the DB and shown in the UI but never passed to the daemon or used at runtime. - Database: migration 032 now also drops tools column - Backend: remove tools from create/update agent APIs and response - Frontend: remove ToolsTab UI, AgentTool type, and tools tab - Update landing page copy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove tools/triggers columns from test fixtures The test fixtures still referenced the dropped tools and triggers columns when inserting agent rows, causing CI failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
beeb8bc107 |
Merge pull request #361 from JimmyPang02/fix/issue-mention-on-comment
fix: issue mentions should not suppress on_comment trigger |
||
|
|
fc6405e4be |
fix(trigger): allow on_comment when thread root @mentions assignee agent (#382)
When a member-started thread root @mentions the assignee agent, replies in that thread should trigger on_comment — the thread is a conversation with the agent, not a member-to-member chat. Previously isReplyToMemberThread only checked the reply content for assignee mentions. Now it also checks the parent (thread root) content. This fixes a gap where path 1 (on_comment) suppressed the trigger and path 2 (on_mention) skipped the assignee, leaving no trigger path. |
||
|
|
6799458807 | fix: issue mentions should not suppress on_comment trigger | ||
|
|
02c9d9f0b0 |
fix(server): @all mentions should not trigger agent execution
@all is a broadcast to all workspace members — it should not trigger the assignee agent's on_comment. Previously @all was treated as "includes everyone" and allowed the trigger. Changes: - commentMentionsOthersButNotAssignee now checks HasMentionAll() early and returns true (suppress) when @all is present - Fix authRequestWithAgent test helper that was making a duplicate HTTP request (one as member, one as agent) Tests: 5 new @all unit test cases, 2 new @all integration test cases. |
||
|
|
4f0c2afcb7 |
test(server): add unit tests for comment trigger logic
Tests cover all on_comment trigger decision scenarios: - commentMentionsOthersButNotAssignee: 6 cases - isReplyToMemberThread: 7 cases - Combined trigger decision: 9 cases matching the full decision table - agentHasTriggerEnabled: 7 cases (nil, empty, explicit, backwards compat) - defaultAgentTriggers: validates all 3 triggers present and enabled |