multica-ai/multica · error

invalid token

Error message

invalid token

What it means

HTTP 401 from the mat_ (agent task token) branch of the auth middleware when the token prefix matches agent task tokens but the middleware was constructed with a nil database queries object, so the token hash cannot be looked up at all. This is a server-side wiring defect, not a bad token: the handler simply has no way to validate task tokens.

Source

Thrown at server/internal/middleware/auth.go:75

			if fromCookie && !auth.ValidateCSRF(r) {
				slog.Debug("auth: CSRF validation failed", "path", r.URL.Path)
				http.Error(w, `{"error":"CSRF validation failed"}`, http.StatusForbidden)
				return
			}

			// Agent task token: "mat_" prefix. Minted by the server at
			// task-claim time and injected by the daemon into the agent
			// process. Authoritative for actor identity — the bound
			// (user_id, agent_id, task_id, workspace_id) triple is
			// written into request headers here, OVERRIDING whatever the
			// client sent, so a downstream actor-resolver cannot be
			// tricked by a client that strips or forges X-Agent-ID /
			// X-Task-ID. Human-only endpoints (e.g. agent env
			// management) reject requests authenticated this way; see
			// `actorSourceFromRequest`. MUL-2600.
			if strings.HasPrefix(tokenString, "mat_") {
				if queries == nil {
					http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
					return
				}
				hash := auth.HashToken(tokenString)
				tt, err := queries.GetTaskTokenByHash(r.Context(), hash)
				if err != nil {
					slog.Warn("auth: invalid task token", "path", r.URL.Path, "error", err)
					http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
					return
				}
				r.Header.Set("X-User-ID", uuidToString(tt.UserID))
				r.Header.Set("X-Agent-ID", uuidToString(tt.AgentID))
				r.Header.Set("X-Task-ID", uuidToString(tt.TaskID))
				r.Header.Set("X-Workspace-ID", uuidToString(tt.WorkspaceID))
				// X-Actor-Source flags the auth path so resolveActor and
				// any owner-only handler can deny without re-querying the
				// token table. The value "task_token" is the only signal
				// this header is allowed to carry — strip anything else a
				// client tried to send.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Fix the server wiring: construct the auth middleware with a non-nil queries object wherever mat_ tokens must be accepted.
  2. If you run the server, check startup logs for database connection failure — queries is nil when DB init failed.
  3. If you are the client, retry against the main API endpoint, not a DB-less auxiliary listener.

Example fix

// before: middleware without DB queries
r.Use(middleware.Auth(nil, nil))

// after: provide queries
r.Use(middleware.Auth(queries, cfg))
Defensive patterns

Strategy: validation

Try / catch

resp, err := client.Do(req)
if err == nil && resp.StatusCode == 401 && isTaskTokenEnv {
    // nil-queries is a server wiring bug: report to operator; re-claim may mint a fresh token anyway
}

Prevention

When it happens

Trigger: A request with an Authorization: Bearer mat_... token reaches an auth middleware instance built without database queries (e.g. a misconfigured router, a test server, or an embedded deployment that skipped DB wiring).

Common situations: Integration tests constructing the middleware without a DB; a partially initialized server during startup races; deployments that intentionally run DB-less (metrics/health) but accidentally route API traffic through them.

Understand the failure class

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/d4c3924f71492d31. Report an issue: GitHub.