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

+7 -1
View File
@@ -42,6 +42,8 @@ const SAFE_OPTS: &[&str] = &["-c", "core.hooksPath=/dev/null", "-c", "core.fsmon
tokio::task_local! {
/// Sandbox of the requesting user for all git calls of this request.
static SANDBOX: Option<crate::sandbox::SandboxSpec>;
/// The user's git config (credentials, identity) for unsandboxed runs.
static GITCONFIG: Option<std::path::PathBuf>;
}
async fn git(dir: &Path, args: &[&str], timeout: u64) -> Result<Out, ApiError> {
@@ -58,6 +60,9 @@ async fn git(dir: &Path, args: &[&str], timeout: u64) -> Result<Out, ApiError> {
None => {
let mut c = Command::new("git");
c.args(&all).current_dir(dir).env("GIT_TERMINAL_PROMPT", "0").env("GIT_PAGER", "cat").env("LC_ALL", "C");
if let Some(cfg) = GITCONFIG.try_with(|g| g.clone()).ok().flatten() {
c.env("GIT_CONFIG_GLOBAL", cfg);
}
c
}
};
@@ -165,7 +170,8 @@ fn parse_status(raw: &str) -> Value {
pub async fn handle(Extension(user): Extension<User>, State(app): State<AppState>, Json(b): Json<GitBody>) -> ApiResult {
let spec = crate::sandbox::spec_for(&app, Some(&user.id));
SANDBOX.scope(spec, handle_inner(user, app, b)).await
let cfg = crate::gitcreds::global_config(&app, &user);
SANDBOX.scope(spec, GITCONFIG.scope(cfg, handle_inner(user, app, b))).await
}
async fn handle_inner(user: User, app: AppState, b: GitBody) -> ApiResult {