Skip to content
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ deny_hosts = ["api.stashbase.dev"]

Stashbase will warn when starting this mode.

#### Config environment variables

Pass non-sensitive config to the agent with `[config_env]`. The values are plaintext and work with both sandbox backends:

```toml
[config_env]
NODE_ENV = "development"
RUST_LOG = "info"
```

Set or override one for a single run with `--config-env NAME=VALUE`. Values that look like credentials are flagged: `agent run` asks before starting and fails when it can't ask. Credentials belong in `[secrets]`. See [Config Environment Variables](docs/agent-profiles.md#config-environment-variables) for the rules and an [example profile](docs/agent-profiles/examples/config-env.toml).

### Filesystem and Network Containment

#### Read and write restrictions
Expand Down
34 changes: 34 additions & 0 deletions docs/agent-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,40 @@ worktree = true

Or per run: `stashbase agent run --profile coding --worktree -- claude` (`--worktree=false` skips it when the profile turns it on). Works with the native backend and, on macOS/Linux, the Docker backend. Review and merge the agent's work from your checkout with `stashbase agent worktrees list`, `merge <name>` and `clean` — see **[Worktrees](sandboxing.md#worktrees)**.

## Config Environment Variables

Set non-sensitive configuration for the agent with `[config_env]`. Values are stored in plaintext in the profile, and both sandbox backends apply them. The Docker backend otherwise starts the container with an empty environment. A value can be a string, a number or a boolean (`PORT = 3000` and `DEBUG = true` work without quotes), and the agent always receives it as a string.

```toml
[config_env]
NODE_ENV = "development"
RUST_LOG = "info"
DATABASE_URL = "postgres://app@localhost:5432/app"
```

- `[config_env]` values take precedence over variables inherited from your shell. They can't replace anything the sandbox sets: proxy and CA variables, `HOME`, `TMPDIR`, `FORCE_COLOR` and anything starting with `STASHBASE_` are rejected. `PATH`, `LD_*` and `DYLD_*` are rejected too, because they control which programs and libraries start the sandbox.
- Each `NAME=value` can be up to 64 KiB, and the whole table up to 256 KiB. That keeps `[config_env]` well within what the operating system accepts when starting the agent. Values can't contain NUL bytes. Keep larger content in a file in the workspace.
- A name that a `[secrets]` or `[personal_credentials]` binding already provides (its `env` name, or a renamed `from` source) is rejected.
- Credentials don't belong here. Stashbase checks every value: a credential-like name (`*TOKEN*`, `*PASSWORD*`, `*API_KEY*`, …), a known token format, a URL containing a password, or a long random-looking string triggers a warning in `agent validate`. `agent run` then asks for confirmation before starting, and fails when it can't ask (non-interactive or `--silent`). Flagged values are never printed: `agent profiles show` displays them as `<hidden: looks like a credential>`.
- If a flagged value really isn't secret, acknowledge it so the check skips it:

```toml
[config_env]
BUILD_HASH = { value = "9f86d081884c7d659a2feaa0c55ad015", not_secret = true }
```

To set or override a variable for one run without editing the profile, pass `--config-env NAME=VALUE` (repeatable):

```bash
stashbase agent run --profile coding --config-env NODE_ENV=test --config-env PORT=4000 -- claude
```

Flag values go through the same checks as the profile's, including the credential check: a value that looks like a credential asks for confirmation, or fails the run when it can't ask.

The audit log's `session_started` event lists the names of the variables set for the run (`config_env_names`), never their values.

A complete example profile is in [examples/config-env.toml](agent-profiles/examples/config-env.toml). It includes commented-out lines that show the warning and the validation errors.

## Network Access and HTTP Rules

By default, the proxy denies all connections. Allow specific destinations:
Expand Down
27 changes: 27 additions & 0 deletions docs/agent-profiles/examples/config-env.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Plaintext, non-sensitive config for the agent via [config_env].
# Egress-only (no secrets), so it runs without a Stashbase project:
# cp docs/agent-profiles/examples/config-env.toml .stashbase/agents/
# stashbase agent validate --profile config-env
# stashbase agent run --profile config-env -- env
egress_hosts = ["api.anthropic.com", "registry.npmjs.org"]

# Uncomment to check the Docker backend, which starts from an empty env.
# [sandbox]
# backend = "docker"

[config_env]
NODE_ENV = "development"
RUST_LOG = "info,stashbase=debug"
# No password in the URL, so it isn't flagged.
DATABASE_URL = "postgres://app@localhost:5432/app"
# Long hex looks like a key; not_secret = true skips the check.
BUILD_HASH = { value = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b", not_secret = true }

# Uncomment to see the check: validate warns, and run asks for confirmation
# (or fails when non-interactive / --silent).
# UPLOAD_URL = "https://deploy:hunter2@uploads.example.com"
# DEPLOY_TOKEN = "abc"

# Uncomment to see hard failures:
# HTTPS_PROXY = "http://elsewhere:8080" # owned by the sandbox
# STASHBASE_API_KEY = "x" # STASHBASE_* is reserved
65 changes: 65 additions & 0 deletions src/cmd/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,13 @@ pub struct AgentRunCommand {
#[arg(long, value_name = "PATHS", value_delimiter = ',')]
pub docker_isolated_paths: Vec<String>,

/// Set a `[config_env]` variable for this run only, overriding the
/// profile's value. Repeatable: `--config-env NODE_ENV=test --config-env
/// PORT=4000`. Checked like the profile's own values, including the
/// credential check.
#[arg(long, value_name = "NAME=VALUE", value_parser = parse_config_env_arg)]
pub config_env: Vec<(String, String)>,

/// Store metadata-only proxy audit events locally
#[arg(
long,
Expand Down Expand Up @@ -711,6 +718,15 @@ pub enum AgentProfileSource {
Auto,
}

/// Parses `--config-env NAME=VALUE`, splitting at the first `=` so the value
/// may itself contain `=`. The name is validated with the profile.
fn parse_config_env_arg(arg: &str) -> Result<(String, String), String> {
match arg.split_once('=') {
Some((name, value)) if !name.is_empty() => Ok((name.to_owned(), value.to_owned())),
_ => Err(format!("expected NAME=VALUE, got '{arg}'")),
}
}

#[cfg(test)]
mod tests {
use clap::ValueEnum;
Expand All @@ -737,6 +753,55 @@ mod tests {
);
}

#[test]
fn config_env_flag_takes_repeatable_name_value_pairs() {
use super::{AgentCommand, AgentSubcommand};
use crate::cmd::root::{Cli, EntityType};
use clap::Parser;

let parse = |args: &[&str]| {
let mut argv = vec!["stashbase", "agent", "run", "--profile", "p"];
argv.extend_from_slice(args);
match Cli::try_parse_from(argv).expect("parses").entity_type {
EntityType::Agent(AgentCommand {
subcommand: AgentSubcommand::Run(run),
}) => run,
_ => panic!("expected `agent run`"),
}
};
let env = parse(&[
"--config-env",
"NODE_ENV=test",
"--config-env",
"QUERY=a=b",
"--config-env",
"EMPTY=",
"claude",
]);
assert_eq!(
env.config_env,
vec![
("NODE_ENV".to_owned(), "test".to_owned()),
("QUERY".to_owned(), "a=b".to_owned()),
("EMPTY".to_owned(), String::new()),
]
);
assert_eq!(env.command, vec!["claude".to_owned()]);
for bad in ["NOVALUE", "=value"] {
let argv = [
"stashbase",
"agent",
"run",
"--profile",
"p",
"--config-env",
bad,
"claude",
];
assert!(Cli::try_parse_from(argv).is_err(), "{bad}");
}
}

#[test]
fn worktree_flag_is_on_off_or_absent_and_never_swallows_the_command() {
use super::{AgentCommand, AgentSubcommand};
Expand Down
Loading
Loading