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/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
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
|
## [v202611-00004] – 2026-11-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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="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="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">
|
<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.
|
- 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.
|
- 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.
|
- 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.
|
- Reference code as path:line.
|
||||||
- Answer in the language the user writes in."#;
|
- Answer in the language the user writes in."#;
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -44,6 +44,8 @@ tokio::task_local! {
|
|||||||
static SANDBOX: Option<crate::sandbox::SandboxSpec>;
|
static SANDBOX: Option<crate::sandbox::SandboxSpec>;
|
||||||
/// The user's git config (credentials, identity) for unsandboxed runs.
|
/// The user's git config (credentials, identity) for unsandboxed runs.
|
||||||
static GITCONFIG: Option<std::path::PathBuf>;
|
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> {
|
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() {
|
if let Some(cfg) = GITCONFIG.try_with(|g| g.clone()).ok().flatten() {
|
||||||
c.env("GIT_CONFIG_GLOBAL", cfg);
|
c.env("GIT_CONFIG_GLOBAL", cfg);
|
||||||
}
|
}
|
||||||
|
c.envs(GITENV.try_with(|e| e.clone()).unwrap_or_default());
|
||||||
c
|
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 {
|
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 spec = crate::sandbox::spec_for(&app, Some(&user.id));
|
||||||
let cfg = crate::gitcreds::global_config(&app, &user);
|
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 {
|
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> {
|
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 spec = crate::sandbox::spec_for(app, Some(&user.id));
|
||||||
let cfg = crate::gitcreds::global_config(app, user);
|
let cfg = crate::gitcreds::global_config(app, user);
|
||||||
|
let env = crate::gitcreds::identity_env(app, &user.id);
|
||||||
let s = session.clone();
|
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()) {
|
if let Some(cwd) = res.get("cwd").and_then(|v| v.as_str()) {
|
||||||
session.cwd = cwd.to_string();
|
session.cwd = cwd.to_string();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,24 @@ pub struct GitSettings {
|
|||||||
pub credentials: Vec<GitCredential>,
|
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 {
|
pub fn home_of(app: &App, user: &User) -> PathBuf {
|
||||||
app.data_dir().join("users").join(&user.username).join("home")
|
app.data_dir().join("users").join(&user.username).join("home")
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -18,6 +18,8 @@ pub struct SandboxSpec {
|
|||||||
pub roots: Vec<PathBuf>,
|
pub roots: Vec<PathBuf>,
|
||||||
/// Host directory mounted as the user's home.
|
/// Host directory mounted as the user's home.
|
||||||
pub home: PathBuf,
|
pub home: PathBuf,
|
||||||
|
/// Extra environment (the user's git identity).
|
||||||
|
pub env: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SandboxSpec {
|
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 home = app.data_dir().join("users").join(&user.username).join("home");
|
||||||
let _ = std::fs::create_dir_all(&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`.
|
/// 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]);
|
push(&[&"--setenv", &k, &v]);
|
||||||
}
|
}
|
||||||
|
for (k, v) in &spec.env {
|
||||||
|
push(&[&"--setenv", k, v]);
|
||||||
|
}
|
||||||
push(&[&"--chdir", &cwd, &program]);
|
push(&[&"--chdir", &cwd, &program]);
|
||||||
for a in args {
|
for a in args {
|
||||||
push(&[a]);
|
push(&[a]);
|
||||||
}
|
}
|
||||||
c
|
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..]))
|
pty_process::Command::new("bwrap").env_clear().args(crate::sandbox::bwrap_args(&spec, cwd, &[], false, shell[0], &shell[1..]))
|
||||||
}
|
}
|
||||||
None => {
|
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) {
|
match crate::gitcreds::global_config(app, user) {
|
||||||
Some(cfg) => c.env("GIT_CONFIG_GLOBAL", cfg),
|
Some(cfg) => c.env("GIT_CONFIG_GLOBAL", cfg),
|
||||||
None => c,
|
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");
|
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.
|
// 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());
|
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)) {
|
if let Some(u) = owner {
|
||||||
c.env("GIT_CONFIG_GLOBAL", cfg);
|
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
|
c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
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": {
|
"202611-00004": {
|
||||||
en: [
|
en: [
|
||||||
"Chats side by side: “Open beside” in a chat's menu in the sidebar",
|
"Chats side by side: “Open beside” in a chat's menu in the sidebar",
|
||||||
|
|||||||
Reference in new issue
Block a user