Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b1d7f91b8 | ||
|
|
0f4bfa0ab0 |
No files matched your search
@@ -3,6 +3,15 @@
|
||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||
until 1.0, minor versions may contain breaking changes.
|
||||
|
||||
## [v202611-00005] – 2026-11-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Commits by agents, terminals and the git panel always use the name and e-mail from
|
||||
*Settings → Account → Git* – also in repositories that have their own `user.name`/`user.email`
|
||||
(agents had set those as a workaround while no identity was configured). Agents are told not
|
||||
to change the git identity.
|
||||
|
||||
## [v202611-00004] – 2026-11-09
|
||||
|
||||
### Added
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Version v202611-00004" src="https://img.shields.io/badge/version-v202611--00004-d69e2e?style=flat-square">
|
||||
<img alt="Version v202611-00005" src="https://img.shields.io/badge/version-v202611--00005-d69e2e?style=flat-square">
|
||||
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
||||
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
||||
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
||||
|
||||
@@ -14,6 +14,7 @@ pub const BASE_PROMPT: &str = r#"You are CodeCraft, an autonomous coding agent w
|
||||
- Use the task tool for broad searches or independent sub-tasks to keep your own context focused.
|
||||
- If you are blocked on a decision that is genuinely the user's, use ask_user. Otherwise pick a sensible default and mention it.
|
||||
- Never commit, push or perform destructive/irreversible actions unless the user asked for it.
|
||||
- Git identity and credentials come from the user's settings: never set user.name, user.email or credential helpers yourself (no `git config user.*`, no `-c user.*`).
|
||||
- Reference code as path:line.
|
||||
- Answer in the language the user writes in."#;
|
||||
|
||||
|
||||
+7
-2
@@ -44,6 +44,8 @@ tokio::task_local! {
|
||||
static SANDBOX: Option<crate::sandbox::SandboxSpec>;
|
||||
/// The user's git config (credentials, identity) for unsandboxed runs.
|
||||
static GITCONFIG: Option<std::path::PathBuf>;
|
||||
/// The user's commit identity (overrides a repository's own user.name/email).
|
||||
static GITENV: Vec<(String, String)>;
|
||||
}
|
||||
|
||||
async fn git(dir: &Path, args: &[&str], timeout: u64) -> Result<Out, ApiError> {
|
||||
@@ -63,6 +65,7 @@ async fn git(dir: &Path, args: &[&str], timeout: u64) -> Result<Out, ApiError> {
|
||||
if let Some(cfg) = GITCONFIG.try_with(|g| g.clone()).ok().flatten() {
|
||||
c.env("GIT_CONFIG_GLOBAL", cfg);
|
||||
}
|
||||
c.envs(GITENV.try_with(|e| e.clone()).unwrap_or_default());
|
||||
c
|
||||
}
|
||||
};
|
||||
@@ -171,7 +174,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));
|
||||
let cfg = crate::gitcreds::global_config(&app, &user);
|
||||
SANDBOX.scope(spec, GITCONFIG.scope(cfg, handle_inner(user, app, b))).await
|
||||
let env = crate::gitcreds::identity_env(&app, &user.id);
|
||||
SANDBOX.scope(spec, GITCONFIG.scope(cfg, GITENV.scope(env, handle_inner(user, app, b)))).await
|
||||
}
|
||||
|
||||
async fn handle_inner(user: User, app: AppState, b: GitBody) -> ApiResult {
|
||||
@@ -388,8 +392,9 @@ fn wt_str<'a>(wt: &'a Value, key: &str) -> &'a str {
|
||||
pub async fn worktree(app: &std::sync::Arc<App>, user: &User, session: &mut crate::types::Session, action: &str) -> Result<Value, ApiError> {
|
||||
let spec = crate::sandbox::spec_for(app, Some(&user.id));
|
||||
let cfg = crate::gitcreds::global_config(app, user);
|
||||
let env = crate::gitcreds::identity_env(app, &user.id);
|
||||
let s = session.clone();
|
||||
let res = SANDBOX.scope(spec, GITCONFIG.scope(cfg, worktree_inner(s, action))).await?;
|
||||
let res = SANDBOX.scope(spec, GITCONFIG.scope(cfg, GITENV.scope(env, worktree_inner(s, action)))).await?;
|
||||
if let Some(cwd) = res.get("cwd").and_then(|v| v.as_str()) {
|
||||
session.cwd = cwd.to_string();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,24 @@ pub struct GitSettings {
|
||||
pub credentials: Vec<GitCredential>,
|
||||
}
|
||||
|
||||
/// The user's commit identity as environment variables. They take precedence over a
|
||||
/// repository's own `user.name`/`user.email`, so commits made by agents, terminals and
|
||||
/// the git panel always carry the identity from the settings.
|
||||
pub fn identity_env(app: &App, user_id: &str) -> Vec<(String, String)> {
|
||||
let s: GitSettings = app.store.user_setting(user_id, "git").unwrap_or_default();
|
||||
let mut env = Vec::new();
|
||||
let (name, email) = (s.name.trim(), s.email.trim());
|
||||
if !name.is_empty() {
|
||||
env.push(("GIT_AUTHOR_NAME".to_string(), name.to_string()));
|
||||
env.push(("GIT_COMMITTER_NAME".to_string(), name.to_string()));
|
||||
}
|
||||
if !email.is_empty() {
|
||||
env.push(("GIT_AUTHOR_EMAIL".to_string(), email.to_string()));
|
||||
env.push(("GIT_COMMITTER_EMAIL".to_string(), email.to_string()));
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
pub fn home_of(app: &App, user: &User) -> PathBuf {
|
||||
app.data_dir().join("users").join(&user.username).join("home")
|
||||
}
|
||||
|
||||
+25
-1
@@ -18,6 +18,8 @@ pub struct SandboxSpec {
|
||||
pub roots: Vec<PathBuf>,
|
||||
/// Host directory mounted as the user's home.
|
||||
pub home: PathBuf,
|
||||
/// Extra environment (the user's git identity).
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl SandboxSpec {
|
||||
@@ -69,7 +71,7 @@ pub fn spec_for(app: &App, owner_id: Option<&str>) -> Option<SandboxSpec> {
|
||||
}
|
||||
let home = app.data_dir().join("users").join(&user.username).join("home");
|
||||
let _ = std::fs::create_dir_all(&home);
|
||||
Some(SandboxSpec { username: user.username.clone(), roots: app.user_roots(Some(&user.id)), home })
|
||||
Some(SandboxSpec { username: user.username.clone(), roots: app.user_roots(Some(&user.id)), home, env: crate::gitcreds::identity_env(app, &user.id) })
|
||||
}
|
||||
|
||||
/// Build a command that runs `program args` inside the sandbox in `cwd`.
|
||||
@@ -148,9 +150,31 @@ pub fn bwrap_args(spec: &SandboxSpec, cwd: &Path, extra_ro: &[&Path], new_sessio
|
||||
] {
|
||||
push(&[&"--setenv", &k, &v]);
|
||||
}
|
||||
for (k, v) in &spec.env {
|
||||
push(&[&"--setenv", k, v]);
|
||||
}
|
||||
push(&[&"--chdir", &cwd, &program]);
|
||||
for a in args {
|
||||
push(&[a]);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn git_identity_reaches_the_sandbox() {
|
||||
let spec = SandboxSpec {
|
||||
username: "ann".into(),
|
||||
roots: vec![],
|
||||
home: PathBuf::from("/tmp/ann"),
|
||||
env: vec![("GIT_AUTHOR_NAME".into(), "Ann Example".into())],
|
||||
};
|
||||
let args: Vec<String> = bwrap_args(&spec, Path::new("/tmp"), &[], true, "bash", &[]).into_iter().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
let i = args.iter().position(|a| a == "GIT_AUTHOR_NAME").expect("identity set");
|
||||
assert_eq!(args[i - 1], "--setenv");
|
||||
assert_eq!(args[i + 1], "Ann Example");
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,12 @@ pub fn spawn(app: &Arc<App>, user: &User, cwd: &Path, session_id: Option<String>
|
||||
pty_process::Command::new("bwrap").env_clear().args(crate::sandbox::bwrap_args(&spec, cwd, &[], false, shell[0], &shell[1..]))
|
||||
}
|
||||
None => {
|
||||
let c = pty_process::Command::new(shell[0]).args(&shell[1..]).current_dir(cwd).env("TERM", "xterm-256color").env("CODECRAFT", "1");
|
||||
let c = pty_process::Command::new(shell[0])
|
||||
.args(&shell[1..])
|
||||
.current_dir(cwd)
|
||||
.env("TERM", "xterm-256color")
|
||||
.env("CODECRAFT", "1")
|
||||
.envs(crate::gitcreds::identity_env(app, &user.id));
|
||||
match crate::gitcreds::global_config(app, user) {
|
||||
Some(cfg) => c.env("GIT_CONFIG_GLOBAL", cfg),
|
||||
None => c,
|
||||
|
||||
@@ -150,8 +150,11 @@ pub fn shell_command(ctx: &ToolCtx, command: &str) -> std::result::Result<Comman
|
||||
c.arg("-c").arg(&script).current_dir(&ctx.cwd).env("CODECRAFT", "1").env("GIT_TERMINAL_PROMPT", "0").env("PAGER", "cat");
|
||||
// The session owner's git credentials and identity.
|
||||
let owner = ctx.app.session(&ctx.session_id).ok().and_then(|s| s.owner_id).and_then(|id| ctx.app.store.user(&id).ok().flatten());
|
||||
if let Some(cfg) = owner.and_then(|u| crate::gitcreds::global_config(&ctx.app, &u)) {
|
||||
c.env("GIT_CONFIG_GLOBAL", cfg);
|
||||
if let Some(u) = owner {
|
||||
if let Some(cfg) = crate::gitcreds::global_config(&ctx.app, &u) {
|
||||
c.env("GIT_CONFIG_GLOBAL", cfg);
|
||||
}
|
||||
c.envs(crate::gitcreds::identity_env(&ctx.app, &u.id));
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||
*/
|
||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||
"202611-00005": {
|
||||
en: ["Agents commit with the git name and e-mail from your settings – also in repositories with their own identity"],
|
||||
de: ["Agents committen mit Git-Name und -E-Mail aus deinen Einstellungen – auch in Repos mit eigener Identität"],
|
||||
},
|
||||
"202611-00004": {
|
||||
en: [
|
||||
"Chats side by side: “Open beside” in a chat's menu in the sidebar",
|
||||
|
||||
Reference in new issue
Block a user