Skip to content

refactor: consolidate fnox client wrapper and secure stdin set#24

Closed
bglusman wants to merge 2 commits intomainfrom
codex-secure-fnox-wrapper
Closed

refactor: consolidate fnox client wrapper and secure stdin set#24
bglusman wants to merge 2 commits intomainfrom
codex-secure-fnox-wrapper

Conversation

@bglusman
Copy link
Copy Markdown
Owner

Summary

Draft follow-up that folds the useful refactor/fnox-client-wrapper work into an owned Codex branch without modifying the parallel feat/secure-chat-commands PR branch.

  • Adds onecli_client::FnoxClient as the shared subprocess wrapper for fnox get, fnox set, fnox list, and availability checks.
  • Migrates !secure set/list onto the wrapper.
  • Tightens fnox set to pass secret values over stdin instead of argv, keeping values out of ps/process-monitor output.
  • Makes !secure parsing whitespace-aware for pasted tab/newline input.
  • Adds structured, value-free logs for secure.set and secure.list with authenticated identity and secret name only.

Stack / Coordination

This branch includes the feat/secure-chat-commands commit (a4a09ffe) plus one Codex-owned follow-up commit. Review after/alongside PR #20. It does not update or depend on pushing to the other agent's branch.

Tests

  • cargo test -p onecli-client fnox_client
  • cargo test -p zeroclawed secure_
  • cargo clippy -p onecli-client -p zeroclawed --all-targets -- -D warnings
  • Full pre-push hook: fmt, workspace clippy, workspace unit tests, loom tests

Notes

vault.rs::get_secret_from_fnox still has its own subprocess path on branches where the fnox vault fallback exists; that should be the next consolidation target once the relevant stack is settled.

bglusman and others added 2 commits April 24, 2026 13:40
Adds `!secure` as a post-auth chat command dispatched before any agent
sees the message. Subcommands:

- `!secure set NAME=value` — stores the secret in fnox and replies
  with "Stored `NAME`" + a retention-warning. Never echoes the value.
- `!secure list` — lists stored secret names (not values). Parses
  fnox's output defensively; extra columns that look like values are
  stripped before replying.
- `!secure help` / `!secure` — usage string.

Dispatch model (matches existing `!switch`/`!sessions`/`!default`):

- `CommandHandler::is_secure_command(&str)` — static, case-insensitive.
- `CommandHandler::handle_secure(text, identity_id) -> String` —
  async (shells out to fnox), takes identity for future per-identity
  audit/ACL though not used yet. Wired into telegram.rs in both
  `handle_message_nonblocking` and `handle_message` paths, and added
  to the unknown-command exclusion list so `!secure` doesn't get
  intercepted as unknown before hitting the post-auth handler.

Subprocess approach today — `Command::new("fnox").args(["set", …])`.
Acknowledged-cost: three places now shell out to fnox (vault.rs,
secure_set, secure_list), which means three places that need an
availability check. **Follow-up PR (in flight) will migrate all three
to the fnox library crate via `fnox = { git = "https://github.com/jdx/fnox" }`**
— fnox already exposes public modules (secret_resolver, commands,
etc.) at `src/lib.rs`, so no upstream PR is needed.

Redaction discipline:
- The success reply names the stored secret but never echoes the
  value; tested.
- The failure reply surfaces fnox's stderr (user needs to know why
  it failed) but doesn't carry the value; tested.
- The telegram channel's `debug!` logs only that a !secure command
  was handled, without the message text. A `!secure set FOO=bar`
  would otherwise leak the value into ops logs.
- Name validation: `[A-Za-z0-9_-]+` only (matches the substitution
  engine's accepted ref-name syntax — so a stored secret can be
  immediately referenced as `{{secret:NAME}}` without a rename).
  Tested with three invalid-char cases.

5 given/when/then behavioral tests:
- `secure_set_reply_includes_name_but_not_value`
- `secure_set_surfaces_fnox_error_without_echoing_value`
- `secure_unknown_subcommand_returns_help`
- `secure_set_rejects_invalid_name_chars`
- `secure_list_returns_names_only`

All use a fake-fnox shell script installed on PATH via TempDir, so
the tests are hermetic regardless of whether the real fnox is
installed on the dev/CI machine.

Explicit non-goals for this PR (follow-ups):
- `!secure remove NAME` — pending a decision on whether fnox's
  own remove command or a config-file edit is the right approach.
- `!secure request NAME` — the out-of-band localhost-paste flow
  that avoids chat-transport retention entirely (see
  docs/rfcs/agent-secret-gateway.md §5).
- Matrix/WhatsApp channel wiring — same pattern as telegram, but
  adds separate verification surface. Doing in a follow-up to keep
  this PR reviewable.
- Library-based fnox integration (swap subprocess → lib calls) —
  committed as next PR per user direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 25, 2026 02:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates all fnox CLI subprocess interactions behind a shared onecli_client::FnoxClient wrapper and migrates zeroclawed’s !secure set/list to use it, including passing secret values over stdin (not argv) to reduce accidental exposure.

Changes:

  • Introduces onecli_client::FnoxClient + FnoxError for fnox get/set/list and availability checks.
  • Updates !secure command parsing to be whitespace-aware and routes !secure in the Telegram channel post-auth without logging message text.
  • Adds/updates tests to verify name-only responses and that secret values are not passed via argv.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/zeroclawed/src/commands.rs Adds whitespace-aware parsing helper, !secure dispatch + set/list implementations, and test coverage for value-non-echo + stdin-only behavior.
crates/zeroclawed/src/channels/telegram.rs Routes !secure as a post-auth local command and excludes it from unknown-command interception; avoids logging full text.
crates/onecli-client/src/lib.rs Exposes the new fnox wrapper module and re-exports FnoxClient/FnoxError.
crates/onecli-client/src/fnox_client.rs Implements consolidated subprocess wrapper for fnox operations with typed errors and unit tests.
crates/onecli-client/Cargo.toml Adds tempfile as a dev-dependency for fnox wrapper tests.
Cargo.lock Updates lockfile for the new dev dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +240 to +246
trimmed.split_whitespace().next().map(String::from)
}
})
.collect();
Ok(names)
}

Comment on lines +247 to +254
async fn run(&self, args: &[&str]) -> Result<std::process::Output, std::io::Error> {
Command::new(&self.binary)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
}
Comment on lines +1217 to +1219
if name.is_empty() || value.is_empty() {
return "⚠️ Usage: `!secure set NAME=value`".to_string();
}
Comment on lines +1965 to +2005
static SECURE_ENV_MUTEX: Mutex<()> = Mutex::new(());

fn install_fake_fnox(dir: &TempDir, body: &str) -> std::path::PathBuf {
let bin = dir.path().join("fnox");
fs::write(&bin, format!("#!/bin/sh\n{body}\n")).expect("write fake fnox");
let mut perms = fs::metadata(&bin).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&bin, perms).expect("chmod fake fnox");
dir.path().to_path_buf()
}

struct PathGuard {
original: Option<String>,
}
impl PathGuard {
fn prepend(dir: &std::path::Path) -> Self {
let original = std::env::var("PATH").ok();
let new_path = match &original {
Some(p) => format!("{}:{}", dir.display(), p),
None => dir.display().to_string(),
};
// Safety: tests holding SECURE_ENV_MUTEX serialize env
// mutation. `std::env::set_var` is marked unsafe in
// Rust 2024 for this exact reason.
unsafe {
std::env::set_var("PATH", new_path);
}
Self { original }
}
}
impl Drop for PathGuard {
fn drop(&mut self) {
unsafe {
match &self.original {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}
}
}
}

@bglusman
Copy link
Copy Markdown
Owner Author

Codex integration sweep note: I reviewed the inline comments on this PR. GitHub rejected direct inline replies for these older/outdated review comments with HTTP 422, so I am responding top-level instead: 3141257308, 3141257326, 3141257333, 3141257340.\n\nI did not edit this branch. Items that overlap the secure/fnox/host-agent/digest integration work are addressed in draft PR #38 (codex-integration-code), including stdin-based fnox set, bounded fnox waits, whitespace-safe !secure parsing, identity-aware !secure audit logs, valid-input host-agent properties, real WhatsApp HMAC verification, loopback OneCLI default bind, and race-free digest temp paths. Remaining PR-specific findings stay actionable for this branch owner or a follow-up.

@bglusman
Copy link
Copy Markdown
Owner Author

Acknowledged. The non-codex refactor/fnox-client-wrapper (#21) was merged into the integration branch (integration/all-code-changes), and the same stdin-not-argv defense from your branch is now applied via #40 (stacked on #34). If you want #24 to land independently, the merge target should pick one source of truth — either rebase #24 onto #21's tip, or close in favor of #21+#40.

@bglusman
Copy link
Copy Markdown
Owner Author

Closing — the stdin-not-argv work and FnoxClient consolidation from this PR were folded into #44 (squashed to 9ed51fbc) via the codex hardening cherry-picks. Thanks for the disciplined work on this; the stdin set was a real security improvement that the parallel branch (PR #21) didn't have first.

@bglusman bglusman closed this Apr 25, 2026
@bglusman bglusman deleted the codex-secure-fnox-wrapper branch May 1, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants