Skip to content

feat: !secure chat commands for secret set/list (#31)#20

Closed
bglusman wants to merge 1 commit intomainfrom
feat/secure-chat-commands
Closed

feat: !secure chat commands for secret set/list (#31)#20
bglusman wants to merge 1 commit intomainfrom
feat/secure-chat-commands

Conversation

@bglusman
Copy link
Copy Markdown
Owner

Summary

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
    `Stored `NAME`` + a retention warning. Never echoes the value.
  • `!secure list` — lists stored secret NAMES (not values). Parses
    fnox's output defensively; extra columns are stripped before
    replying.
  • `!secure help` / bare `!secure` — usage string.

Wired into Telegram (both `handle_message` paths) and excluded from
the unknown-command pre-auth filter. Matrix/WhatsApp wiring is a
follow-up (same pattern, separate verification surface).

Redaction discipline

  • Success reply names the stored secret but never echoes the value.
  • Failure reply surfaces fnox's stderr (user needs to debug) but
    doesn't carry the value.
  • Telegram `debug!` logs only "handling !secure command" — no text
    (the text contains the value).
  • Name validation `[A-Za-z0-9_-]+` only — matches the substitution
    engine's accepted ref-name syntax so stored secrets can be
    immediately used as `{{secret:NAME}}`.

Tests

5 given/when/then in `commands::tests::secure_*`:

  1. Success reply contains NAME, not value.
  2. Failure reply surfaces fnox stderr without echoing value.
  3. Unknown subcommand returns help, doesn't shell out.
  4. Invalid name chars rejected without shelling out.
  5. `!secure list` returns names only, strips value-like columns.

All use a fake-fnox shell script installed on `PATH` via `TempDir` —
hermetic, works whether or not real fnox is installed.

Known tradeoff

Subprocess approach means three call-sites now shell out to fnox
(`vault.rs::get_secret_from_fnox`, `secure_set`, `secure_list`) —
three availability checks. Follow-up PR migrates all three to the
fnox library crate
(`fnox = { git = "https://github.com/jdx/fnox" }`
— lib.rs already public, no upstream PR needed).

Non-goals (follow-ups)

  • `!secure remove NAME` — pending fnox API choice.
  • `!secure request NAME` — out-of-band localhost-paste flow per
    `docs/rfcs/agent-secret-gateway.md` §5.
  • Matrix/WhatsApp wiring.
  • Library-based fnox integration.

🤖 Generated with Claude Code

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 24, 2026 17:41
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

Adds a post-auth !secure chat command to ZeroClawed so secrets can be set/listed without ever being routed into an agent’s context (currently wired for Telegram only).

Changes:

  • Introduces CommandHandler::is_secure_command and handle_secure plus secure_set/secure_list/secure_help implementations.
  • Adds hermetic tests using a fake fnox binary injected via PATH.
  • Wires !secure dispatch into both Telegram message handling paths and excludes it from the unknown-command filter.

Reviewed changes

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

File Description
crates/zeroclawed/src/commands.rs Implements !secure parsing/dispatch, shells out to fnox, and adds tests ensuring no value is echoed back.
crates/zeroclawed/src/channels/telegram.rs Routes !secure messages to the new handler (without logging raw text) and prevents unknown-command handling from intercepting it.

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

Comment on lines +288 to 294
let cmd = trimmed.split(' ').next().unwrap_or("").to_lowercase();
cmd == "!secure"
}

/// Respond to unknown commands with a helpful message.
pub fn unknown_command(&self, text: &str) -> String {
let cmd = text.trim().split(' ').next().unwrap_or("").to_string();
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

is_secure_command splits on a literal space (split(' ')). If a user sends !secure followed by a tab/newline (common when pasting from mobile), this will fail to detect the command and it may fall through to the unknown-command path (which can echo the entire first “token”, potentially including the secret). Use split_whitespace() (or a char::is_whitespace split) to reliably parse the leading command word.

Suggested change
let cmd = trimmed.split(' ').next().unwrap_or("").to_lowercase();
cmd == "!secure"
}
/// Respond to unknown commands with a helpful message.
pub fn unknown_command(&self, text: &str) -> String {
let cmd = text.trim().split(' ').next().unwrap_or("").to_string();
let cmd = trimmed.split_whitespace().next().unwrap_or("").to_lowercase();
cmd == "!secure"
}
/// Respond to unknown commands with a helpful message.
pub fn unknown_command(&self, text: &str) -> String {
let cmd = text
.trim()
.split_whitespace()
.next()
.unwrap_or("")
.to_string();

Copilot uses AI. Check for mistakes.
Comment on lines +876 to +880
// `!secure ...` — split off the subcommand word.
let mut parts = trimmed.splitn(3, ' ');
let _lead = parts.next(); // `!secure`
let sub = parts.next().map(|s| s.to_lowercase()).unwrap_or_default();
let rest = parts.next().unwrap_or("").trim();
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

handle_secure uses splitn(..., ' ') to parse !secure <subcommand> .... Even if is_secure_command is fixed, this still won’t handle tabs/newlines or repeated whitespace between words, which can make set/list/help parsing fail for pasted input. Consider parsing the leading words with split_whitespace() and then deriving rest from the original string via slicing (or by joining the remaining iterator) to preserve the value portion.

Suggested change
// `!secure ...` — split off the subcommand word.
let mut parts = trimmed.splitn(3, ' ');
let _lead = parts.next(); // `!secure`
let sub = parts.next().map(|s| s.to_lowercase()).unwrap_or_default();
let rest = parts.next().unwrap_or("").trim();
// `!secure ...` — parse the leading words using whitespace, but preserve
// the remainder for `set NAME=value`.
let mut parts = trimmed.split_whitespace();
let lead = parts.next().unwrap_or_default(); // `!secure`
let sub_raw = parts.next().unwrap_or_default();
let sub = sub_raw.to_lowercase();
let rest = if sub_raw.is_empty() {
""
} else {
let after_lead = &trimmed[lead.len()..];
if let Some(sub_start) = after_lead.find(sub_raw) {
let after_sub = &after_lead[sub_start + sub_raw.len()..];
after_sub.trim()
} else {
""
}
};

Copilot uses AI. Check for mistakes.
Comment on lines +1217 to +1219
// Only allow the same name-shape substitution accepts, so callers
// can immediately use the stored name in a `{{secret:NAME}}` ref.
// See crates/security-proxy/src/substitution.rs.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The reference crates/security-proxy/src/substitution.rs appears to be stale/nonexistent in this repo, so the comment doesn’t provide a valid pointer for why the name regex is chosen. Please update it to the actual location that defines the secret-name syntax (or remove the reference if the check is self-contained here).

Suggested change
// Only allow the same name-shape substitution accepts, so callers
// can immediately use the stored name in a `{{secret:NAME}}` ref.
// See crates/security-proxy/src/substitution.rs.
// Only allow names that are immediately usable in a `{{secret:NAME}}`
// reference. The accepted syntax here is ASCII alphanumeric plus `_`
// and `-`, matching the validation enforced below.

Copilot uses AI. Check for mistakes.
Comment on lines +1230 to +1233
use tokio::process::Command;
let output = match Command::new("fnox")
.args(["set", &name, &value])
.output()
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

fnox set is invoked with the secret value as a command-line argument (args(["set", &name, &value])). This exposes the secret to host-level process inspection (e.g., ps, procfs, process monitors) even though you avoid routing it to an agent. If fnox supports it, prefer passing the value via stdin (or a temp file descriptor / env var) to keep it out of argv; otherwise this should be called out as an explicit limitation until the library-based integration lands.

Copilot uses AI. Check for mistakes.
Comment on lines +874 to +876
pub async fn handle_secure(&self, text: &str, _identity_id: &str) -> String {
let trimmed = text.trim();
// `!secure ...` — split off the subcommand word.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

handle_secure accepts an identity_id parameter but it’s currently unused. Either remove this parameter to avoid implying auditing/attribution, or use it (e.g., include it in an audit log entry or pass it to fnox metadata if supported) so the call sites’ “post-auth for audit” rationale is actually realized.

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +267
// agent. Runs post-auth so we can audit who set what; doesn't yet
// gate by role (open for any authenticated identity). The handler
// is async because it shells out to `fnox`.
//
// We deliberately do NOT log `text` here or in the handler — the
// `!secure set NAME=value` form contains a secret value that must
// not appear in ops logs. debug! logs only that a !secure command
// was handled; the handler never logs `text` either.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The comment says !secure runs post-auth “so we can audit who set what”, but handle_secure(..., identity_id) currently ignores the identity and secure_set/secure_list don’t emit any audit record. Please either implement auditing/attribution (at least a structured log with identity + secret name, never the value) or adjust the comment to avoid overstating the current behavior.

Suggested change
// agent. Runs post-auth so we can audit who set what; doesn't yet
// gate by role (open for any authenticated identity). The handler
// is async because it shells out to `fnox`.
//
// We deliberately do NOT log `text` here or in the handler — the
// `!secure set NAME=value` form contains a secret value that must
// not appear in ops logs. debug! logs only that a !secure command
// was handled; the handler never logs `text` either.
// agent. Runs post-auth so the authenticated identity is available
// to downstream handling; doesn't yet gate by role (open for any
// authenticated identity). The handler is async because it shells
// out to `fnox`.
//
// We deliberately do NOT log `text` here or in the handler — the
// `!secure set NAME=value` form contains a secret value that must
// not appear in ops logs. This site logs only that a !secure
// command was handled, not a structured audit record of the secret
// operation itself.

Copilot uses AI. Check for mistakes.
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

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


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

Comment on lines +859 to +861
/// Handle `!secure <subcommand>`. Dispatches to `fnox` as a
/// subprocess; nothing about the secret value transits an agent's
/// context. Never echoes back values — responses are name-only.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

PR description mentions three fnox subprocess call-sites (including vault.rs::get_secret_from_fnox), but in this branch the only Rust fnox subprocess usage appears to be in secure_set/secure_list (no other Command::new("fnox") call-sites found). Either update the PR description to match the actual change set, or include the other referenced migration in this PR if it was intended.

Copilot uses AI. Check for mistakes.
Comment on lines +1276 to +1278
// that looks like a secret name (non-whitespace leading token)
// and present a name-only summary. Don't attempt structured
// parsing; bail to a raw echo if the output isn't lines-of-tokens.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The comment says to “bail to a raw echo if the output isn't lines-of-tokens”, but the current implementation always tokenizes every non-empty/non-# line and never falls back to raw output. Either implement the described fallback (e.g., detect unexpected formats) or adjust the comment to match the actual behavior.

Suggested change
// that looks like a secret name (non-whitespace leading token)
// and present a name-only summary. Don't attempt structured
// parsing; bail to a raw echo if the output isn't lines-of-tokens.
// that looks like a secret name (the first non-whitespace token on
// each non-empty, non-comment line) and present a name-only summary.
// Keep parsing permissive rather than attempting a strict structured
// format check.

Copilot uses AI. Check for mistakes.
Comment on lines +1262 to +1266
async fn secure_list() -> String {
use tokio::process::Command;
let output = match Command::new("fnox").arg("list").output().await {
Ok(o) => o,
Err(e) => {
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

Both secure_set and secure_list await Command::output() with no timeout. If fnox blocks (waiting for input, stuck on backend, etc.) these spawned tasks can hang indefinitely and accumulate under load. Consider wrapping the subprocess in a tokio::time::timeout and returning a clear error on expiry (and/or killing the child process).

Copilot uses AI. Check for mistakes.
Comment on lines +2002 to +2014
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"),
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The unsafe { std::env::set_var(...) } block is unnecessary in this crate (edition 2021), so it will trigger the unused_unsafe warning; with CI running cargo clippy -- -D warnings, this likely fails the build. Either remove the unsafe blocks, or add a targeted #[allow(unused_unsafe)]/#[allow(clippy::unused_unsafe)] if you want to keep the code forward-compatible with Rust 2024’s set_var signature change.

Suggested change
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"),
}
std::env::set_var("PATH", new_path);
Self { original }
}
}
impl Drop for PathGuard {
fn drop(&mut self) {
match &self.original {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),

Copilot uses AI. Check for mistakes.
Comment on lines +2010 to +2014
unsafe {
match &self.original {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

Same unused_unsafe issue in Drop for PathGuard: the unsafe { ... } block currently contains only safe calls in edition 2021 and is likely to fail -D warnings in CI. Remove the unsafe or suppress the lint in a narrowly-scoped way.

Suggested change
unsafe {
match &self.original {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}
match &self.original {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),

Copilot uses AI. Check for mistakes.
@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: 3139403790, 3139403820, 3139403838, 3139403851, 3139403866, 3139403882, 3141251479, 3141251485, 3141251494, 3141251502, 3141251509.\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 added a commit that referenced this pull request Apr 25, 2026
Cross-cutting triage finding (PRs #20, #21, #23, #26, #28, #31): the
!secure parser used `splitn(' ')` which mis-shapes multi-space
input — `"!secure  set NAME=v"` parsed as sub="" rest="set NAME=v",
silently routing through the unknown-subcommand path with the secret
in the help-error message. Switch to `split_whitespace()`.

Also wires the previously-unused `identity_id` into an audit log so
the `_` prefix can drop. The log records identity + subcommand, never
the value (`rest`). Closes the "audit-claim docstring with no audit
implementation" finding from triage.
bglusman added a commit that referenced this pull request Apr 25, 2026
…ning (#44)

Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security
fixes + codex agent's hardening, all green CI (14/14 checks).

## Features landing
- **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21)
- **Adversarial commit-reviewer + mechanical pre-commit gate** (#18)
- **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19)
- **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense
- **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31)
- **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23)
- **install.sh wires MCP** into Claude Code agent configs (#26)
- **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34)
- **Bulk paste UI** — .env-style multi-secret onboarding with per-line results
- **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance
- **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening)

## Security fixes folded in
- /vault/:secret bearer auth + 127.0.0.1 default bind (#39)
- URL-embedded secrets honor destination allowlist (#41)
- Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40)
- Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43)
- Subprocess timeouts + kill_on_drop on FnoxClient
- BrokenPipe-tolerant stdin write (Linux CI surface)
- Header-value log redaction
- OneCLI bound to 127.0.0.1 by default
- Sanitized real API token + Telegram IDs from sample configs (#36)

## Architecture / refactors
- Consolidated onecli binary into security-proxy (#17)
- Hardcoded vault URL removed from onecli-client
- security-proxy resolver wired into hot path
- Extracted build_app router; migrated /vault/:secret route
- !secure parser uses split_whitespace (was splitn), audit-logs invocations

## Test coverage added
- security-proxy substitution engine + body/headers tests
- onecli-client retry + Http(_) variant + adversarial fallthrough suite
- onecli-client client.rs rewritten from tautologies to wiremock-backed
- config/validator coverage (was zero, now 290-line module covered)
- 16 zeroclawed-secret-paste tests including bulk-mode cases

## Docs / RFCs
- agent-secret-gateway holistic architecture
- consolidation-findings (what #28 must address)
- secret-input-web-ui RFC (input-only, new-by-default)
- browser-harness integration spike
- test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files)

## Codex agent's hardening cherry-picks
- Subprocess timeouts on fnox calls
- map_spawn_error helper
- Validator hardening + atomic-counter digest race fix
- WhatsApp HMAC implementation + tests
- proxy header-value log redaction

CI: all 14 checks green at squash time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@bglusman
Copy link
Copy Markdown
Owner Author

Subsumed by #44 (squashed to 9ed51fbc on main). All commits from this branch are present in the squash. Closing as redundant rather than merging again.

@bglusman bglusman closed this Apr 25, 2026
bglusman added a commit that referenced this pull request Apr 25, 2026
…ning (#44)

Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security
fixes + codex agent's hardening, all green CI (14/14 checks).

## Features landing
- **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21)
- **Adversarial commit-reviewer + mechanical pre-commit gate** (#18)
- **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19)
- **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense
- **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31)
- **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23)
- **install.sh wires MCP** into Claude Code agent configs (#26)
- **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34)
- **Bulk paste UI** — .env-style multi-secret onboarding with per-line results
- **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance
- **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening)

## Security fixes folded in
- /vault/:secret bearer auth + 127.0.0.1 default bind (#39)
- URL-embedded secrets honor destination allowlist (#41)
- Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40)
- Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43)
- Subprocess timeouts + kill_on_drop on FnoxClient
- BrokenPipe-tolerant stdin write (Linux CI surface)
- Header-value log redaction
- OneCLI bound to 127.0.0.1 by default
- Sanitized real API token + Telegram IDs from sample configs (#36)

## Architecture / refactors
- Consolidated onecli binary into security-proxy (#17)
- Hardcoded vault URL removed from onecli-client
- security-proxy resolver wired into hot path
- Extracted build_app router; migrated /vault/:secret route
- !secure parser uses split_whitespace (was splitn), audit-logs invocations

## Test coverage added
- security-proxy substitution engine + body/headers tests
- onecli-client retry + Http(_) variant + adversarial fallthrough suite
- onecli-client client.rs rewritten from tautologies to wiremock-backed
- config/validator coverage (was zero, now 290-line module covered)
- 16 zeroclawed-secret-paste tests including bulk-mode cases

## Docs / RFCs
- agent-secret-gateway holistic architecture
- consolidation-findings (what #28 must address)
- secret-input-web-ui RFC (input-only, new-by-default)
- browser-harness integration spike
- test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files)

## Codex agent's hardening cherry-picks
- Subprocess timeouts on fnox calls
- map_spawn_error helper
- Validator hardening + atomic-counter digest race fix
- WhatsApp HMAC implementation + tests
- proxy header-value log redaction

CI: all 14 checks green at squash time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
bglusman added a commit that referenced this pull request Apr 26, 2026
V1 of `.github/copilot-instructions.md` was ~970 words and read more like
documentation than reviewer guidance. Two issues that hurt signal:

1. **Length** — Copilot's per-repo instructions read window is ~4000
   chars; v1 was over that, so the trailing past-mistake list and
   skip-list were getting truncated.
2. **Format** — long bulleted exposition reads less like a rule and
   more like prose, which Copilot treats as background context rather
   than as constraints to apply.

V2 changes:

- Cuts to ~3500 chars by condensing the prioritization tiers and
  removing per-class HIGH/MED/LOW exposition (the priority-order list
  carries the same info in 5 lines).
- Leads with a single review philosophy line ("if uncertain, do not
  comment"), the highest-leverage rule borrowed from deno's
  copilot-instructions.md.
- Names specific past Copilot noise patterns from this repo's PR
  history (env-mutex/serial_test repeated 8+ times across #19/#22/#23;
  dead-doc-reference 4x across #20/#23/#25) so the "don't repeat
  across PRs" rule has teeth.
- Cross-references a new path-scoped file at
  `.github/instructions/rust.instructions.md` (`applyTo: "**/*.rs"`),
  which carries the Rust-specific review nits (`#[expect]` over
  `#[allow]`, `// SAFETY:` requirement, `Mutex` across `.await`,
  `select!` cancellation safety, `kill_on_drop`, `&str` over
  `&String`, `LazyLock<Regex>` for hot paths, etc.).

Path-scoped instructions are loaded only when a PR touches a file
matching `applyTo`, so Rust-specific rules don't burn the global
4000-char budget on PRs that only touch docs / TOML / shell.
bglusman added a commit that referenced this pull request Apr 27, 2026
…de (#56)

* chore(.github): add copilot-instructions.md to tune PR-review behavior

GitHub Copilot supports per-repo review instructions at
.github/copilot-instructions.md (≤2 pages, applied to every Copilot
PR review automatically). Adds calciforge-specific guidance to
improve signal-to-noise:

- Skip what pre-commit already gates (fmt, clippy, gitleaks)
- Prioritize HIGH-severity classes that bit us in past reviews:
  secret leakage in logs, substitution-boundary correctness,
  unwrap/expect outside tests, missing unsafe around set_var
  (edition 2024), blocking I/O in async, auth bypass paths
- Tell Copilot what's NOT a bug despite looking like one:
  {{secret:NAME}} sentinel syntax, post-history-scrub fake test
  values, FnoxClient subprocess-by-design, clashd/zeroclaw_*
  upstream references, mixed Rust edition (known)
- Past-mistake checklist (6 classes from real findings that
  landed and were caught later — substitution-after-bypass,
  None dest_host, bearer-in-info-log, fnox set argv leak,
  0.0.0.0 default, hardcoded fallback URLs)
- Skip even-if-correct: 'consider adding tests' without
  specifics, rename suggestions vs. functional convention,
  feature-creep proposals

Cross-references AGENTS.md (host-agent coding standards) and
CLAUDE.md (public-repo secret discipline) so Copilot follows
both. 83 lines, well under the documented 2-page cap.

* chore(.github): tighten copilot-instructions + add path-scoped Rust file

V1 of `.github/copilot-instructions.md` was ~970 words and read more like
documentation than reviewer guidance. Two issues that hurt signal:

1. **Length** — Copilot's per-repo instructions read window is ~4000
   chars; v1 was over that, so the trailing past-mistake list and
   skip-list were getting truncated.
2. **Format** — long bulleted exposition reads less like a rule and
   more like prose, which Copilot treats as background context rather
   than as constraints to apply.

V2 changes:

- Cuts to ~3500 chars by condensing the prioritization tiers and
  removing per-class HIGH/MED/LOW exposition (the priority-order list
  carries the same info in 5 lines).
- Leads with a single review philosophy line ("if uncertain, do not
  comment"), the highest-leverage rule borrowed from deno's
  copilot-instructions.md.
- Names specific past Copilot noise patterns from this repo's PR
  history (env-mutex/serial_test repeated 8+ times across #19/#22/#23;
  dead-doc-reference 4x across #20/#23/#25) so the "don't repeat
  across PRs" rule has teeth.
- Cross-references a new path-scoped file at
  `.github/instructions/rust.instructions.md` (`applyTo: "**/*.rs"`),
  which carries the Rust-specific review nits (`#[expect]` over
  `#[allow]`, `// SAFETY:` requirement, `Mutex` across `.await`,
  `select!` cancellation safety, `kill_on_drop`, `&str` over
  `&String`, `LazyLock<Regex>` for hot paths, etc.).

Path-scoped instructions are loaded only when a PR touches a file
matching `applyTo`, so Rust-specific rules don't burn the global
4000-char budget on PRs that only touch docs / TOML / shell.

* chore(.github): restore AGENTS.md + CLAUDE.md cross-refs in copilot instructions

Verified GitHub's copilot-instructions docs do not specify the ~4000-char
read window I'd assumed in the previous commit — that was the older
Copilot Chat feature, not the Copilot code-review one. With no real
length pressure, the AGENTS.md / CLAUDE.md pointers (dropped in v2 to
save chars) are worth restoring. CLAUDE.md's "never commit these" list
is exactly the kind of leakage Copilot should be enforcing on diff.

* docs: split AGENTS.md into workspace-wide root + host-agent crate file

The root `AGENTS.md` was titled "Calciforge Host-Agent" and carried
host-agent-specific build/architecture rules — at the repo root, where
agents (Claude Code, Codex, Copilot cloud agent, OpenClaw) read it as
workspace-wide guidance. The mismatch meant agents working in any
non-host-agent crate were getting irrelevant rules ("ZFS snapshot
delegation", "mTLS CN→Unix user") and missing the actually-shared
ones (substitution-boundary order, sentinel string contract,
public-repo secret discipline pointer).

Restructure:

- Move the existing host-agent content verbatim to
  `crates/host-agent/AGENTS.md` (`git mv` so history is preserved).
- New root `AGENTS.md` covers the whole workspace: crate inventory,
  project vocabulary, mandatory rules every agent must follow
  (CLAUDE.md secret discipline, pre-commit gate, sentinel contract,
  substitution boundary order, no-secret-values-in-logs, fnox stdin
  mode), workspace build/test commands, and pointers into per-area
  files (`crates/host-agent/AGENTS.md`, `docs/rfcs/`,
  `docs/security-gateway.md`, `docs/model-gateway.md`).

Cross-refs `.github/copilot-instructions.md` and
`.github/instructions/rust.instructions.md` so agents that find
AGENTS.md first can pick up the Copilot-specific tuning if relevant.

Pairs with the copilot-instructions tightening earlier on this branch.

* fix(.github): correct gitleaks allowlist description in copilot-instructions

Copilot's review caught a real factual error in v2: the line claimed
specific literals (`+15555550100`, `7000000001`, `eyJ0eXAi...`) were
allowlisted in `.gitleaks.toml`. They aren't — `7000000001` is even
used in non-allowlisted source (`crates/calciforge/src/auth.rs`). The
real allowlist mechanism is path-based (`tests/**/fixtures/`,
`docs/rfcs/*.md`, lockfiles, etc.) plus a small regex list (loopback,
RFC 5737, a few inherited-from-main values).

Replace the misleading "these specific literals are allowlisted" claim
with an accurate description of how the allowlist actually works, so
Copilot doesn't downgrade real findings on the assumption they fall
under a non-existent literal-match exemption.

Pleasingly meta: this is exactly the "verify against the codebase
before commenting" rule from the same file working as intended on the
PR that introduced the file.
@bglusman bglusman deleted the feat/secure-chat-commands 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