Files
multica/server/cmd/multica/main.go
Bohan Jiang 50a48cef1e feat(slack): unified multica chat history pull for channel backfill (MUL-3871) (#4747)
* 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>
2026-06-30 16:48:13 +08:00

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))
}
}