Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ All configuration is via environment variables (or CLI flags — every env var h
| `--heartbeat-prompt` | `BUZZ_ACP_HEARTBEAT_PROMPT` | (built-in) | Custom heartbeat prompt text. Conflicts with `--heartbeat-prompt-file`. |
| `--heartbeat-prompt-file` | `BUZZ_ACP_HEARTBEAT_PROMPT_FILE` | — | Read heartbeat prompt from a file. Conflicts with `--heartbeat-prompt`. |

### Prompts

Every prompt the harness sends is assembled from a `[Base]` section (Buzz platform orientation, compiled in from `src/base_prompt.md`) followed by an optional `[System]` section (the agent's persona).

| Flag | Env Var | Default | Description |
|------|---------|---------|-------------|
| `--system-prompt` | `BUZZ_ACP_SYSTEM_PROMPT` | — | Persona `[System]` prompt text. Conflicts with `--system-prompt-file`. |
| `--system-prompt-file` | `BUZZ_ACP_SYSTEM_PROMPT_FILE` | — | Read the persona prompt from a file. Conflicts with `--system-prompt`. |
| `--base-prompt` | `BUZZ_ACP_BASE_PROMPT` | (built-in) | Replace the shipped `[Base]` prompt with custom text (max 1 MB). Conflicts with `--no-base-prompt` and `--base-prompt-file`. |
| `--base-prompt-file` | `BUZZ_ACP_BASE_PROMPT_FILE` | (built-in) | Replace the shipped `[Base]` prompt with the contents of a file (max 1 MB). Conflicts with `--no-base-prompt`. |
| `--no-base-prompt` | `BUZZ_ACP_NO_BASE_PROMPT` | `false` | Drop the `[Base]` section entirely — agents receive only the persona `[System]` prompt with no Buzz orientation. |

A custom base prompt fully replaces the shipped one — the harness does not merge them. Agents relying on the shipped orientation (CLI usage, mention rules, threading) lose it unless your replacement covers the same ground; start from [`src/base_prompt.md`](src/base_prompt.md) and edit.

### Inbound Author Gate

Controls which authors' events the harness forwards to the agent. Events from disallowed authors are silently dropped before reaching subscription rules.
Expand Down
89 changes: 89 additions & 0 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,15 @@ pub struct CliArgs {
#[arg(long, env = "BUZZ_ACP_NO_BASE_PROMPT")]
pub no_base_prompt: bool,

/// Custom base prompt text. Overrides the compiled-in default.
/// Mutually exclusive with --no-base-prompt and --base-prompt-file.
#[arg(
long,
env = "BUZZ_ACP_BASE_PROMPT",
conflicts_with_all = ["no_base_prompt", "base_prompt_file"]
)]
pub base_prompt: Option<String>,

/// Path to a custom base prompt file. Overrides the compiled-in default.
/// Mutually exclusive with --no-base-prompt.
#[arg(
Expand Down Expand Up @@ -909,6 +918,14 @@ impl Config {

let base_prompt_content = if args.no_base_prompt {
None
} else if let Some(text) = args.base_prompt {
if text.len() > 1_048_576 {
return Err(ConfigError::ConfigFile(format!(
"base prompt exceeds 1 MB limit ({} bytes)",
text.len()
)));
}
Some(text)
} else if let Some(ref path) = args.base_prompt_file {
let content = std::fs::read_to_string(path)?;
if content.len() > 1_048_576 {
Expand Down Expand Up @@ -2991,6 +3008,78 @@ channels = "ALL"
assert_eq!(compose_session_title(&agent, Some("buzz-dev")), agent);
}

// --- inline base prompt override ---

#[test]
fn base_prompt_inline_resolves_into_content() {
let args = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--base-prompt",
"custom orientation",
])
.expect("clap should parse args");
let config = Config::from_args(args).expect("config should build");

assert_eq!(
config.base_prompt_content.as_deref(),
Some("custom orientation")
);
assert!(!config.no_base_prompt);
}

#[test]
fn base_prompt_inline_conflicts_with_no_base_prompt() {
let result = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--base-prompt",
"custom orientation",
"--no-base-prompt",
]);
assert!(result.is_err(), "clap should reject the conflicting flags");
}

#[test]
fn base_prompt_inline_conflicts_with_base_prompt_file() {
let result = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--base-prompt",
"custom orientation",
"--base-prompt-file",
"/tmp/prompt.md",
]);
assert!(result.is_err(), "clap should reject the conflicting flags");
}

#[test]
fn base_prompt_inline_over_1mb_rejected() {
let oversized = "a".repeat(1_048_577);
let args = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--base-prompt",
&oversized,
])
.expect("clap should parse args");
let result = Config::from_args(args);

assert!(
result.is_err(),
"from_args should reject an oversized base prompt"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("1 MB limit"),
"error should mention the 1 MB limit: {msg}"
);
}

/// Every arg whose env var name contains KEY/SECRET/TOKEN/PASSWORD/CRED/AUTH
/// must set `hide_env_values = true` to prevent credential leakage in --help.
#[test]
Expand Down
Loading