mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-13 03:15:34 +02:00
* feat(slack): add unified `multica chat history` pull for channel backfill (MUL-3871) Agents @mentioned in a Slack thread/channel only saw the triggering message, never the prior conversation (GitHub #4717). Instead of force-assembling a recent-context block on every inbound (the Feishu approach), expose a single channel-agnostic pull command the agent runs on demand. - channel: normalized HistoryMessage/HistoryPage/HistoryOptions vocab so the agent sees one shape regardless of platform. - slack.History: resolves session -> binding -> installation -> bot token and reads conversations.replies (real thread) or conversations.history (DM / top-level channel, capturing sibling messages). thread_ts is recorded on the binding config at session creation to pick the right call. - handler GET /api/chat/history: authorized purely by the task-scoped token (stamped X-Task-ID -> the task's own chat session), so an agent can only read the conversation it is currently running for. - multica chat history CLI command (no args; same for every channel). - buildChatPrompt nudge so the agent discovers the command. Feishu is intentionally untouched. Adding a platform = implement the reader. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): require task-token actor source on chat history endpoint Niko's review caught a privilege-boundary hole: the endpoint trusted X-Task-ID, but it is mounted under the general Auth group where a normal JWT / mul_ PAT request does NOT strip a client-forged X-Task-ID — only the mat_ task-token branch stamps it. A workspace member who knew a chat task id could forge the header and read that task's Slack channel/DM/thread history. Gate on the server-set X-Actor-Source == "task_token" (the Auth middleware deletes any client-supplied value and re-stamps it only on the mat_ branch), then trust X-Task-ID. Adds a regression test: a forged X-Task-ID without the task-token actor source is rejected with 403 and never reaches the reader. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): thread-first history for follow-ups, channel for first turn (MUL-3871) A Slack conversation has two nested histories: the surrounding channel and the agent's own thread (the bot's first reply opens a thread on the @mention). The first version picked replies-vs-history from a thread_ts fixed at session creation, so a session started by a top-level @mention always read CHANNEL history — even on follow-ups inside the bot's thread, which should read THREAD history first. - Add a HistoryScope (auto|thread|channel). The handler resolves auto: first turn (no prior bot reply) -> channel; follow-up -> thread. The agent can override with --scope channel|thread, and the response reports the scope read. - The thread root is derived from the binding (last_thread_id / composite-key suffix), available for every engaged group session, instead of the creation-time thread_ts (now removed from the binding config). - A DM degrades a thread request to channel history (DMs have no threads). - Prompt guidance + CLI help updated to explain the policy. Tests: scope selection (thread/channel/DM-fallback/no-root), root derivation, and handler auto-resolution (first->channel, follow-up->thread, explicit override). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/multica-ai/multica/server/internal/cli"
|
|
)
|
|
|
|
var (
|
|
version = "dev"
|
|
commit = "unknown"
|
|
date = "unknown"
|
|
)
|
|
|
|
// debugFlag is bound to the persistent --debug flag and, when set, makes
|
|
// FormatError emit the full original error chain instead of just the
|
|
// user-facing message.
|
|
var debugFlag bool
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "multica",
|
|
Short: "Multica CLI — local agent runtime and management tool",
|
|
Long: "Work seamlessly with Multica from the command line.",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.Version = fmt.Sprintf("%s (commit: %s, built: %s)\ngo: %s, os/arch: %s/%s", version, commit, date, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
|
rootCmd.SetVersionTemplate("multica {{.Version}}\n")
|
|
|
|
// Tag every CLI HTTP request with this binary's build version so the
|
|
// server can split logs/metrics by client version.
|
|
cli.ClientVersion = version
|
|
|
|
rootCmd.PersistentFlags().String("server-url", "", "Multica server URL (env: MULTICA_SERVER_URL)")
|
|
rootCmd.PersistentFlags().String("workspace-id", "", "Workspace ID (env: MULTICA_WORKSPACE_ID)")
|
|
rootCmd.PersistentFlags().String("profile", "", "Configuration profile name (e.g. dev) — isolates config, daemon state, and workspaces")
|
|
rootCmd.PersistentFlags().BoolVar(&debugFlag, "debug", false, "Print full error details on failure (env: MULTICA_DEBUG)")
|
|
|
|
// Core commands
|
|
issueCmd.GroupID = groupCore
|
|
projectCmd.GroupID = groupCore
|
|
labelCmd.GroupID = groupCore
|
|
agentCmd.GroupID = groupCore
|
|
autopilotCmd.GroupID = groupCore
|
|
workspaceCmd.GroupID = groupCore
|
|
repoCmd.GroupID = groupCore
|
|
skillCmd.GroupID = groupCore
|
|
squadCmd.GroupID = groupCore
|
|
chatCmd.GroupID = groupCore
|
|
|
|
// Runtime commands
|
|
daemonCmd.GroupID = groupRuntime
|
|
runtimeCmd.GroupID = groupRuntime
|
|
|
|
// Additional commands
|
|
authCmd.GroupID = groupAdditional
|
|
userCmd.GroupID = groupAdditional
|
|
loginCmd.GroupID = groupAdditional
|
|
setupCmd.GroupID = groupAdditional
|
|
attachmentCmd.GroupID = groupAdditional
|
|
configCmd.GroupID = groupAdditional
|
|
updateCmd.GroupID = groupAdditional
|
|
versionCmd.GroupID = groupAdditional
|
|
|
|
rootCmd.AddCommand(issueCmd)
|
|
rootCmd.AddCommand(projectCmd)
|
|
rootCmd.AddCommand(labelCmd)
|
|
rootCmd.AddCommand(agentCmd)
|
|
rootCmd.AddCommand(autopilotCmd)
|
|
rootCmd.AddCommand(workspaceCmd)
|
|
rootCmd.AddCommand(repoCmd)
|
|
rootCmd.AddCommand(skillCmd)
|
|
rootCmd.AddCommand(squadCmd)
|
|
rootCmd.AddCommand(chatCmd)
|
|
rootCmd.AddCommand(daemonCmd)
|
|
rootCmd.AddCommand(runtimeCmd)
|
|
rootCmd.AddCommand(authCmd)
|
|
rootCmd.AddCommand(userCmd)
|
|
rootCmd.AddCommand(loginCmd)
|
|
rootCmd.AddCommand(setupCmd)
|
|
rootCmd.AddCommand(attachmentCmd)
|
|
rootCmd.AddCommand(configCmd)
|
|
rootCmd.AddCommand(updateCmd)
|
|
rootCmd.AddCommand(versionCmd)
|
|
|
|
initHelp(rootCmd)
|
|
}
|
|
|
|
func main() {
|
|
cli.CleanupStaleUpdateArtifacts()
|
|
if err := rootCmd.Execute(); err != nil {
|
|
if err != errSilent {
|
|
fmt.Fprintln(os.Stderr, cli.FormatError(err, debugFlag))
|
|
}
|
|
os.Exit(cli.ExitCodeFor(err))
|
|
}
|
|
}
|