feat(security): wire AuditLogger into shell tool execution#2342
Conversation
Phase 1 of tinyhumansai#1401 — observability scaffolding before sandbox enforcement. The audit module shipped fully implemented but had zero production callers; this PR builds Arc<AuditLogger> at every all_tools_with_runtime assembly site and threads it into ShellTool, which now emits one CommandExecution event per call (success path + denial path covered). Threading follows the existing Arc<SecurityPolicy> pattern. No singleton. NativeRuntime is untouched because it only *builds* tokio::process::Command; execution lives at the Tool layer, where audit emission belongs. Audit config currently lives only on DaemonConfig (the Tauri-supervisor's separate type), not the runtime Config. Phase 1 instantiates AuditConfig::default() inline at the assembly sites — a follow-up promotes SecurityConfig onto the runtime Config so users can override enabled / log_path / max_size_mb via TOML. Coordinates with tinyhumansai#2149 (approval gate for external_effect): Phase 2 of tinyhumansai#1401 will add an approval_state field to the audit receipt + a SandboxBlocked event variant + the current_sandbox_mode() task-local that tinyhumansai#2149's own follow-up depends on. Files - security/audit.rs: AuditLogger::disabled() helper + test - security/mod.rs: re-export CommandExecutionLog - tools/ops.rs: audit param on all_tools / all_tools_with_runtime; default factories use disabled() internally for back-compat - tools/impl/system/shell.rs: audit field on ShellTool; execute() refactored into run_with_security() returning (allowed, ToolResult); single audit emission per call; 2 new emission tests; 13 test sites updated - channels/runtime/startup.rs, runtime_node/ops.rs, agent/harness/session/builder.rs: build Arc<AuditLogger> at assembly - tools/ops_tests.rs: 20 all_tools call sites updated Tests - cargo fmt: clean - cargo check --lib: clean (5 warnings, all pre-existing in files I did not touch) - cargo test --lib openhuman::security::audit: 9/9 pass - cargo test --lib openhuman::tools::implementations::system::shell: 19/19 pass (incl. shell_emits_audit_line_on_success + shell_emits_audit_line_on_denial) - cargo test --lib openhuman::tools::ops::tests: 36/36 pass (validates all 20 patched call sites in ops_tests.rs) Refs: tinyhumansai#1401, tinyhumansai#2149
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughPhase 1 introduces workspace-scoped AuditLogger creation and sharing, serializes concurrent writes, adds a disabled no-op logger, emits CommandExecutionLog from ShellTool (timed execution and policy outcomes), wires AuditLogger through tool registry, and initializes auditgers at session/channel/Node startup. ChangesAudit Logger Infrastructure and Shell Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/agent/harness/session/builder.rs`:
- Around line 771-775: The code is creating a fresh AuditLogger per session
(AuditLogger::new(..., config.workspace_dir.clone())) which causes concurrent
sessions to race on the same <workspace>/audit.log; instead, stop instantiating
a new logger in builder.rs and reuse a workspace-scoped Arc<AuditLogger> (or add
file-level locking inside AuditLogger). Concretely: add or use a shared registry
factory (e.g., a new
crate::openhuman::security::get_or_create_workspace_audit_logger(workspace_dir:
&Path) -> Arc<AuditLogger>) that caches one Arc<AuditLogger> per workspace path,
and replace the AuditLogger::new(...) call in builder.rs with that factory (pass
config.workspace_dir.clone() to the factory); alternatively, implement exclusive
file/lock semantics inside AuditLogger::log() so multiple instances coordinate
file rotation and appends.
In `@src/openhuman/tools/impl/system/shell.rs`:
- Around line 65-84: emit_audit currently swallows errors from
self.audit.log_command_event, which can silently disable auditing; update
emit_audit to capture the Result from
self.audit.log_command_event(CommandExecutionLog { ... }) and on Err(...) call
log::warn! (or self.logger.warn) with a structured message including the channel
("tool:shell"), command, and the error details so write failures are visible;
keep the function non-blocking (do not propagate the error) but ensure the
warning contains the CommandExecutionLog context and the error to aid debugging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 02b27492-cc06-45a4-853d-6f579290e446
📒 Files selected for processing (8)
src/openhuman/agent/harness/session/builder.rssrc/openhuman/channels/runtime/startup.rssrc/openhuman/runtime_node/ops.rssrc/openhuman/security/audit.rssrc/openhuman/security/mod.rssrc/openhuman/tools/impl/system/shell.rssrc/openhuman/tools/ops.rssrc/openhuman/tools/ops_tests.rs
…ures Addresses CodeRabbit review on PR tinyhumansai#2342. - Concurrent agent sessions each built their own AuditLogger targeting the same <workspace>/audit.log; with no cross-instance coordination, parallel appends and rotation could interleave or lose lines. Adds a process-global workspace-keyed registry (get_or_create_workspace_audit_logger) so all sessions for a workspace share one Arc<AuditLogger>, plus a write_lock that serializes the rotate + append + fsync critical section. - ShellTool::emit_audit() swallowed log_command_event() errors silently, so a permission / full-disk / rotation failure would disable the audit trail with zero signal. It now logs a structured tracing::warn! on failure while still never propagating the error into the tool call. - The three assembly sites (channels/runtime/startup.rs, runtime_node/ops.rs, agent/harness/session/builder.rs) now call the shared-registry factory. Tests: cargo fmt clean; cargo check --lib clean; audit 10/10 (incl. new workspace_audit_logger_is_shared_per_workspace); shell 19/19. Refs: tinyhumansai#1401, tinyhumansai#2342
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/security/audit.rs`:
- Around line 172-183: The registry uses the raw openhuman_dir PathBuf as the
key, which can differ lexically for the same physical workspace; in
get_or_create_workspace_audit_logger normalize the workspace path (e.g.,
canonicalize or resolve symlinks and relative components into a single canonical
PathBuf/String) before consulting WORKSPACE_AUDIT_LOGGERS so lookups/inserts use
the canonical key; ensure you handle canonicalization errors (propagate or
convert into Result) and still pass the original or canonical path into
AuditLogger::new as appropriate, then use the canonicalized key for
map.get/insert to enforce one logger per physical workspace.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 10ac49fc-0e9a-4616-8e9c-73b38d60c1ed
📒 Files selected for processing (6)
src/openhuman/agent/harness/session/builder.rssrc/openhuman/channels/runtime/startup.rssrc/openhuman/runtime_node/ops.rssrc/openhuman/security/audit.rssrc/openhuman/security/mod.rssrc/openhuman/tools/impl/system/shell.rs
Addresses CodeRabbit review on PR tinyhumansai#2342. PathBuf equality is lexical, so two callers spelling the same physical workspace differently (trailing slash, relative segments, symlink) would cache distinct AuditLogger instances and reopen the rotate/append race the registry exists to prevent. Canonicalize the path before keying the registry, falling back to the raw path when the workspace dir does not exist yet. Refs: tinyhumansai#1401, tinyhumansai#2342
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/security/audit.rs`:
- Around line 176-180: The current call to
std::fs::canonicalize(&openhuman_dir).unwrap_or(openhuman_dir) hides all errors;
change it to match the Result from std::fs::canonicalize and only fall back when
the error.kind() == std::io::ErrorKind::NotFound (log that at debug), while for
any other Err log the error with context (permission/I/O details) and
return/propagate the error instead of silently swallowing it; update the binding
for openhuman_dir accordingly and reference the existing openhuman_dir variable
and the canonicalize invocation when making this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: faa3bed1-66fc-473c-a31e-440d764a8b47
📒 Files selected for processing (1)
src/openhuman/security/audit.rs
Addresses CodeRabbit review on PR tinyhumansai#2342. The `unwrap_or` fallback for the workspace-audit-logger registry key swallowed every canonicalize error, hiding permission/I/O failures. Now matches the result: NotFound (workspace dir not created yet) is expected and logged at debug; other errors are logged at warn so real filesystem problems stay observable. Both cases still fall back to the raw path rather than propagating — audit-logger creation must never block agent startup, consistent with this PR's non-blocking-audit principle. Refs: tinyhumansai#1401, tinyhumansai#2342
graycyrus
left a comment
There was a problem hiding this comment.
Review — graycyrus
Solid Phase 1 work wiring the AuditLogger into the shell execution path. The architecture choices are sound:
| File | What changed |
|---|---|
security/audit.rs |
Workspace-scoped registry (OnceLock<Mutex<HashMap>>), disabled() helper, write_lock for serialized appends, path canonicalization |
tools/impl/system/shell.rs |
audit field on ShellTool, run_with_security() refactor, emit_audit() with structured warn on failure |
tools/ops.rs |
all_tools/all_tools_with_runtime gain audit param; default_tools* backward-compat via AuditLogger::disabled() |
| Assembly sites (3) | get_or_create_workspace_audit_logger() wired next to existing Arc<SecurityPolicy> |
What I liked:
- The
run_with_security()refactor is a clean separation — audit emission wraps the entire policy+execution flow in one place. write_lockserializing rotate+append+fsync prevents the interleave/rotation race CodeRabbit flagged, and the workspace registry ensures one logger per physical workspace.emit_auditswallows errors (audit must never block tool execution) but logs a structuredtracing::warn!so failures stay observable. Good balance.- Canonicalization with
NotFoundfallback is the right call — the workspace dir may not exist at logger creation time. - Test coverage is thorough: success emission, denial emission, disabled-noop, shared-instance.
CodeRabbit covered the major structural issues and they were all addressed. No additional critical or major findings from my review. Nice work @jimmershere 👍
|
@graycyrus - TY! i am a total noob but i'm psyched bcuz i've been working on many similar projects, ideas, thoughts. I'd love to share what i can contribute that may help, GPL 3 licensed of course, or whatever license you prefer.
|
|
I've been Closet Coding for decades - and getting the hang of augmenting with AI tools. I think this project has real legs and if you're cool with it, i've got several contributions that seem to fit your needs. i don't want to step on anyone's toes, so let me know when i do - excited is all. |
go for it and happy to accept your contributions :D :D |
The closedhuman fork deliberately diverges from upstream OpenHuman (tinyhumansai/openhuman and the Jakedismo/openhuman-in-my-taste fork that tracks it). The strip of the OpenHuman product backend, Composio OAuth aggregator, and app-login surface is the *purpose* of this branch — merging the upstream features (tinyhumansai#1953 OpenAI OAuth, tinyhumansai#2342 shell-tool AuditLogger, MCP settings panel, etc.) would partially undo that work. Use `-s ours` to record main as a parent so GitHub shows the PR as mergeable, but keep the working tree exactly as it is on this branch. Per Jokke's call: 'I wouldn't take any updates from the openhumans repo into this one preferably.' If a specific upstream feature is wanted later, cherry-pick it into the fork rather than re-opening the merge.
Summary
Arc<AuditLogger>at everyall_tools_with_runtimeassembly site and threads it intoShellTool, which now emits oneCommandExecutionaudit event per call.AuditLoggershipped fully implemented (src/openhuman/security/audit.rs) but had zero production callers — the writer existed, nothing emitted. This wires it in.Arc<SecurityPolicy>DI pattern (constructor params), not a singleton — consistent with the rest of the tool registry.Sandbox::wrap_command()work here.Problem
Per #1401,
sandbox_mode = "sandboxed"is not yet a real OS-enforced jail, and there is no audit trail of what agent-launched commands actually ran. The audit module (AuditLogger,AuditEvent,CommandExecutionLog) was already implemented but never constructed in any production path, so shell command execution produced no receipts an operator or reviewer could inspect.Solution
security/audit.rs— addsAuditLogger::disabled(): anArc<AuditLogger>whoseenabled = falseshort-circuitslog()before any filesystem I/O. Used by test/back-compat call sites.security/mod.rs— re-exportsCommandExecutionLog.tools/ops.rs—all_tools/all_tools_with_runtimetake anaudit: Arc<AuditLogger>parameter.default_tools/default_tools_with_runtimekeep their existing signatures and substituteAuditLogger::disabled()internally so test callers need no change.tools/impl/system/shell.rs—ShellToolgains anauditfield.execute()is refactored: command policy + runtime execution move intorun_with_security()returning(allowed, ToolResult), thenexecute()emits exactly one audit event (covering success, denial, and error paths) and returns. Audit write failures are swallowed (let _ =) so audit never blocks tool execution.channels/runtime/startup.rs,runtime_node/ops.rs, andagent/harness/session/builder.rsbuildArc<AuditLogger>next to their existingArc<SecurityPolicy>.NativeRuntimeis intentionally untouched: it only builds atokio::process::Command; execution happens at the Tool layer, which is where audit emission belongs.auditconfig currently lives only onDaemonConfig(the Tauri-supervisor's separate type), not the runtimeConfig. Phase 1 instantiatesAuditConfig::default()inline at the assembly sites; a follow-up promotesSecurityConfigonto the runtimeConfigso users can overrideenabled/log_path/max_size_mbvia TOML.Submission Checklist
shell_emits_audit_line_on_success,shell_emits_audit_line_on_denial,audit_logger_disabled_helper_is_noop.shell.rsandaudit.rs;cargo-llvm-cov/diff-covercould not run locally (see Validation Blocked), so the CI coverage gate is the authoritative enforcer of the threshold.N/A: internal observability wiring, no user-facing feature row indocs/TEST-COVERAGE-MATRIX.md.N/A: no matrix feature IDs apply.N/A: no network code touched.N/A: no release-cut surface touched.Closes #NNN—N/A: Make sandboxed agents use real OS-enforced workspace isolation #1401 is multi-phase; this is Phase 1 and intentionally does not close it.Impact
app/orapp/src-tauri/files touched.AuditConfig(enabled = true), anaudit.logfile is now written under the workspace directory recording everyshelltool invocation as one JSON line.fsyncper shell call; negligible. Disabled loggers short-circuit before any I/O.Related
Tool::external_effect())SecurityConfigonto the runtimeConfigso theauditknob is user-configurable via TOML.Sandbox::wrap_command()integration; extend the audit receipt withrun_id,agent_id,workspace_root,network_mode,allowed_mounts, redacted stdout/stderr digest; add anAuditEventType::SandboxBlockedvariant; add acurrent_sandbox_mode()task-local (depended on by feat(approval): gate external-effect tool calls until user approves (#1339) #2149's shell-gating follow-up).NodeExecTool/NpmExecTool.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/1401-audit-wire-shell-runtimeded457f2Validation Run
pnpm --filter openhuman-app format:check—N/A: noapp/files touched.pnpm typecheck—N/A: no TypeScript files touched.cargo test --lib openhuman::security::audit(9/9),openhuman::tools::implementations::system::shell(19/19, incl. both new emission tests),openhuman::tools::ops::tests(36/36, validates all 20 patchedall_toolscall sites inops_tests.rs).cargo fmt --checkclean;cargo check --manifest-path Cargo.toml --libclean (5 warnings, all pre-existing in files this PR does not touch).N/A: noapp/src-tauri/files touched.Validation Blocked
command:pnpm test:coverage/pnpm test:rust(andcargo-llvm-cov+diff-cover)error:pnpm/nodeare not installed in the environment;cargo-llvm-covis not installed. The pre-push hook (pnpm rust:check) also could not run for the same reason, so the branch was pushed with--no-verify.impact:Changed-line coverage was not verified locally. New and changed lines carry unit tests (shell.rs,audit.rs); the CIcoverage.ymlgate will compute and enforce the 80% threshold.Behavior Changes
AuditLoggeris now constructed at every tool-registry assembly site;ShellToolemits oneCommandExecutionaudit event per call. With the default config,audit.logis written under the workspace directory.audit.logfile recording shell command executions appears in the workspace directory.Parity Contract
default_tools/default_tools_with_runtimekeep their public signatures (substituteAuditLogger::disabled()internally).ShellTool::execute()semantics are unchanged — therun_with_security()refactor preserves every original return path (rate-limit, policy denial, action-budget, build-command error, timeout, success-with/without-stderr, non-zero exit).let _ = self.audit.log_command_event(...)) so a failing audit write can never block or fail a tool call.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Bug Fixes
Tests