ruvnet/RuView · error · TypeError

guidance repoRoot must be a string or null

Error message

guidance repoRoot must be a string or null

What it means

refresh_token (auth.py:363) raises this when verify_token succeeds but get_user(sub) returns None or the user record has is_active falsy. The refresh flow re-validates the user on every refresh, so a valid-but-orphaned or deactivated token cannot be exchanged for a new one.

Source

Thrown at harness/homecore/src/guidance.js:69

export function getGuidance(input = {}, options = {}) {
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
    throw new TypeError('guidance input must be an object');
  }
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
    throw new TypeError('guidance options must be an object');
  }
  if (input.topic !== undefined && typeof input.topic !== 'string') {
    throw new TypeError('guidance topic must be a string');
  }
  if (input.query !== undefined && typeof input.query !== 'string') {
    throw new TypeError('guidance query must be a string');
  }
  if (input.limit !== undefined && (typeof input.limit !== 'number' || !Number.isFinite(input.limit))) {
    throw new TypeError('guidance limit must be a finite number');
  }
  if (options.repoRoot !== undefined && options.repoRoot !== null && typeof options.repoRoot !== 'string') {
    throw new TypeError('guidance repoRoot must be a string or null');
  }

  const topic = input.topic === undefined ? 'overview' : input.topic;
  if (!GUIDANCE_TOPICS.includes(topic)) {
    throw new RangeError(`unsupported guidance topic: ${topic}`);
  }
  const query = input.query === undefined ? '' : input.query.trim();
  if (query && (query.length < 2 || query.length > 500)) {
    throw new RangeError('guidance query must contain 2..500 characters');
  }
  const rawLimit = input.limit === undefined ? 20 : input.limit;
  if (rawLimit < 1 || rawLimit > 20) {
    throw new RangeError('guidance limit must be between 1 and 20');
  }
  const limit = Math.floor(rawLimit);
  const wanted = tokenize(query);

  const candidates = CAPABILITIES

View on GitHub (pinned to 4685618388)

Solutions

  1. Fall back to full login (username/password) to obtain a brand-new token
  2. Ensure the user exists and is_active=True in this process before refreshing
  3. Persist the user store so restarts do not orphan live tokens

Example fix

# before
tokens = await auth.refresh_token(old_token)
# after
try:
    tokens = await auth.refresh_token(old_token)
except AuthenticationError:
    tokens = await auth.login(username, password)  # full re-auth
Defensive patterns

Strategy: fallback

Validate before calling

def refresh_will_succeed(token_manager, user_manager, token: str) -> bool:
    """refresh_token re-checks user existence and is_active."""
    claims = token_manager.decode_token_claims(token)
    if claims is None:
        return False
    user = user_manager.get_user(claims.get("sub", ""))
    return user is not None and bool(user.get("is_active", False))

Try / catch

try:
    tokens = await middleware.refresh_token(token)
except AuthenticationError:
    # covers 'User not found or inactive' and the masked generic case
    tokens = await middleware.login(username, password)  # fallback re-auth

Prevention

When it happens

Trigger: Calling refresh_token with a token whose user was created before a server restart (in-memory store now empty); user deactivated between issuance and refresh; sub claim is a username never registered on this instance.

Common situations: Long-running clients attempting silent refresh after a backend restart; deployments where refresh is expected to survive user-store resets; multi-instance setups with per-process user dicts.

Related errors


AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16). Data as JSON: /api/errors/8e828d204bd1e78f. Report an issue: GitHub.