mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
refactor(quick-create): skip daemon CLI version gate in dev
Restores the gate (reverts the full-removal commit) and bypasses it in non-production environments instead. The motivation for the original removal — local source-built daemons report a `git describe` version like v0.2.15-N-gHASH that parses below 0.2.20 and blocks dev testing — is now handled by checking APP_ENV on the server and NODE_ENV on the client. Production keeps the original "needs upgrade" UX. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/multica-ai/multica/server/internal/logger"
|
||||
"github.com/multica-ai/multica/server/internal/util"
|
||||
"github.com/multica-ai/multica/server/pkg/agent"
|
||||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||||
"github.com/multica-ai/multica/server/pkg/protocol"
|
||||
)
|
||||
@@ -929,6 +931,22 @@ func (h *Handler) QuickCreateIssue(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Daemon CLI version gate. The agent-side prompt + create-flow rely on
|
||||
// behaviors introduced in MinQuickCreateCLIVersion (URL attachment
|
||||
// handling, no-retry on partial failure). Older daemons either
|
||||
// double-create issues on partial CLI failures or mishandle pasted
|
||||
// screenshot URLs; fail closed before enqueuing rather than surface
|
||||
// the breakage as an inbox failure twenty seconds later. Skipped in
|
||||
// non-production environments so local dev daemons (which report a
|
||||
// `git describe` version like v0.2.15-N-gHASH that parses below the
|
||||
// threshold) don't get blocked during testing.
|
||||
if isProductionEnv() {
|
||||
if status, payload := h.checkQuickCreateDaemonVersion(r.Context(), agent.RuntimeID); status != 0 {
|
||||
writeJSON(w, status, payload)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
task, err := h.TaskService.EnqueueQuickCreateTask(r.Context(), wsUUID, requesterUUID, agentUUID, prompt)
|
||||
if err != nil {
|
||||
slog.Warn("quick-create enqueue failed", append(logger.RequestAttrs(r), "error", err)...)
|
||||
@@ -962,6 +980,71 @@ func (h *Handler) isRuntimeOnline(ctx context.Context, runtimeID pgtype.UUID) bo
|
||||
return rt.Status == "online"
|
||||
}
|
||||
|
||||
// checkQuickCreateDaemonVersion enforces MinQuickCreateCLIVersion against the
|
||||
// CLI version the daemon reported at registration time (stored on the runtime
|
||||
// row's metadata.cli_version). Returns (0, nil) when the version is
|
||||
// acceptable, otherwise (status, payload) ready to hand to writeJSON.
|
||||
//
|
||||
// Failure shape is stable so the modal can branch on the `code` field and
|
||||
// surface a "needs upgrade" hint that points at the specific runtime:
|
||||
//
|
||||
// 422 {
|
||||
// "code": "daemon_version_unsupported",
|
||||
// "current_version": "0.2.18" | "",
|
||||
// "min_version": "0.2.20",
|
||||
// "runtime_id": "<uuid>"
|
||||
// }
|
||||
func (h *Handler) checkQuickCreateDaemonVersion(ctx context.Context, runtimeID pgtype.UUID) (int, map[string]any) {
|
||||
rt, err := h.Queries.GetAgentRuntime(ctx, runtimeID)
|
||||
if err != nil {
|
||||
// Runtime row vanished between the online check and here — treat
|
||||
// as unavailable rather than wedging the request on a 500.
|
||||
return http.StatusUnprocessableEntity, map[string]any{
|
||||
"code": "agent_unavailable",
|
||||
"reason": "agent's runtime is no longer registered",
|
||||
}
|
||||
}
|
||||
current := readRuntimeCLIVersion(rt.Metadata)
|
||||
switch err := agent.CheckMinCLIVersion(current); {
|
||||
case err == nil:
|
||||
return 0, nil
|
||||
case errors.Is(err, agent.ErrCLIVersionMissing), errors.Is(err, agent.ErrCLIVersionTooOld):
|
||||
return http.StatusUnprocessableEntity, map[string]any{
|
||||
"code": "daemon_version_unsupported",
|
||||
"current_version": current,
|
||||
"min_version": agent.MinQuickCreateCLIVersion,
|
||||
"runtime_id": uuidToString(runtimeID),
|
||||
}
|
||||
default:
|
||||
// Defensive fall-through: unknown error from the version check is
|
||||
// also fail-closed, since the gate exists precisely because we
|
||||
// can't trust older daemons with this flow.
|
||||
return http.StatusUnprocessableEntity, map[string]any{
|
||||
"code": "daemon_version_unsupported",
|
||||
"current_version": current,
|
||||
"min_version": agent.MinQuickCreateCLIVersion,
|
||||
"runtime_id": uuidToString(runtimeID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readRuntimeCLIVersion pulls metadata.cli_version off a runtime row. The
|
||||
// metadata column is JSONB on the wire; the daemon stores the multica CLI
|
||||
// version under that key during registration (see DaemonRegister).
|
||||
func readRuntimeCLIVersion(metadata []byte) string {
|
||||
if len(metadata) == 0 {
|
||||
return ""
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(metadata, &m); err != nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := m["cli_version"].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateIssueRequest struct {
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
|
||||
Reference in New Issue
Block a user