yolop 0.4.0

Yolop — a terminal coding agent built on everruns-runtime
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! Informational schema for the yolop settings file (`settings.toml`).
//!
//! The schema is *informational*, not a validator. Loading settings never
//! checks against it and never rejects unknown keys (see
//! [`crate::settings::Settings::from_table`]), so a user or another tool can
//! drop extra keys into the file without breaking yolop. What the schema adds
//! is **semantics**: a title, description, type, default, and examples for
//! every known configuration key. That lets the agent explain and edit
//! configuration in a human-friendly way through the `get_config` /
//! `set_config` tools and the `yolop-config` skill — the schema here is the
//! single source of truth those surfaces render.
//!
//! Keys are addressed the way a human would name them. Scalar keys are plain
//! (`default_provider`, `attribution`); per-provider tables are addressed with
//! a dotted provider segment (`tokens.openai`, `models.anthropic`).

use crate::runtime::SUPPORTED_PROVIDERS;

/// The kind of value a configuration key accepts. Drives validation in
/// `set_config` and how `get_config` renders the current value.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValueKind {
    /// A free-form string: provider name, model spec, URL, …
    Text,
    /// An on/off boolean.
    Bool,
    /// A secret string (an API token). `get_config` never echoes the value,
    /// only whether one is stored.
    Secret,
    /// An ordered list (harness capability overrides).
    List,
}

impl ValueKind {
    pub fn as_str(self) -> &'static str {
        match self {
            ValueKind::Text => "text",
            ValueKind::Bool => "bool",
            ValueKind::Secret => "secret",
            ValueKind::List => "list",
        }
    }
}

/// One described configuration key. Every field is `&'static` so the whole
/// schema is a compile-time constant.
pub struct ConfigField {
    /// Canonical name the agent/user addresses. For provider-scoped tables
    /// this is the bare table name (e.g. `tokens`); see `provider_scoped`.
    pub key: &'static str,
    /// Alternative names accepted when reading or writing this key.
    pub aliases: &'static [&'static str],
    pub title: &'static str,
    pub description: &'static str,
    pub kind: ValueKind,
    /// Effective default when the key is unset — for display only.
    pub default: Option<&'static str>,
    pub examples: &'static [&'static str],
    /// When true the key names a `[table]` whose entries are keyed by
    /// provider, so it is addressed as `<key>.<provider>`.
    pub provider_scoped: bool,
}

/// The full configuration schema. Order is the user-facing display order.
pub fn schema() -> &'static [ConfigField] {
    const FIELDS: &[ConfigField] = &[
        ConfigField {
            key: "default_provider",
            aliases: &["provider"],
            title: "Default provider",
            description: "The model provider used when no --provider flag is given. A value set \
                          here takes precedence over environment-credential auto-detection, which \
                          applies only when this is unset. Persisted as `default_provider` (the \
                          legacy `provider` key is still read, and accepted as an alias).",
            kind: ValueKind::Text,
            default: Some("openai (auto-detected from available credentials)"),
            examples: &[
                "anthropic",
                "codex",
                "openai",
                "google",
                "openrouter",
                "ollama",
            ],
            provider_scoped: false,
        },
        ConfigField {
            key: "default_model",
            aliases: &["model"],
            title: "Default model",
            description: "Global fallback model spec applied to the active provider when that \
                          provider has no per-provider entry under `models`. Provider-relative, \
                          same `model [reasoning-effort]` form `/setup model` accepts. A \
                          per-provider `models.<provider>` pick always wins over this. Applied \
                          only when the model id is recognized for the active provider.",
            kind: ValueKind::Text,
            default: Some("the active provider's built-in default model"),
            examples: &["claude-sonnet-4-5", "gpt-5.5 high", "gemini-2.5-pro"],
            provider_scoped: false,
        },
        ConfigField {
            key: "models",
            aliases: &["model_for"],
            title: "Per-provider model",
            description: "Model spec remembered for a specific provider, so a pick survives \
                          restarts and provider switches. Addressed as `models.<provider>`.",
            kind: ValueKind::Text,
            default: None,
            examples: &[
                "models.openai = gpt-5.5 high",
                "models.codex = gpt-5.5 high",
                "models.anthropic = claude-opus-4-5",
            ],
            provider_scoped: true,
        },
        ConfigField {
            key: "tokens",
            aliases: &["token"],
            title: "Provider API token",
            description: "API token for a provider, stored owner-only (0o600). Environment \
                          variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, …) always override the \
                          stored value. Addressed as `tokens.<provider>`.",
            kind: ValueKind::Secret,
            default: None,
            examples: &["tokens.openai = sk-…", "tokens.anthropic = …"],
            provider_scoped: true,
        },
        ConfigField {
            key: "base_urls",
            aliases: &["base_url", "url"],
            title: "Provider endpoint base URL",
            description: "Endpoint base URL for a provider. Used by the `custom` \
                          OpenAI-compatible provider (vLLM, llama.cpp, LM Studio, gateways). \
                          Addressed as `base_urls.<provider>`; must start with http:// or https://.",
            kind: ValueKind::Text,
            default: None,
            examples: &["base_urls.custom = http://localhost:8000/v1"],
            provider_scoped: true,
        },
        ConfigField {
            key: "approval_mode",
            aliases: &["approval"],
            title: "Soft-approval paranoia level",
            description: "How cautious yolop is about confirming critical actions: `protective` \
                          asks before any state change, `normal` asks only before destructive or \
                          outward-facing actions, `off` never asks. Common synonyms like \
                          `paranoid` and `yolo` are also accepted.",
            kind: ValueKind::Text,
            default: Some("normal"),
            examples: &["protective", "normal", "off"],
            provider_scoped: false,
        },
        ConfigField {
            key: "attribution",
            aliases: &[],
            title: "Commit & PR attribution",
            description: "When on, yolop appends a Co-Authored-By git trailer to commits it \
                          makes and a footer to PR descriptions it writes.",
            kind: ValueKind::Bool,
            default: Some("on"),
            examples: &["on", "off"],
            provider_scoped: false,
        },
        ConfigField {
            key: "proactive_wake",
            aliases: &["background_wake", "wake"],
            title: "Proactive background wake",
            description: "When on, the TUI auto-starts a turn so the agent reacts as soon as a \
                          background task finishes (instead of waiting for your next prompt). Turn \
                          off for a quieter session where you review finished tasks yourself.",
            kind: ValueKind::Bool,
            default: Some("on"),
            examples: &["on", "off"],
            provider_scoped: false,
        },
        ConfigField {
            key: "worktrees",
            aliases: &[],
            title: "Git worktree isolation",
            description: "Controls session worktrees for code changes: `auto` creates one when a \
                          prompt looks like implementation work, `always` uses a worktree in git \
                          repos from the start, `off` disables worktrees.",
            kind: ValueKind::Text,
            default: Some("auto"),
            examples: &["auto", "always", "off"],
            provider_scoped: false,
        },
        ConfigField {
            key: "capabilities",
            aliases: &["capability"],
            title: "Harness capabilities",
            description: "Ordered `[[capabilities]]` overrides applied on top of the default \
                          harness. Each entry has a `ref` (capability id), optional \
                          `enabled=false` to remove every instance with that ref, optional \
                          `append=true` to add a duplicate instance, and capability-specific \
                          config keys validated via `config_schema` / `validate_config`. Use \
                          `get_config key=capabilities` for the full registered catalog, or \
                          `get_config key=capabilities.<ref>` for one capability's schema \
                          metadata. Append entries with `set_config key=capabilities json=…`; \
                          pass `value=clear` to drop all stored overrides.",
            kind: ValueKind::List,
            default: None,
            examples: &[
                "capabilities.message_metadata",
                "set_config key=capabilities json={\"ref\":\"message_metadata\",\"config\":{\"fields\":[\"timestamp\"]}}",
            ],
            provider_scoped: true,
        },
    ];
    FIELDS
}

/// A parsed, routable configuration target. `set_config`/`get_config` match on
/// this rather than re-parsing key strings.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeyTarget {
    DefaultProvider,
    DefaultModel,
    Attribution,
    ApprovalMode,
    ProactiveWake,
    Worktrees,
    /// Per-provider model spec, for the named provider.
    Model(String),
    /// Per-provider API token.
    Token(String),
    /// Per-provider endpoint base URL.
    BaseUrl(String),
    /// The ordered `[[capabilities]]` override list.
    Capabilities,
    /// Catalog metadata for one harness capability ref (`capabilities.<ref>`).
    CapabilityRef(String),
}

impl KeyTarget {
    /// The schema field describing this target.
    pub fn field(&self) -> &'static ConfigField {
        let key = match self {
            KeyTarget::DefaultProvider => "default_provider",
            KeyTarget::DefaultModel => "default_model",
            KeyTarget::Attribution => "attribution",
            KeyTarget::ApprovalMode => "approval_mode",
            KeyTarget::ProactiveWake => "proactive_wake",
            KeyTarget::Worktrees => "worktrees",
            KeyTarget::Model(_) => "models",
            KeyTarget::Token(_) => "tokens",
            KeyTarget::BaseUrl(_) => "base_urls",
            KeyTarget::Capabilities | KeyTarget::CapabilityRef(_) => "capabilities",
        };
        schema()
            .iter()
            .find(|f| f.key == key)
            .expect("KeyTarget always maps to a schema field")
    }
}

/// Parse a human-supplied key (with aliases) into a routable target. Provider
/// segments are validated against the supported provider list.
pub fn parse_key(input: &str) -> Result<KeyTarget, String> {
    let norm = input.trim().to_ascii_lowercase();
    if norm.is_empty() {
        return Err(format!("empty config key; known keys: {}", known_keys()));
    }
    let (head, sub) = match norm.split_once('.') {
        Some((h, t)) => (h, Some(t.trim())),
        None => (norm.as_str(), None),
    };

    let scalar = |target: KeyTarget| -> Result<KeyTarget, String> {
        if sub.is_some() {
            return Err(format!(
                "`{head}` is a scalar key and takes no `.<provider>` segment"
            ));
        }
        Ok(target)
    };
    let scoped = |make: fn(String) -> KeyTarget| -> Result<KeyTarget, String> {
        let provider = sub.filter(|s| !s.is_empty()).ok_or_else(|| {
            format!("`{head}` is per-provider; address it as `{head}.<provider>`")
        })?;
        if !SUPPORTED_PROVIDERS.contains(&provider) {
            return Err(format!(
                "unknown provider `{provider}`; expected one of {}",
                SUPPORTED_PROVIDERS.join(", ")
            ));
        }
        Ok(make(provider.to_string()))
    };

    match head {
        "default_provider" | "provider" => scalar(KeyTarget::DefaultProvider),
        "default_model" | "model" => scalar(KeyTarget::DefaultModel),
        "attribution" => scalar(KeyTarget::Attribution),
        "approval_mode" | "approval" => scalar(KeyTarget::ApprovalMode),
        "proactive_wake" | "background_wake" | "wake" => scalar(KeyTarget::ProactiveWake),
        "worktrees" | "worktree" => scalar(KeyTarget::Worktrees),
        "models" | "model_for" => scoped(KeyTarget::Model),
        "tokens" | "token" => scoped(KeyTarget::Token),
        "base_urls" | "base_url" | "url" => scoped(KeyTarget::BaseUrl),
        "capabilities" | "capability" => {
            if let Some(cap_ref) = sub.filter(|s| !s.is_empty()) {
                Ok(KeyTarget::CapabilityRef(cap_ref.to_string()))
            } else {
                Ok(KeyTarget::Capabilities)
            }
        }
        _ => Err(format!(
            "unknown config key `{input}`; known keys: {}",
            known_keys()
        )),
    }
}

/// Comma-separated list of canonical keys, for error messages.
pub fn known_keys() -> String {
    schema()
        .iter()
        .map(|f| {
            if f.provider_scoped {
                format!("{}.<provider>", f.key)
            } else {
                f.key.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(", ")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn aliases_resolve_to_canonical_targets() {
        assert_eq!(parse_key("provider").unwrap(), KeyTarget::DefaultProvider);
        assert_eq!(
            parse_key("default_provider").unwrap(),
            KeyTarget::DefaultProvider
        );
        assert_eq!(parse_key("model").unwrap(), KeyTarget::DefaultModel);
        assert_eq!(parse_key("default_model").unwrap(), KeyTarget::DefaultModel);
        assert_eq!(parse_key("approval").unwrap(), KeyTarget::ApprovalMode);
        assert_eq!(parse_key("approval_mode").unwrap(), KeyTarget::ApprovalMode);
    }

    #[test]
    fn provider_scoped_keys_parse_and_validate() {
        assert_eq!(
            parse_key("tokens.openai").unwrap(),
            KeyTarget::Token("openai".to_string())
        );
        assert_eq!(
            parse_key("url.custom").unwrap(),
            KeyTarget::BaseUrl("custom".to_string())
        );
        assert_eq!(
            parse_key("Models.Anthropic").unwrap(),
            KeyTarget::Model("anthropic".to_string())
        );
    }

    #[test]
    fn scalar_key_rejects_provider_segment() {
        assert!(parse_key("attribution.openai").is_err());
        assert!(parse_key("default_model.openai").is_err());
    }

    #[test]
    fn provider_scoped_requires_segment_and_known_provider() {
        assert!(parse_key("tokens").unwrap_err().contains("per-provider"));
        assert!(
            parse_key("tokens.nope")
                .unwrap_err()
                .contains("unknown provider")
        );
    }

    #[test]
    fn unknown_key_lists_known_keys() {
        let err = parse_key("frobnicate").unwrap_err();
        assert!(err.contains("default_provider"));
        assert!(err.contains("tokens.<provider>"));
    }

    #[test]
    fn capabilities_keys_parse() {
        assert_eq!(parse_key("capabilities").unwrap(), KeyTarget::Capabilities);
        assert_eq!(
            parse_key("capabilities.message_metadata").unwrap(),
            KeyTarget::CapabilityRef("message_metadata".to_string())
        );
    }

    #[test]
    fn every_target_maps_to_a_field() {
        for target in [
            KeyTarget::DefaultProvider,
            KeyTarget::DefaultModel,
            KeyTarget::Attribution,
            KeyTarget::ApprovalMode,
            KeyTarget::Model("openai".into()),
            KeyTarget::Token("openai".into()),
            KeyTarget::BaseUrl("custom".into()),
            KeyTarget::Capabilities,
            KeyTarget::CapabilityRef("message_metadata".into()),
        ] {
            let _ = target.field();
        }
    }
}