Skip to content
Draft
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
19 changes: 19 additions & 0 deletions docs/07-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,24 @@ awman config set yoloDisallowedTools "" # set an empty list

An empty repo list actively overrides a non-empty global list. To stop overriding, remove the field from the repo config file. See [Yolo Mode](06-yolo-mode.md).

### Control credential injection (`auth` mode)

By default awman injects host keychain credentials into agent containers
(`keychain` mode). Two alternatives are available for harnesses that supply
credentials through other means:

```json
{ "auth": "passthrough" }
```

| Value | Behaviour |
|-------|-----------|
| `keychain` (default) | Inject host keychain credentials. When the repo also declares `env(ANTHROPIC_API_KEY)` (or another credential that covers the same provider) **and that variable is set on the host**, the keychain OAuth token for that provider is automatically suppressed at injection time — the container receives exactly one set of credentials per provider. If the declared passthrough var is not set on the host, the keychain credential is retained so the container is not left with zero credentials for that provider. |
| `passthrough` | No KEYCHAIN credential injection; declared `env()` overlays still apply. Supply credentials via `env(VAR)` overlays. |
| `none` | No KEYCHAIN credential injection; declared `env()` overlays still apply. |

Set `auth` in `.awman/config.json` directly (it is not settable via `config set`). The field is per-repo only — cloud harnesses that do not declare an anthropic env var remain on the default `keychain` path and continue to receive keychain OAuth unaffected.

---

## Runtimes
Expand Down Expand Up @@ -220,6 +238,7 @@ awman keeps global config and data (workflows, skills, worktrees, API state) und
| `agentStuckTimeout` | integer (seconds) | 30 | Inactivity period before an agent is flagged as stuck | yes |
| `baseImage` | string | (unset → global) | Image tag for workflow setup/teardown containers — see [Workflows](05-workflows.md) | no (edit file) |
| `dockerfile` | string | `Dockerfile.dev` | Path to the project base Dockerfile, relative to repo root or absolute | no (edit file) |
| `auth` | `"keychain"` \| `"passthrough"` \| `"none"` | `"keychain"` | Credential injection mode — see [Control credential injection](#control-credential-injection-auth-mode) | no (edit file) |

### Global config fields (`$HOME/.awman/config.json`)

Expand Down
88 changes: 86 additions & 2 deletions src/data/config/effective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::time::Duration;
use crate::data::config::env::EnvSnapshot;
use crate::data::config::flags::FlagConfig;
use crate::data::config::global::GlobalConfig;
use crate::data::config::repo::RepoConfig;
use crate::data::config::repo::{AgentAuthMode, RepoConfig};
use crate::data::config::{DEFAULT_AGENT_STUCK_TIMEOUT_SECS, DEFAULT_SCROLLBACK_LINES};

/// Merged view of every configuration source, in precedence order.
Expand Down Expand Up @@ -206,6 +206,20 @@ impl EffectiveConfig {
}
self.global.base_image.clone()
}

/// Effective credential injection mode (repo > built-in default `keychain`).
///
/// `passthrough` skips keychain injection entirely; `none` disables keychain
/// injection; `keychain` (default) injects host keychain creds, subject to
/// injection-time dedup of any repo-declared env vars.
///
/// Single-layer resolution (repo only, no global flag override) is a
/// deliberate conservative choice: a credential toggle must be an explicit
/// per-repo decision, not silently inherited from a global default that the
/// operator may not realize is set.
pub fn auth_mode(&self) -> AgentAuthMode {
self.repo.auth.unwrap_or_default()
}
}

#[cfg(test)]
Expand All @@ -214,7 +228,7 @@ mod tests {
use crate::data::config::env::{
EnvSnapshot, AWMAN_API_KEY, AWMAN_REMOTE_ADDR, AWMAN_REMOTE_SESSION,
};
use crate::data::config::repo::{ApiConfig, RemoteConfig};
use crate::data::config::repo::{AgentAuthMode, ApiConfig, RemoteConfig};
use std::time::Duration;

fn make_effective(
Expand Down Expand Up @@ -803,4 +817,74 @@ mod tests {
);
assert_eq!(ec4.scrollback_lines(), DEFAULT_SCROLLBACK_LINES);
}

// ── Part B: auth_mode ─────────────────────────────────────────────────────

#[test]
fn auth_mode_default_is_keychain() {
let ec = make_effective(
FlagConfig::default(),
EnvSnapshot::empty(),
RepoConfig::default(),
GlobalConfig::default(),
);
assert_eq!(
ec.auth_mode(),
AgentAuthMode::Keychain,
"default auth mode must be keychain"
);
}

#[test]
fn auth_mode_passthrough_from_repo() {
let repo = RepoConfig {
auth: Some(AgentAuthMode::Passthrough),
..Default::default()
};
let ec = make_effective(
FlagConfig::default(),
EnvSnapshot::empty(),
repo,
GlobalConfig::default(),
);
assert_eq!(
ec.auth_mode(),
AgentAuthMode::Passthrough,
"passthrough set in repo config must be effective"
);
}

#[test]
fn auth_mode_none_from_repo() {
let repo = RepoConfig {
auth: Some(AgentAuthMode::None),
..Default::default()
};
let ec = make_effective(
FlagConfig::default(),
EnvSnapshot::empty(),
repo,
GlobalConfig::default(),
);
assert_eq!(
ec.auth_mode(),
AgentAuthMode::None,
"none set in repo config must be effective"
);
}

#[test]
fn auth_mode_keychain_explicitly_set_in_repo() {
let repo = RepoConfig {
auth: Some(AgentAuthMode::Keychain),
..Default::default()
};
let ec = make_effective(
FlagConfig::default(),
EnvSnapshot::empty(),
repo,
GlobalConfig::default(),
);
assert_eq!(ec.auth_mode(), AgentAuthMode::Keychain);
}
}
3 changes: 2 additions & 1 deletion src/data/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ pub use env::{Env, EnvSnapshot};
pub use flags::FlagConfig;
pub use global::GlobalConfig;
pub use repo::{
ApiConfig, RemoteConfig, RepoConfig, WorkItemsConfig, REPO_CONFIG_FILENAME, REPO_CONFIG_SUBDIR,
AgentAuthMode, ApiConfig, RemoteConfig, RepoConfig, WorkItemsConfig, REPO_CONFIG_FILENAME,
REPO_CONFIG_SUBDIR,
};

/// Built-in default number of scrollback lines for the container terminal emulator.
Expand Down
27 changes: 27 additions & 0 deletions src/data/config/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ pub struct ApiConfig {
pub always_non_interactive: Option<bool>,
}

/// Per-repo credential injection mode.
///
/// Controls how awman injects credentials into agent containers. The default
/// (`keychain`) injects whatever the host keychain resolves, subject to
/// injection-time deduplication (Part A). `passthrough` skips keychain
/// injection entirely — the harness must supply credentials via `env(VAR)`
/// overlays. `none` disables KEYCHAIN credential injection; declared `env()`
/// overlays still apply.
///
/// Resolved through `EffectiveConfig::auth_mode()`.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AgentAuthMode {
/// Inject host keychain credentials (with injection-time dedup). Default.
#[default]
Keychain,
/// Skip KEYCHAIN injection; rely on repo-declared `env()` overlays.
/// Declared `env()` overlays still apply.
Passthrough,
/// No KEYCHAIN credential injection; declared `env()` overlays still apply.
None,
}

/// Work-items configuration nested within `RepoConfig`.
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkItemsConfig {
Expand Down Expand Up @@ -79,6 +102,10 @@ pub struct RepoConfig {
pub base_image: Option<String>,
#[serde(rename = "dockerfile", skip_serializing_if = "Option::is_none")]
pub dockerfile: Option<String>,
/// Credential injection mode for agent containers. See [`AgentAuthMode`].
/// Resolved per-repo; `keychain` (default) injects host keychain creds.
#[serde(rename = "auth", skip_serializing_if = "Option::is_none")]
pub auth: Option<AgentAuthMode>,
}

impl RepoConfig {
Expand Down
Loading