astrid-runtime/astrid · error

existing [security.capsule_local_egress].{capsule_id} is not

Error message

existing [security.capsule_local_egress].{capsule_id} is not an array

What it means

record_local_egress parses the operator's config file with toml_edit and inserts host arrays under [security.capsule_local_egress].<capsule_id>. If the key already exists but its value is not a TOML array (e.g. a string or inline table), the code cannot append ports and bails with this message. It protects the config from being silently overwritten with a wrong type.

Source

Thrown at crates/astrid-cli/src/commands/capsule/local_egress.rs:155

        toml_edit::DocumentMut::new()
    };

    // Navigate / create `[security.capsule_local_egress]`.
    let security = doc["security"].or_insert(toml_edit::table());
    if let Some(t) = security.as_table_mut() {
        // Keep the nested table from being rendered inline.
        t.set_implicit(true);
    }
    let egress = doc["security"]["capsule_local_egress"].or_insert(toml_edit::table());
    if let Some(t) = egress.as_table_mut() {
        t.set_implicit(true);
    }

    let list = doc["security"]["capsule_local_egress"][capsule_id].or_insert(
        toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new())),
    );
    let Some(arr) = list.as_array_mut() else {
        anyhow::bail!("existing [security.capsule_local_egress].{capsule_id} is not an array");
    };

    // Idempotent: skip if already present (case-insensitive host match handled
    // by the host enforcement; here exact-string is enough for the operator
    // file's own dedup).
    let already = arr
        .iter()
        .any(|v| v.as_str().is_some_and(|s| s.eq_ignore_ascii_case(entry)));
    if !already {
        arr.push(entry);
    }

    if let Some(parent) = config_path.parent() {
        std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
    }
    write_atomic(config_path, doc.to_string().as_bytes())
        .with_context(|| format!("write {}", config_path.display()))
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Open the config file and change the security.capsule_local_egress.<capsule_id> entry to a TOML array of strings, e.g. hosts = ["example.local"].
  2. Remove the malformed key and re-run the command so it recreates the array correctly.
  3. Validate the TOML file (e.g. with a TOML linter) after manual edits before running capsule commands.

Example fix

// before (config.toml)
[security.capsule_local_egress]
my-capsule = "api.local, db.local"
// after
[security.capsule_local_egress]
my-capsule = ["api.local", "db.local"]
Defensive patterns

Strategy: validation

Validate before calling

let cfg: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
let bad = cfg.get("security")
    .and_then(|s| s.get("capsule_local_egress"))
    .and_then(|e| e.get(capsule_id))
    .map(|v| !v.is_array())
    .unwrap_or(false);
if bad { eprintln!("fix [security.capsule_local_egress].{capsule_id}: must be an array"); }

Type guard

fn is_host_array(v: &toml::Value) -> bool {
    v.as_array().map_or(false, |a| a.iter().all(|x| x.is_str()))
}

Try / catch

match record_local_egress(...).await {
    Err(e) if e.to_string().contains("is not an array") => {
        // back up config, rewrite the key as an array, retry
    }
    r => r?,
}

Prevention

When it happens

Trigger: Running a capsule command that records local egress hosts when the config already defines security.capsule_local_egress.<capsule_id> as a non-array TOML value (string, integer, table).

Common situations: Hand-edited config where a list was written as a quoted string ('host1, host2') or as an inline table; an older tool version wrote a different shape; copy-pasting config snippets with mismatched types.

Related errors


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