Files
Kigi-CLI/crates/codegen/kigi-sandbox/src/logging.rs
T
雷电芽衣 97c934a647 fix(sandbox): strict profile no longer lets a child rewrite ~/.kigi
Port of upstream grok-build 1.0.14 ("--sandbox strict now restricts
writes to ~/.grok/sessions only"), shaped for Kigi.

The strict profile granted the whole ~/.kigi read-write, so a sandboxed
command could plant a global SessionStart hook or flip
`[sandbox].profile` in config.toml for the next launch. Landlock cannot
carve a child out of a write grant, so the home is now read-only
except an explicit allowlist of state subtrees (run, sessions,
worktrees, worktree_pool, tasks, memory, logs, debug, crash, memtrace,
trace-exports); config.toml, auth.json, sandbox.toml, hooks/, bin/ and
vendor/ stay read-only. A symlinked state dir under the home is
followed only to the default ~/.kigi/sessions.

What the process itself writes there after enforcement moves or is
minted first:
- kigi-shell `apply_sandbox` mints the device id, extracts the bundled
  rg and the bundled skills BEFORE enforcement for every profile that
  restricts home writes (`restricts_home_writes`, custom profiles
  extending strict included).
- The sandbox audit log lives at ~/.kigi/sessions/sandbox-events.jsonl.
- Leader lock/socket default to ~/.kigi/run/, the leader stderr log to
  ~/.kigi/logs/leader.log; discovery scans run/ and the legacy root,
  and the default target prefers the current socket.
Root-level caches (version.json, agent_id, slash-mru.json, pager.toml,
claude_import_state.json) degrade best-effort under strict; documented.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 02:08:42 -04:00

104 lines
2.7 KiB
Rust

//! Sandbox event logger.
//!
//! Events (profile applied, violations, bypasses) are buffered in memory and
//! flushed as JSONL to `~/.kigi/sessions/sandbox-events.jsonl`.
use std::path::PathBuf;
use std::sync::Mutex;
use crate::types::{SandboxEvent, SandboxEventType, SandboxMetrics};
pub struct SandboxLogger {
events: Mutex<Vec<SandboxEvent>>,
metrics: SandboxMetrics,
}
impl SandboxLogger {
pub fn new() -> Self {
Self {
events: Mutex::new(Vec::new()),
metrics: SandboxMetrics::default(),
}
}
pub fn log(&self, event: SandboxEvent) {
match &event.event_type {
SandboxEventType::FsViolation => self.metrics.inc_fs_violation(),
SandboxEventType::NetViolation => self.metrics.inc_net_violation(),
SandboxEventType::BypassGranted => self.metrics.inc_bypass_granted(),
SandboxEventType::BypassDenied => self.metrics.inc_bypass_denied(),
_ => {}
}
tracing::debug!(
event_type = ?event.event_type,
profile = %event.profile,
target = ?event.target,
operation = ?event.operation,
"sandbox event"
);
if let Ok(mut events) = self.events.lock() {
events.push(event);
}
}
pub fn metrics(&self) -> &SandboxMetrics {
&self.metrics
}
/// Drains the buffer.
pub fn take_events(&self) -> Vec<SandboxEvent> {
self.events
.lock()
.map(|mut events| std::mem::take(&mut *events))
.unwrap_or_default()
}
pub fn flush_to_disk(&self) -> anyhow::Result<()> {
let events = self.take_events();
if events.is_empty() {
return Ok(());
}
let log_path = Self::log_file_path();
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)?;
}
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
for event in &events {
if let Ok(json) = serde_json::to_string(event) {
writeln!(file, "{}", json)?;
}
}
tracing::debug!(
path = %log_path.display(),
count = events.len(),
"flushed sandbox events to disk"
);
Ok(())
}
/// Under `sessions/`: the only home subtree a strict sandbox leaves
/// writable, and this flush runs after enforcement.
fn log_file_path() -> PathBuf {
kigi_config::kigi_home()
.join("sessions")
.join("sandbox-events.jsonl")
}
}
impl Default for SandboxLogger {
fn default() -> Self {
Self::new()
}
}