Compare commits
2
Commits
1253430ed4
...
c77dcf5890
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77dcf5890 | ||
|
|
9c80f2c857 |
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-00021] – 2026-11-11
|
||||
|
||||
### Added
|
||||
|
||||
- **Commits of agents name the model:** every `git commit` an agent makes gets the trailer
|
||||
`Co-Authored-By: <model> (<provider>) via CodeCraft` (a git wrapper in the agent shell, also in
|
||||
the sandbox; your own commits stay unchanged). The text is set under *Settings → General*
|
||||
(`server.commit_trailer`, `{model}`, `{provider}`, `{model_id}`; empty = off).
|
||||
|
||||
## [v202611-00020] – 2026-11-11
|
||||
|
||||
### Added
|
||||
|
||||
@@ -96,6 +96,8 @@ COPY --from=cli /out /app/cli
|
||||
# Used with a mounted /var/run/docker.sock (see docs/MOVING.md, --docker-sock).
|
||||
COPY --from=dockercli /docker /usr/local/bin/docker
|
||||
COPY server/ghidra /app/ghidra
|
||||
# git for agent shells: commits get the co-author trailer of the model (server.commit_trailer).
|
||||
COPY --chmod=755 docker/git-coauthor /usr/local/lib/codecraft/bin/git
|
||||
COPY server/resources /app/resources
|
||||
# The agent's home lives on the data volume: tools it installs in user space
|
||||
# (pip --user, pipx, rustup, npm -g, downloaded binaries) survive rebuilds.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Version v202611-00020" src="https://img.shields.io/badge/version-v202611--00020-d69e2e?style=flat-square">
|
||||
<img alt="Version v202611-00021" src="https://img.shields.io/badge/version-v202611--00021-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">
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# git for the shells of CodeCraft agents: `git commit` gets the co-author trailer from
|
||||
# $CODECRAFT_COMMIT_TRAILER (model, provider, CodeCraft). Everything else goes to the real
|
||||
# git unchanged. Git itself skips the trailer when the message already ends with it.
|
||||
real=${CODECRAFT_REAL_GIT:-/usr/bin/git}
|
||||
[[ -n $CODECRAFT_COMMIT_TRAILER ]] || exec "$real" "$@"
|
||||
# The subcommand comes after git's global options (-C dir, -c key=value, --git-dir=…).
|
||||
i=1
|
||||
while (( i <= $# )); do
|
||||
case ${!i} in
|
||||
-C | -c | --git-dir | --work-tree | --namespace | --exec-path | --config-env) i=$((i + 2)) ;;
|
||||
--) break ;;
|
||||
-*) i=$((i + 1)) ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
if (( i <= $# )) && [[ ${!i} == commit ]]; then
|
||||
# Right after "commit", so it stays an option even before "-- <paths>".
|
||||
exec "$real" "${@:1:i}" --trailer "$CODECRAFT_COMMIT_TRAILER" "${@:i+1}"
|
||||
fi
|
||||
exec "$real" "$@"
|
||||
@@ -307,8 +307,13 @@ pub struct ServerConfig {
|
||||
/// Bearer token for GET /metrics (Prometheus); empty = /metrics disabled.
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub metrics_token: String,
|
||||
/// Trailer added to every git commit an agent makes ({model}, {provider}, {model_id});
|
||||
/// empty = off.
|
||||
pub commit_trailer: String,
|
||||
}
|
||||
|
||||
pub const DEFAULT_COMMIT_TRAILER: &str = "Co-Authored-By: {model} ({provider}) via CodeCraft";
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -327,6 +332,7 @@ impl Default for ServerConfig {
|
||||
max_upload_mb: 500,
|
||||
update_url: "https://git.familie-zink.org/api/v1/repos/zink/codecraft/releases/latest".into(),
|
||||
metrics_token: String::new(),
|
||||
commit_trailer: DEFAULT_COMMIT_TRAILER.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ impl Tool for Bash {
|
||||
/// The bash command for a script, sandboxed for non-admin users.
|
||||
pub fn shell_command(ctx: &ToolCtx, command: &str) -> std::result::Result<Command, ToolOutput> {
|
||||
// stderr of every command (not just the last one) goes into the output.
|
||||
let script = format!("exec 2>&1\n{command}");
|
||||
let script = format!("exec 2>&1\n{}{command}", commit_attribution(ctx));
|
||||
Ok(match &ctx.sandbox {
|
||||
Some(spec) => {
|
||||
if !crate::sandbox::available() {
|
||||
@@ -161,6 +161,36 @@ pub fn shell_command(ctx: &ToolCtx, command: &str) -> std::result::Result<Comman
|
||||
})
|
||||
}
|
||||
|
||||
/// Directory with the git wrapper that adds the co-author trailer to commits.
|
||||
fn git_wrapper_dir() -> String {
|
||||
std::env::var("CODECRAFT_GIT_WRAPPER").unwrap_or_else(|_| "/usr/local/lib/codecraft/bin".into())
|
||||
}
|
||||
|
||||
/// Shell lines that make `git commit` in this command carry the co-author trailer of
|
||||
/// the session's model (see docker/git-coauthor). Set in the script itself, so it
|
||||
/// works inside the sandbox (which clears the environment) too.
|
||||
fn commit_attribution(ctx: &ToolCtx) -> String {
|
||||
let template = ctx.app.config().server.commit_trailer.clone();
|
||||
let dir = git_wrapper_dir();
|
||||
if template.trim().is_empty() || !std::path::Path::new(&dir).join("git").exists() {
|
||||
return String::new();
|
||||
}
|
||||
let Ok(session) = ctx.app.session(&ctx.session_id) else { return String::new() };
|
||||
let (provider, model_id) = session.model.split_once('/').unwrap_or(("", session.model.as_str()));
|
||||
let name = ctx.app.registry().resolve(&session.model).ok().and_then(|(_, m)| m.name).filter(|n| !n.trim().is_empty()).unwrap_or_else(|| model_id.to_string());
|
||||
let trailer = commit_trailer(&template, &name, provider, model_id);
|
||||
format!("export PATH={}:\"$PATH\" CODECRAFT_COMMIT_TRAILER={}\n", shell_quote(&dir), shell_quote(&trailer))
|
||||
}
|
||||
|
||||
/// The trailer text for a model ("Co-Authored-By: DeepSeek V4.1 Flash (opencode-go) via CodeCraft").
|
||||
pub fn commit_trailer(template: &str, model: &str, provider: &str, model_id: &str) -> String {
|
||||
template.replace("{model}", model).replace("{provider}", provider).replace("{model_id}", model_id).replace(['\n', '\r'], " ").trim().to_string()
|
||||
}
|
||||
|
||||
fn shell_quote(s: &str) -> String {
|
||||
format!("'{}'", s.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
fn tail(s: &str, max: usize) -> String {
|
||||
if s.len() <= max {
|
||||
return s.to_string();
|
||||
@@ -171,3 +201,18 @@ fn tail(s: &str, max: usize) -> String {
|
||||
}
|
||||
s[start..].to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod commit_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn commit_trailer_text() {
|
||||
assert_eq!(
|
||||
commit_trailer(crate::config::DEFAULT_COMMIT_TRAILER, "DeepSeek V4.1 Flash", "opencode-go", "deepseek-v4.1-flash"),
|
||||
"Co-Authored-By: DeepSeek V4.1 Flash (opencode-go) via CodeCraft"
|
||||
);
|
||||
assert_eq!(commit_trailer("By {model_id}\nx", "M", "p", "m-1"), "By m-1 x");
|
||||
assert_eq!(shell_quote("it's"), "'it'\\''s'");
|
||||
}
|
||||
}
|
||||
@@ -248,6 +248,7 @@ function General({ cfg, reload }: { cfg: ConfigResponse; reload: () => void }) {
|
||||
small_model: s.small_model ?? "",
|
||||
currency: s.currency ?? "USD",
|
||||
auto_compact_threshold: s.auto_compact_threshold ?? 0.85,
|
||||
commit_trailer: ((s as any).commit_trailer ?? "Co-Authored-By: {model} ({provider}) via CodeCraft") as string,
|
||||
max_parallel_subagents: s.max_parallel_subagents ?? 4,
|
||||
monthly_budget: s.monthly_budget ?? 0,
|
||||
budget_warning: s.budget_warning ?? 0.8,
|
||||
@@ -271,6 +272,7 @@ function General({ cfg, reload }: { cfg: ConfigResponse; reload: () => void }) {
|
||||
{ path: ["server", "small_model"], value: form.small_model },
|
||||
{ path: ["server", "currency"], value: form.currency },
|
||||
{ path: ["server", "auto_compact_threshold"], value: Number(form.auto_compact_threshold) },
|
||||
{ path: ["server", "commit_trailer"], value: form.commit_trailer.trim() },
|
||||
{ path: ["server", "max_parallel_subagents"], value: Number(form.max_parallel_subagents) },
|
||||
{ path: ["server", "monthly_budget"], value: Number(form.monthly_budget) || 0 },
|
||||
{ path: ["server", "budget_warning"], value: Number(form.budget_warning) || 0.8 },
|
||||
@@ -305,6 +307,9 @@ function General({ cfg, reload }: { cfg: ConfigResponse; reload: () => void }) {
|
||||
<label>{t("settings.autoCompact")}</label>
|
||||
<input type="number" min={0} max={1} step={0.05} value={form.auto_compact_threshold} onChange={(e) => setForm({ ...form, auto_compact_threshold: e.target.value })} />
|
||||
<div className="hint">{t("settings.autoCompactHint")}</div>
|
||||
<label>{t("settings.commitTrailer")}</label>
|
||||
<input className="mono" value={form.commit_trailer} placeholder={t("settings.commitTrailerOff")} onChange={(e) => setForm({ ...form, commit_trailer: e.target.value })} />
|
||||
<div className="hint">{t("settings.commitTrailerHint")}</div>
|
||||
<label>{t("settings.monthlyBudgetTotal")}</label>
|
||||
<input type="number" min={0} value={form.monthly_budget} onChange={(e) => setForm({ ...form, monthly_budget: e.target.value })} />
|
||||
<div className="hint">{t("settings.monthlyBudgetHint")}</div>
|
||||
|
||||
@@ -770,6 +770,9 @@ export const de: Messages = {
|
||||
"settings.smallModelHint": "Für Session-Titel. Die Komprimierung nutzt das Session-Modell.",
|
||||
"settings.currency": "Währung",
|
||||
"settings.autoCompact": "Auto-Komprimierung bei",
|
||||
"settings.commitTrailer": "Commit-Vermerk der Agenten",
|
||||
"settings.commitTrailerOff": "aus",
|
||||
"settings.commitTrailerHint": "Wird als Trailer an jeden Git-Commit eines Agenten angehängt. {model} = Modellname, {provider} = Anbieter, {model_id} = Modell-ID. Leer = aus. Deine eigenen Commits bleiben unverändert.",
|
||||
"settings.autoCompactHint": "Anteil des Kontextfensters (0 = aus).",
|
||||
"settings.craft": "Craft-Modus",
|
||||
"settings.cleanRoom": "Clean Room",
|
||||
|
||||
@@ -769,6 +769,9 @@ export const en = {
|
||||
"settings.smallModelHint": "Used for session titles. Compaction uses the session model.",
|
||||
"settings.currency": "Currency",
|
||||
"settings.autoCompact": "Auto-compact at",
|
||||
"settings.commitTrailer": "Commit note of agents",
|
||||
"settings.commitTrailerOff": "off",
|
||||
"settings.commitTrailerHint": "Added as a trailer to every git commit an agent makes. {model} = model name, {provider} = provider, {model_id} = model id. Empty = off. Your own commits stay unchanged.",
|
||||
"settings.autoCompactHint": "Fraction of the context window (0 = off).",
|
||||
"settings.craft": "Craft mode",
|
||||
"settings.cleanRoom": "Clean room",
|
||||
|
||||
@@ -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-00021": {
|
||||
en: ["Git commits of agents name the model: “Co-Authored-By: <model> (<provider>) via CodeCraft” (Settings → General)"],
|
||||
de: ["Git-Commits von Agenten nennen das Modell: „Co-Authored-By: <Modell> (<Anbieter>) via CodeCraft“ (Einstellungen → Allgemein)"],
|
||||
},
|
||||
"202611-00020": {
|
||||
en: ["Reverse engineering: decompile with Ghidra can be enabled for any mode (Settings → Agents & commands), now also for raw firmware"],
|
||||
de: ["Reverse Engineering: Dekompilieren mit Ghidra lässt sich für jeden Modus einschalten (Einstellungen → Agenten & Befehle), jetzt auch für rohe Firmware"],
|
||||
|
||||
Reference in new issue
Block a user