Add web terminal, git credentials, OpenSCAD, review loops, automations

- Web terminal (PTY over WebSocket, xterm.js, sandboxed, reattach with replay)
- Per-user git credentials and commit identity (credential store in the user's home)
- OpenSCAD tool (STL/3MF + preview renders, bounding box) and cad skill; 3D model card
- Craft phase inferred from tool activity when the agent does not report it
- Configurable review loop per mode (rounds, reviewer model, criteria)
- Delete config and Markdown agents from the agent editor
- Automations: cron-scheduled agents from Markdown job files, schedule tool, page
- change_directory tool (only on the user's request)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 22:00:38 +01:00
1 parent a0d0e707f2
commit df1924b0a1
41 files changed
+2809 -34

No files matched your search

+37 -15
View File
@@ -82,36 +82,56 @@ pub fn command_with(spec: &SandboxSpec, cwd: &Path, extra_ro: &[&Path], program:
let mut c = Command::new("bwrap");
// Empty environment: bubblewrap's own helper process must not carry secrets.
c.env_clear();
c.args(bwrap_args(spec, cwd, extra_ro, true, program, args));
c
}
/// Arguments for `bwrap` running `program args` in the sandbox. `new_session`
/// detaches from the controlling terminal (protects the caller's tty); a
/// terminal with its own PTY leaves it off to keep job control.
pub fn bwrap_args(spec: &SandboxSpec, cwd: &Path, extra_ro: &[&Path], new_session: bool, program: &str, args: &[&str]) -> Vec<std::ffi::OsString> {
let mut c: Vec<std::ffi::OsString> = Vec::new();
let mut push = |items: &[&dyn AsRef<std::ffi::OsStr>]| {
for i in items {
c.push(i.as_ref().to_os_string());
}
};
for dir in ["/usr", "/bin", "/lib", "/etc", "/sbin", "/opt"] {
if Path::new(dir).is_dir() && !Path::new(dir).is_symlink() {
c.args(["--ro-bind", dir, dir]);
push(&[&"--ro-bind", &dir, &dir]);
} else if let Ok(target) = std::fs::read_link(dir) {
c.arg("--symlink").arg(target).arg(dir);
push(&[&"--symlink", &target, &dir]);
}
}
for dir in ["/lib64", "/lib32"] {
let p = Path::new(dir);
if let Ok(target) = std::fs::read_link(p) {
c.arg("--symlink").arg(target).arg(dir);
push(&[&"--symlink", &target, &dir]);
} else if p.is_dir() {
c.args(["--ro-bind", dir, dir]);
push(&[&"--ro-bind", &dir, &dir]);
}
}
for root in &spec.roots {
c.arg("--bind").arg(root).arg(root);
push(&[&"--bind", root, root]);
}
// Built-in skills and templates are readable (not writable) in the sandbox.
let builtin = crate::prompts::builtin_resources();
for dir in extra_ro.iter().copied().chain(std::iter::once(builtin.as_path())) {
if dir.exists() {
c.arg("--ro-bind").arg(dir).arg(dir);
push(&[&"--ro-bind", &dir, &dir]);
}
}
let home = spec.home_inside();
c.arg("--bind").arg(&spec.home).arg(&home);
c.args(["--tmpfs", "/tmp", "--dev", "/dev", "--proc", "/proc"]);
c.args(["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--clearenv"]);
push(&[&"--bind", &spec.home, &home]);
push(&[&"--tmpfs", &"/tmp", &"--dev", &"/dev", &"--proc", &"/proc"]);
push(&[&"--unshare-all", &"--share-net", &"--die-with-parent", &"--clearenv"]);
if new_session {
push(&[&"--new-session"]);
}
let path = format!("{home}/.local/bin:{home}/.cargo/bin:{home}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin");
let npm = format!("{home}/.npm-global");
let pipx = format!("{home}/.local/pipx");
let pipx_bin = format!("{home}/.local/bin");
for (k, v) in [
("HOME", home.as_str()),
("USER", spec.username.as_str()),
@@ -122,13 +142,15 @@ pub fn command_with(spec: &SandboxSpec, cwd: &Path, extra_ro: &[&Path], program:
("GIT_TERMINAL_PROMPT", "0"),
("PAGER", "cat"),
("PIP_BREAK_SYSTEM_PACKAGES", "1"),
("NPM_CONFIG_PREFIX", npm.as_str()),
("PIPX_HOME", pipx.as_str()),
("PIPX_BIN_DIR", pipx_bin.as_str()),
] {
c.args(["--setenv", k, v]);
push(&[&"--setenv", &k, &v]);
}
push(&[&"--chdir", &cwd, &program]);
for a in args {
push(&[a]);
}
c.arg("--setenv").arg("NPM_CONFIG_PREFIX").arg(format!("{home}/.npm-global"));
c.arg("--setenv").arg("PIPX_HOME").arg(format!("{home}/.local/pipx"));
c.arg("--setenv").arg("PIPX_BIN_DIR").arg(format!("{home}/.local/bin"));
c.arg("--chdir").arg(cwd);
c.arg(program).args(args);
c
}