Environment
- Astrid daemon + CLI 0.9.4
- macOS
Symptom
astrid capsule install <path> for a capsule whose [env] config carries a URL pointing at a private-IP endpoint (e.g. http://127.0.0.1:1234) should trigger the operator local-egress consent prompt so the endpoint gets added to [security.capsule_local_egress] in ~/.astrid/config.toml. The prompt is present and works correctly on a TTY.
On non-TTY stdin (piped install, astrid capsule install invoked from a script or CI runner, headless install from a coding agent) the prompt is silently skipped with no output. The capsule installs "successfully" from every visible signal, but its first HTTP call to that private-IP endpoint later fails with ErrorCode::AirlockRejected — hundreds of lines of guest-log noise before someone traces it back to the missing operator config.
Repro
# 1. Any capsule whose Capsule.toml declares capabilities that resolve to
# an IP-literal net_connect entry (e.g. 127.0.0.1:1234), or whose env
# schema has a URL pointing at a private IP. Ours is a routing capsule
# talking to LM Studio at 127.0.0.1:1234.
# 2. Install non-interactively (no TTY):
echo "" | astrid capsule install ./my-capsule
# or from a script/CI/an agent — no operator sitting at a prompt.
# 3. Install reports success:
# Live: the running daemon loaded 'my-capsule' — no restart needed.
# 4. Send the capsule any request that triggers its outbound HTTP call.
# Result: silent failure, or (with tracing) ErrorCode::AirlockRejected.
Where the silence comes from
astrid-cli/src/commands/capsule/local_egress.rs in maybe_prompt_local_egress:
pub(crate) fn maybe_prompt_local_egress(capsule_id: &str, value: &str, config_path: &Path) {
use std::io::IsTerminal;
// Only a TTY is a real operator at a prompt. A non-interactive stdin
// (script / pipe / CI) must not be read — reading would block waiting for
// input or steal the caller's piped data — and must not auto-write the
// exemption. Decline before touching stdin or the config.
if !std::io::stdin().is_terminal() {
return;
}
// …
}
The TTY guard is right — silently blocking on stdin or auto-blessing a private IP without operator consent would both be worse than skipping the prompt. What's missing is any signal to the operator that a bless step was skipped.
Suggested fix
Where the TTY guard bails, emit a one-line stderr notice:
warning: skipping local-egress bless for '<capsule_id>' → '<value>' (stdin is not a terminal).
add manually to ~/.astrid/config.toml:
[security.capsule_local_egress]
<capsule_id> = ["<host:port>"]
Bonus: include the same reminder in the install-summary output, so operators piping installs into build scripts see the missing step in the same place they see the install-success message.
Why this matters
The AirlockRejected symptom presents dozens of layers away from the actual cause — a capsule's HTTP call to a permitted endpoint fails with an error whose text ("Host function call failed") gives no hint that operator-side config is the culprit. Cost me an evening of debugging (patched daemon + several extra rounds of tracing) before I found the guard in maybe_prompt_local_egress. A one-line warning on the install path would collapse that debugging to a Ctrl-F.
Related
The manifest-side declaration (net_connect = ["127.0.0.1:1234"] in Capsule.toml) is necessary but not sufficient — I had that set from day one but the airlock still rejected because [security.capsule_local_egress] was empty. That's the correct design (operator has the final say on which capsules can reach loopback), but the install UX doesn't surface the operator-side requirement.
Environment
Symptom
astrid capsule install <path>for a capsule whose[env]config carries a URL pointing at a private-IP endpoint (e.g.http://127.0.0.1:1234) should trigger the operator local-egress consent prompt so the endpoint gets added to[security.capsule_local_egress]in~/.astrid/config.toml. The prompt is present and works correctly on a TTY.On non-TTY stdin (piped install,
astrid capsule installinvoked from a script or CI runner, headless install from a coding agent) the prompt is silently skipped with no output. The capsule installs "successfully" from every visible signal, but its first HTTP call to that private-IP endpoint later fails withErrorCode::AirlockRejected— hundreds of lines of guest-log noise before someone traces it back to the missing operator config.Repro
Where the silence comes from
astrid-cli/src/commands/capsule/local_egress.rsinmaybe_prompt_local_egress:The TTY guard is right — silently blocking on stdin or auto-blessing a private IP without operator consent would both be worse than skipping the prompt. What's missing is any signal to the operator that a bless step was skipped.
Suggested fix
Where the TTY guard bails, emit a one-line stderr notice:
Bonus: include the same reminder in the install-summary output, so operators piping installs into build scripts see the missing step in the same place they see the install-success message.
Why this matters
The
AirlockRejectedsymptom presents dozens of layers away from the actual cause — a capsule's HTTP call to a permitted endpoint fails with an error whose text ("Host function call failed") gives no hint that operator-side config is the culprit. Cost me an evening of debugging (patched daemon + several extra rounds of tracing) before I found the guard inmaybe_prompt_local_egress. A one-line warning on the install path would collapse that debugging to a Ctrl-F.Related
The manifest-side declaration (
net_connect = ["127.0.0.1:1234"]inCapsule.toml) is necessary but not sufficient — I had that set from day one but the airlock still rejected because[security.capsule_local_egress]was empty. That's the correct design (operator has the final say on which capsules can reach loopback), but the install UX doesn't surface the operator-side requirement.