#1163: file.list lists a folder for agents, held to the read root
An agent driving VectorCraft over the confined MCP server had no way to
see which files it was given, so it guessed names. `file.list {path}`
lists a folder's files (with their sizes) and folders by name, held to
the automation read root like every read; links are listed as links and
not followed. At most 5000 entries, with `truncated` saying when there
were more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6d07135456
commit
339b01f48b
File diff suppressed because one or more lines are too long
|
|
@ -87,6 +87,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
always,
|
||||
load::open
|
||||
),
|
||||
cmd!(
|
||||
query "file.list",
|
||||
"List Folder",
|
||||
[],
|
||||
None,
|
||||
"{path} the files and folders in folder `path`, by name: what an agent may open there (held to the automation read root when one is in force) → {path, entries: [{name, kind: file|folder|link, size? (files, bytes)}], truncated (more than 5000 entries)} (#1163)",
|
||||
always,
|
||||
file_list
|
||||
),
|
||||
cmd!(
|
||||
query "document.serialize",
|
||||
"Serialize Document",
|
||||
|
|
@ -783,6 +792,48 @@ pub(crate) fn write_new_file(path: &str, bytes: &[u8]) -> Result<()> {
|
|||
})
|
||||
}
|
||||
|
||||
/// The most entries `file.list` returns.
|
||||
const LIST_MAX: usize = 5000;
|
||||
|
||||
fn file_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p.get("path").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or_else(|| bad("file.list", "missing `path`"))?;
|
||||
let (entries, truncated) = list_dir(path, LIST_MAX)?;
|
||||
Ok(json!({ "path": absolute_path(path), "entries": entries, "truncated": truncated }))
|
||||
}
|
||||
|
||||
/// The entries of folder `path`, sorted by name: at most `max`, and whether there were more. A
|
||||
/// link isn't followed: its target may lie outside the automation roots.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn list_dir(path: &str, max: usize) -> Result<(Vec<Value>, bool)> {
|
||||
may_read(path)?;
|
||||
let dir = std::fs::read_dir(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let mut entries: Vec<(String, Value)> = vec![];
|
||||
let mut truncated = false;
|
||||
for e in dir.flatten() {
|
||||
if entries.len() == max {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
let name = e.file_name().to_string_lossy().into_owned();
|
||||
let Ok(t) = e.file_type() else { continue };
|
||||
let entry = if t.is_symlink() {
|
||||
json!({ "name": name, "kind": "link" })
|
||||
} else if t.is_dir() {
|
||||
json!({ "name": name, "kind": "folder" })
|
||||
} else {
|
||||
json!({ "name": name, "kind": "file", "size": e.metadata().map(|m| m.len()).ok() })
|
||||
};
|
||||
entries.push((name, entry));
|
||||
}
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok((entries.into_iter().map(|(_, v)| v).collect(), truncated))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn list_dir(_: &str, _: usize) -> Result<(Vec<Value>, bool)> {
|
||||
Err(EngineError::Other("no file system here".into()))
|
||||
}
|
||||
|
||||
/// A file's size (bytes) and modification time (ms since the Unix epoch, when the file system
|
||||
/// keeps one); `None` when there is no file at `path` (or automation may not read it).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
|
|
@ -1670,3 +1670,32 @@ fn path_commands_reject_bad_anchors_before_modifying_geometry() {
|
|||
s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": [{"x": 0, "y": 0}, {"x": 30, "y": 40}], "closed": false}]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 2);
|
||||
}
|
||||
|
||||
/// #1163: `file.list` lists a folder's files and folders by name, held to the automation read root
|
||||
/// when one is in force.
|
||||
#[test]
|
||||
fn file_list_lists_a_folder_within_the_read_root() {
|
||||
let base = std::env::temp_dir().join(format!("vc-file-list-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
for d in ["in/sub", "outside"] {
|
||||
std::fs::create_dir_all(base.join(d)).unwrap();
|
||||
}
|
||||
std::fs::write(base.join("in/b.svg"), "<svg/>").unwrap();
|
||||
std::fs::write(base.join("in/a.pdf"), "1234").unwrap();
|
||||
let path = |d: &str| base.join(d).to_string_lossy().into_owned();
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.list", &json!({"path": path("in")})).unwrap();
|
||||
assert_eq!(
|
||||
r["entries"],
|
||||
json!([{"name": "a.pdf", "kind": "file", "size": 4}, {"name": "b.svg", "kind": "file", "size": 6}, {"name": "sub", "kind": "folder"}])
|
||||
);
|
||||
assert_eq!(r["truncated"], false);
|
||||
let roots = crate::file_access::AutomationRoots::new(Some(&base.join("in")), None).unwrap().unwrap();
|
||||
crate::file_access::confine(Some(&roots), || {
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/sub")})).is_ok());
|
||||
assert!(s.execute("file.list", &json!({"path": path("outside")})).is_err(), "outside the read root");
|
||||
});
|
||||
assert!(s.execute("file.list", &json!({"path": path("in/b.svg")})).is_err(), "not a folder");
|
||||
assert!(s.execute("file.list", &json!({})).is_err());
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2101,6 +2101,7 @@
|
|||
Redefine Symbol Symbol neu definieren
|
||||
Redefine {kind} Style {kind}format neu definieren
|
||||
Redo Wiederholen
|
||||
List Folder Ordner auflisten
|
||||
Begin Undo Group Rückgängig-Gruppe beginnen
|
||||
End Undo Group Rückgängig-Gruppe beenden
|
||||
Redo {name} Wiederholen: {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2152,6 +2152,7 @@
|
|||
Redefine Symbol Redefinir símbolo
|
||||
Redefine {kind} Style Redefinir estilo de {kind}
|
||||
Redo Rehacer
|
||||
List Folder Listar carpeta
|
||||
Begin Undo Group Iniciar grupo de deshacer
|
||||
End Undo Group Finalizar grupo de deshacer
|
||||
Redo {name} Rehacer {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2149,6 +2149,7 @@
|
|||
Redefine Symbol Redéfinir le symbole
|
||||
Redefine {kind} Style Redéfinir le style de {kind}
|
||||
Redo Rétablir
|
||||
List Folder Lister le dossier
|
||||
Begin Undo Group Commencer un groupe d’annulation
|
||||
End Undo Group Terminer le groupe d’annulation
|
||||
Redo {name} Rétablir {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2152,6 +2152,7 @@
|
|||
Redefine Symbol Ridefinisci simbolo
|
||||
Redefine {kind} Style Ridefinisci stile {kind}
|
||||
Redo Ripeti
|
||||
List Folder Elenca cartella
|
||||
Begin Undo Group Inizia gruppo di annullamento
|
||||
End Undo Group Termina gruppo di annullamento
|
||||
Redo {name} Ripeti {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -101,6 +101,7 @@
|
|||
Edit 編集
|
||||
Undo 取り消し
|
||||
Redo やり直し
|
||||
List Folder フォルダーの内容を一覧表示
|
||||
Begin Undo Group 取り消しグループを開始
|
||||
End Undo Group 取り消しグループを終了
|
||||
Cut 切り取り
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2148,6 +2148,7 @@
|
|||
Redefine Symbol Переопределить символ
|
||||
Redefine {kind} Style Переопределить стиль {kind}
|
||||
Redo Повторить
|
||||
List Folder Показать содержимое папки
|
||||
Begin Undo Group Начать группу отмены
|
||||
End Undo Group Завершить группу отмены
|
||||
Redo {name} Повторить {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2144,6 +2144,7 @@
|
|||
Redefine Symbol Перевизначити символ
|
||||
Redefine {kind} Style Перевизначити стиль {kind}
|
||||
Redo Повторити
|
||||
List Folder Показати вміст теки
|
||||
Begin Undo Group Почати групу скасування
|
||||
End Undo Group Завершити групу скасування
|
||||
Redo {name} Повторити {name}
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -2077,6 +2077,7 @@
|
|||
Redefine Symbol 重新定义符号
|
||||
Redefine {kind} Style 重新定义{kind}样式
|
||||
Redo 重做
|
||||
List Folder 列出文件夹
|
||||
Begin Undo Group 开始还原组
|
||||
End Undo Group 结束还原组
|
||||
Redo {name} 重做 {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2130,6 +2130,7 @@
|
|||
Redefine Symbol 重新定義符號
|
||||
Redefine {kind} Style 重新定義{kind}樣式
|
||||
Redo 重做
|
||||
List Folder 列出資料夾
|
||||
Begin Undo Group 開始還原群組
|
||||
End Undo Group 結束還原群組
|
||||
Redo {name} 重做 {name}
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -69,6 +69,7 @@ the suite) keep the agent to the folders you give it:
|
|||
- Every file a command writes must lie inside the **write root**: `save_file`, `export`, the save and
|
||||
export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`,
|
||||
library and preset saves to a path.
|
||||
- `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read root like any read, so an agent can see what it was given (#1163).
|
||||
- Read and write access are separate. Give the same folder twice for both; a root left out grants none
|
||||
of its access (with only `--automation-read-root`, nothing can be written to disk, though `export` and
|
||||
the save commands without a path still return the bytes). Without either flag nothing changes.
|
||||
|
|
|
|||
Loading…
Reference in New Issue