astrid-runtime/astrid · error

could not authenticate as principal

Error message

could not authenticate as principal '{caller}' for `astrid mcp serve`: no keypair found (keys/{caller}.key), so the daemon would bind this connection to the no-capability `anonymous` identity. Every tool call would then fail the ingress-trust and capability checks and appear to hang. Refusing to serve the MCP bridge as `anonymous`.

Fix: run `astrid agent create {caller}` to mint its keypair (or back-fill an existing keyless principal's), then retry. To serve unauthenticated on purpose, pass `--principal anonymous` before `mcp serve`.

What it means

Before serving the MCP bridge, require_authenticated_unless_anonymous refuses to start when the caller principal is not authenticated and has no keypair on disk (keys/{caller}.key). Without a keypair the daemon would bind the connection to the capability-less `anonymous` identity, causing every tool call to fail ingress-trust/capability checks and appear to hang. The error is deliberately verbose and actionable.

Solutions

  1. Run `astrid agent create <principal>` to mint the missing keypair, then retry `astrid mcp serve`.
  2. If the principal already exists keylessly, back-fill its keypair with the agent-create command as instructed.
  3. If unauthenticated serving is intended, pass `--principal anonymous` explicitly before `mcp serve`.
  4. Verify the exact principal name and that keys/{principal}.key exists relative to the expected keys directory.

Example fix

// before
astrid mcp serve --principal alice   # no keys/alice.key
// after
astrid agent create alice
astrid mcp serve --principal alice
Defensive patterns

Strategy: validation

Validate before calling

let caller = principal_name;
let key_path = std::path::Path::new("keys").join(format!("{caller}.key"));
if caller != "anonymous" && !key_path.exists() {
    eprintln!("run: astrid agent create {caller}");
    std::process::exit(1);
}

Type guard

fn can_serve(caller: &str, authenticated: bool) -> bool {
    authenticated || caller == "anonymous"
        || std::path::Path::new("keys").join(format!("{caller}.key")).exists()
}

Try / catch

if let Err(e) = serve(principal, workspace).await {
    if e.to_string().contains("could not authenticate as principal") {
        // message already includes the fix command; surface it verbatim to the user
        eprintln!("{e}");
    }
}

Prevention

When it happens

Trigger: Running `astrid mcp serve --principal <name>` (or the library call) where <name> is not anonymous, authenticated=false (no keypair at keys/{name}.key), so serve() invokes this guard and bails.

Common situations: Principal created conceptually but `astrid agent create` never run; keypair deleted or keys/ directory lost; typo in the principal name so no key file matches; migrating machines without copying keys/.

Understand the failure class

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/c7b13f24aeec753f. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/mcp/mod.rs:141

/// identity.
///
/// `astrid --principal X mcp serve` connects, but if `X` has no keypair the
/// handshake falls to the legacy single-frame path and the daemon stamps the
/// connection `anonymous`. The bridge would then come up "successfully" yet
/// every `tools/call` fails the ingress-trust and capability checks — to a
/// client it just hangs/times out, with no hint why. This turns that silent,
/// confusing failure into a loud, actionable error at startup.
///
/// Requesting `anonymous` explicitly (`astrid --principal anonymous mcp serve`) is allowed:
/// serving unauthenticated is then a deliberate choice, not an accident.
fn require_authenticated_unless_anonymous(
    caller: &astrid_core::PrincipalId,
    authenticated: bool,
) -> Result<()> {
    if authenticated || *caller == astrid_core::PrincipalId::anonymous() {
        return Ok(());
    }
    anyhow::bail!(
        "could not authenticate as principal '{caller}' for `astrid mcp serve`: \
         no keypair found (keys/{caller}.key), so the daemon would bind this \
         connection to the no-capability `anonymous` identity. Every tool call \
         would then fail the ingress-trust and capability checks and appear to \
         hang. Refusing to serve the MCP bridge as `anonymous`.\n\n\
         Fix: run `astrid agent create {caller}` to mint its keypair (or \
         back-fill an existing keyless principal's), then retry. To serve \
         unauthenticated on purpose, pass `--principal anonymous` before `mcp serve`."
    );
}

/// Explicit `anonymous` MCP is a transport-only, no-capability mode and has no
/// broker capsule to prove. Named principals must always prove their broker
/// front door before stdio is exposed.
fn broker_readiness_required(caller: &astrid_core::PrincipalId) -> bool {
    *caller != astrid_core::PrincipalId::anonymous()
}

View on GitHub (pinned to affd8760f4)