iflytek/astron-agent · error
tenant bootstrap API key conflicts with an unmanaged…
Error message
tenant bootstrap API key conflicts with an unmanaged credential
What it means
When the bootstrap api_key row is not managed (missing the managed marker), it is treated as an unmanaged credential. The function tolerates it only if the row is valid, not deleted, and its secret matches the configured secret via constant-time compare; otherwise it reports a conflict between the bootstrap API key and an unmanaged credential.
Solutions
- Align the configured bootstrap secret with the existing row's secret (or vice versa) so they match
- Migrate the existing row into the managed flow by adding the managed marker and correct secret
- Choose a new api_key value with no pre-existing row
- Clean up the soft-deleted/NULL-flagged conflicting row before re-running bootstrap
Example fix
// before
conf = {APIKey: "ak-1", Secret: "new-secret"} // DB row has old-secret, unmanaged
// after
conf = {APIKey: "ak-1", Secret: "old-secret"} // matches unmanaged row, or re-key Defensive patterns
Strategy: validation
Validate before calling
var secret string; var marker string
err := db.QueryRow(`SELECT secret, marker FROM tb_app WHERE api_key = ?`, apiKey).Scan(&secret, &marker)
if err == nil && marker != managedMarker && secret != configuredSecret { return errors.New("api_key conflicts with unmanaged credential") } Try / catch
if err != nil && strings.Contains(err.Error(), "unmanaged credential") { rotateBootstrapKey(ctx, creds); return reconcileBootstrap(ctx, creds) } Prevention
- Provision apps only through the managed bootstrap flow
- Rotate bootstrap credentials in DB and config atomically
- Preserve the managed marker column in migrations
When it happens
Trigger: A tb_app row with the same api_key exists but was created outside the bootstrap flow (no tenantBootstrapManagedMarker), and either its secret differs from credentials.Secret, its secret/flags are NULL, or the row is soft-deleted.
Common situations: The api_key was issued manually or by a legacy provisioning tool, credentials were rotated in config but not in the DB, or the managed marker was lost after a manual DB edit.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- locked tenant bootstrap app does not match the reserved…
- reserved tenant bootstrap app is disabled or deleted
- tenant bootstrap API key is already assigned to another…
- ensure tenant bootstrap app failed
- check tenant bootstrap API key ownership failed
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/fc28645d4d7749ce.
Report an issue: GitHub.
Appendix: source
Thrown at core/tenant/tools/database/bootstrap_credentials.go:196
return false, fmt.Errorf("check tenant bootstrap API key ownership failed: %w", err)
}
var unmanagedSecret sql.NullString
var unmanagedIsDelete sql.NullBool
err = transaction.QueryRowContext(
ctx,
`SELECT api_secret, is_delete
FROM tb_auth
WHERE app_id = ? AND api_key = ? AND COALESCE(extend, '') <> ?
LIMIT 1 FOR UPDATE`,
credentials.TenantID,
credentials.APIKey,
tenantBootstrapManagedMarker,
).Scan(&unmanagedSecret, &unmanagedIsDelete)
if err == nil {
if !unmanagedSecret.Valid || !unmanagedIsDelete.Valid || unmanagedIsDelete.Bool ||
subtle.ConstantTimeCompare([]byte(unmanagedSecret.String), []byte(credentials.Secret)) != 1 {
return false, errors.New("tenant bootstrap API key conflicts with an unmanaged credential")
}
return true, nil
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, fmt.Errorf("check tenant bootstrap managed credential failed: %w", err)
}
return false, nil
}
func adoptTenantBootstrapCredential(
ctx context.Context,
transaction bootstrapTransaction,
credentials config.TenantBootstrapCredentials,
now string,
) error {
// A strong pair explicitly configured by the deployment may already have
// been created through Tenant's public API on an older release. Because it
// belongs to the reserved app and exactly matches the current deploymentView on GitHub (pinned to 5e758547a8)