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:
Jiayuan Zhang
2026-05-06 08:15:28 +08:00
parent 9dd90eef22
commit c45cc052e8
6 changed files with 261 additions and 4 deletions

View File

@@ -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"`