Skip to content

Commit 86d4749

Browse files
committed
new: filesystem.append_to_file tool
1 parent ddecfb9 commit 86d4749

File tree

2 files changed

+87
-2
lines changed

2 files changed

+87
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
To append a structured JSON object to the log file:

nerve-core/src/agent/namespaces/filesystem/mod.rs

+86-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use std::collections::HashMap;
2-
use std::fs::{self, FileType};
2+
use std::fs::{self, FileType, OpenOptions};
3+
use std::io::Write;
34
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
45

56
use async_trait::async_trait;
67
use chrono::{DateTime, Local};
8+
use colored::Colorize;
79
use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
810

911
use anyhow::Result;
12+
use serde::Serialize;
1013

1114
use super::{Action, Namespace};
1215
use crate::agent::state::SharedState;
16+
use crate::agent::task::variables::get_variable;
1317

1418
// cast needed for Darwin apparently
1519
#[allow(clippy::unnecessary_cast)]
@@ -154,11 +158,91 @@ impl Action for ReadFile {
154158
}
155159
}
156160

161+
const DEFAULT_APPEND_TO_FILE_TARGET: &str = "findings.jsonl";
162+
163+
#[derive(Debug, Default, Serialize)]
164+
struct InvalidJSON {
165+
data: String,
166+
}
167+
168+
#[derive(Debug, Default, Clone)]
169+
struct AppendToFile {}
170+
171+
#[async_trait]
172+
impl Action for AppendToFile {
173+
fn name(&self) -> &str {
174+
"append_to_file"
175+
}
176+
177+
fn description(&self) -> &str {
178+
include_str!("append_to_file.prompt")
179+
}
180+
181+
fn example_payload(&self) -> Option<&str> {
182+
Some(
183+
r#"{
184+
"title": "Example title",
185+
"description": "Example description.",
186+
}"#,
187+
)
188+
}
189+
190+
async fn run(
191+
&self,
192+
_: SharedState,
193+
_: Option<HashMap<String, String>>,
194+
payload: Option<String>,
195+
) -> Result<Option<String>> {
196+
let payload = payload.unwrap();
197+
198+
let filepath = match get_variable("filesystem.append_to_file.target") {
199+
Some(filepath) => filepath,
200+
None => {
201+
log::warn!(
202+
"filesystem.append_to_file.target not defined, using default {}",
203+
DEFAULT_APPEND_TO_FILE_TARGET
204+
);
205+
DEFAULT_APPEND_TO_FILE_TARGET.to_string()
206+
}
207+
};
208+
209+
// parse the payload as a JSON object
210+
let one_line_json = if let Ok(value) = serde_json::from_str::<serde_json::Value>(&payload) {
211+
// reconvert to make sure it's on a single line
212+
serde_json::to_string(&value).unwrap()
213+
} else {
214+
log::error!("can't parse payload as JSON: {}", payload);
215+
serde_json::to_string(&InvalidJSON { data: payload }).unwrap()
216+
};
217+
218+
// append the JSON to the file
219+
let mut file = OpenOptions::new()
220+
.append(true)
221+
.create(true)
222+
.open(&filepath)?;
223+
224+
writeln!(file, "{}", one_line_json)?;
225+
226+
log::info!(
227+
"{}: appended {} bytes to {}",
228+
"filesystem.append_to_file".dimmed(),
229+
one_line_json.len(),
230+
filepath.bold()
231+
);
232+
233+
Ok(None)
234+
}
235+
}
236+
157237
pub fn get_namespace() -> Namespace {
158238
Namespace::new_non_default(
159239
"Filesystem".to_string(),
160240
include_str!("ns.prompt").to_string(),
161-
vec![Box::<ReadFile>::default(), Box::<ReadFolder>::default()],
241+
vec![
242+
Box::<ReadFile>::default(),
243+
Box::<ReadFolder>::default(),
244+
Box::<AppendToFile>::default(),
245+
],
162246
None,
163247
)
164248
}

0 commit comments

Comments
 (0)