fix: allow office mcp mode for existing tasks (#1315)
This commit is contained in:
@@ -53,7 +53,7 @@ type CreateInstanceRequest struct {
|
||||
DisableAskQuestion bool `json:"disable_ask_question,omitempty"` // Disable ask_user_question MCP tool (TUI agents)
|
||||
AssumeMcpSse bool `json:"assume_mcp_sse,omitempty"` // Assume agent supports SSE MCP servers
|
||||
AssumeMcpHttp bool `json:"assume_mcp_http,omitempty"` // Assume agent supports HTTP MCP servers
|
||||
McpMode string `json:"mcp_mode,omitempty"` // MCP tool mode: "task" (default) or "config"
|
||||
McpMode string `json:"mcp_mode,omitempty"` // MCP tool mode: "task" (default), "config", or "office"
|
||||
// RequiresProcessKill forces agentctl to kill the agent's process group
|
||||
// (not just close stdin) on shutdown. Required for agents whose runtime
|
||||
// keeps child processes (e.g. MCP servers) alive when stdin closes —
|
||||
|
||||
@@ -263,7 +263,7 @@ type ExecutorCreateRequest struct {
|
||||
McpServers []McpServerConfig
|
||||
AgentConfig agents.Agent // Agent type info needed by runtimes
|
||||
PreviousExecutionID string // Non-empty when reconnecting to a previous execution
|
||||
McpMode string // MCP tool mode: "task" (default) or "config"
|
||||
McpMode string // MCP tool mode: "task" (default), "config", or "office"
|
||||
AuthToken string // Previously handshaken agentctl token for reconnects
|
||||
BootstrapNonce string // Stored nonce for re-handshake after container restart
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ type LaunchRequest struct {
|
||||
ExecutorType string // Executor type (e.g., "local", "worktree", "local_docker") - determines runtime
|
||||
ExecutorConfig map[string]string // Executor config (docker_host, git_token, etc.)
|
||||
PreviousExecutionID string // Previous execution ID for runtime reconnect
|
||||
McpMode string // MCP tool mode: "config" activates config-mode tools
|
||||
McpMode string // MCP tool mode: "task" (default), "config", or "office"
|
||||
|
||||
// Environment preparation
|
||||
SetupScript string // Setup script to run before agent starts
|
||||
|
||||
@@ -76,7 +76,7 @@ type LaunchSpec struct {
|
||||
// PriorACPSession is the ACP session id to resume, if any.
|
||||
PriorACPSession string
|
||||
|
||||
// McpMode selects the MCP tool surface (e.g. "config" for config-mode tools).
|
||||
// McpMode selects the MCP tool surface ("task", "config", or "office").
|
||||
McpMode string
|
||||
|
||||
// Metadata carries integration-specific fields. Phase 1 supports
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/kandev/kandev/internal/agentctl/server/config"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/process"
|
||||
mcpserver "github.com/kandev/kandev/internal/mcp/server"
|
||||
)
|
||||
|
||||
func newTestServerWithMCP(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
log := newTestLogger()
|
||||
cfg := &config.InstanceConfig{
|
||||
Port: 0,
|
||||
WorkDir: "/tmp/test",
|
||||
}
|
||||
procMgr := process.NewManager(cfg, log)
|
||||
backend := mcpserver.NewChannelBackendClient(log)
|
||||
t.Cleanup(backend.Close)
|
||||
mcpServer := mcpserver.New(backend, "test-session", "test-task", 0, log, "", false, mcpserver.ModeTask)
|
||||
return NewServer(cfg, procMgr, mcpServer, nil, log)
|
||||
}
|
||||
|
||||
func setMcpMode(t *testing.T, s *Server, mode string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
body, err := json.Marshal(map[string]string{"mode": mode})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal request: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/mcp/mode", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func TestHandleSetMcpMode_AcceptsSupportedModes(t *testing.T) {
|
||||
s := newTestServerWithMCP(t)
|
||||
|
||||
for _, mode := range []string{mcpserver.ModeTask, mcpserver.ModeConfig, mcpserver.ModeOffice} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
rec := setMcpMode(t, s, mode)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if body.Mode != mode {
|
||||
t.Fatalf("mode = %q, want %q", body.Mode, mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSetMcpMode_RejectsUnsupportedMode(t *testing.T) {
|
||||
s := newTestServerWithMCP(t)
|
||||
|
||||
rec := setMcpMode(t, s, mcpserver.ModeExternal)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -299,8 +299,8 @@ func (s *Server) handleSetMcpMode(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Mode != "task" && req.Mode != "config" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mode: must be 'task' or 'config'"})
|
||||
if req.Mode != mcp.ModeTask && req.Mode != mcp.ModeConfig && req.Mode != mcp.ModeOffice {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mode: must be 'task', 'config', or 'office'"})
|
||||
return
|
||||
}
|
||||
s.mcpServer.SetMode(req.Mode)
|
||||
|
||||
@@ -209,7 +209,7 @@ type InstanceConfig struct {
|
||||
AssumeMcpHttp bool
|
||||
|
||||
// McpMode controls which MCP tools are registered for this instance.
|
||||
// "task" (default) registers kanban/plan tools; "config" registers config tools.
|
||||
// "task" (default), "config", and "office" select distinct tool surfaces.
|
||||
McpMode string
|
||||
|
||||
// AuthToken is a shared secret for authenticating requests.
|
||||
|
||||
@@ -148,7 +148,7 @@ type CreateRequest struct {
|
||||
// AssumeMcpHttp overrides MCP capability filtering to assume HTTP support.
|
||||
AssumeMcpHttp bool `json:"assume_mcp_http,omitempty"`
|
||||
|
||||
// McpMode controls which MCP tools are registered: "task" (default) or "config".
|
||||
// McpMode controls which MCP tools are registered: "task" (default), "config", or "office".
|
||||
McpMode string `json:"mcp_mode,omitempty"`
|
||||
|
||||
// RequiresProcessKill forces the agent's process group to be killed on
|
||||
|
||||
@@ -400,6 +400,9 @@ func (h *Handlers) handleCreateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
if req.Title == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "title is required", nil)
|
||||
}
|
||||
if req.AssigneeAgentProfileID != "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "assignee_agent_profile_id is office-only and cannot be set via create_task_kandev", nil)
|
||||
}
|
||||
|
||||
// Default start_agent to true for backward compatibility
|
||||
startAgent := req.StartAgent == nil || *req.StartAgent
|
||||
|
||||
@@ -142,6 +142,30 @@ func TestHandleCreateTask_MissingTitle(t *testing.T) {
|
||||
assertWSError(t, resp, ws.ErrorCodeValidation)
|
||||
}
|
||||
|
||||
func TestHandleCreateTask_RejectsAssigneeAgentProfileID(t *testing.T) {
|
||||
svc, _ := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
workspaces, err := svc.ListWorkspaces(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
workflows, err := svc.ListWorkflows(ctx, workspaces[0].ID, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workflows, 1)
|
||||
|
||||
h := &Handlers{taskSvc: svc, logger: testLogger(t).WithFields()}
|
||||
msg := makeWSMessage(t, ws.ActionMCPCreateTask, map[string]interface{}{
|
||||
"workspace_id": workspaces[0].ID,
|
||||
"workflow_id": workflows[0].ID,
|
||||
"title": "Office child from kanban",
|
||||
"assignee_agent_profile_id": "agent-inst-42",
|
||||
"start_agent": false,
|
||||
})
|
||||
|
||||
resp, err := h.handleCreateTask(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
assertWSError(t, resp, ws.ErrorCodeValidation)
|
||||
}
|
||||
|
||||
func TestSessionStateEventsIncludeUpdatedAt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -839,16 +863,15 @@ func TestHandleCreateTask_AutoResolveFailsWithMultipleWorkflows(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleCreateTask_NewFields_Unmarshalled(t *testing.T) {
|
||||
// Verify that execution_policy and assignee_agent_profile_id are accepted
|
||||
// in the payload without triggering a parse error. The handler will fail
|
||||
// at the workspace_id validation stage, not at unmarshal.
|
||||
// Verify that extra fields are tolerated by JSON decoding. Office-only
|
||||
// assignee_agent_profile_id is covered by
|
||||
// TestHandleCreateTask_RejectsAssigneeAgentProfileID.
|
||||
h := &Handlers{}
|
||||
msg := makeWSMessage(t, ws.ActionMCPCreateTask, map[string]interface{}{
|
||||
"title": "My task",
|
||||
"workspace_id": "ws-1",
|
||||
"workflow_id": "wf-1",
|
||||
"execution_policy": `{"stages":[]}`,
|
||||
"assignee_agent_profile_id": "agent-inst-42",
|
||||
"title": "My task",
|
||||
"workspace_id": "ws-1",
|
||||
"workflow_id": "wf-1",
|
||||
"execution_policy": `{"stages":[]}`,
|
||||
})
|
||||
|
||||
// taskSvc is nil so CreateTask will panic before we reach it; the payload
|
||||
@@ -859,9 +882,8 @@ func TestHandleCreateTask_NewFields_Unmarshalled(t *testing.T) {
|
||||
// a payload that fails a post-unmarshal check (missing workspace) which
|
||||
// exercised the request struct fields.
|
||||
msgMissingWs := makeWSMessage(t, ws.ActionMCPCreateTask, map[string]interface{}{
|
||||
"title": "My task",
|
||||
"execution_policy": `{"stages":[]}`,
|
||||
"assignee_agent_profile_id": "agent-inst-42",
|
||||
"title": "My task",
|
||||
"execution_policy": `{"stages":[]}`,
|
||||
})
|
||||
|
||||
resp, err := h.handleCreateTask(context.Background(), msgMissingWs)
|
||||
|
||||
@@ -269,7 +269,7 @@ type LaunchAgentRequest struct {
|
||||
ExecutorType string // Executor type (e.g., "local", "worktree", "local_docker") - determines runtime
|
||||
ExecutorConfig map[string]string // Executor config (docker_host, git_token, etc.)
|
||||
PreviousExecutionID string // Previous execution ID for runtime reconnect
|
||||
McpMode string // MCP tool mode: "config" activates config-mode tools
|
||||
McpMode string // MCP tool mode: "task" (default), "config", or "office"
|
||||
IsEphemeral bool // Ephemeral task (quick chat) — enables fallback workspace creation
|
||||
WorkspacePath string // Optional host folder for repo-less tasks (overrides scratch fallback)
|
||||
|
||||
@@ -361,7 +361,7 @@ type LaunchOptions struct {
|
||||
Prompt string
|
||||
WorkflowStepID string
|
||||
StartAgent bool
|
||||
McpMode string // MCP tool mode: McpModeConfig activates config-mode tools
|
||||
McpMode string // MCP tool mode: empty task default, McpModeConfig, or McpModeOffice
|
||||
Attachments []v1.MessageAttachment
|
||||
Env map[string]string
|
||||
// RouteOverride carries a provider-routing override resolved by the
|
||||
|
||||
Reference in New Issue
Block a user