Skip to main content

warble_claude_code/
targets.rs

1//! Target capability profiles — the declarative side of the capability model (see
2//! [`capability-model.md`][spec-cap]).
3//!
4//! A runtime target is `engine × mode`, never just "claude-code": the same engine in a different
5//! mode is a genuinely different capability set (headless *loses* `human_approval` but *gains*
6//! `structured_output_capture`). This module only declares profiles; `resolve.rs` links them.
7//!
8//! [spec-cap]: https://github.com/Canner/Warble/blob/main/docs/spec/capability-model.md
9
10use serde::{Deserialize, Serialize};
11use std::borrow::Cow;
12use std::collections::HashMap;
13
14/// One of the four resolution outcomes a capability can take on a target.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "kebab-case")]
17pub enum CapabilityOutcome {
18    Native,
19    RealizeVia,
20    Degrade,
21    Fail,
22}
23
24/// Who supplies a resolved capability.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum ProvidedBy {
28    Runtime,
29    Warble,
30    None,
31}
32
33/// safety-critical capabilities must never silently degrade — unsupported means the resolution
34/// pass aborts. required/best-effort may degrade with a warning recorded in the report.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37pub enum Criticality {
38    SafetyCritical,
39    Required,
40    BestEffort,
41}
42
43/// Keeping a component's intermediate work out of the context of whatever called it. The IR names
44/// the requirement; how a target provides a child context — and whether it can at all — is the
45/// target's business, which is why this is a capability rather than a flag.
46pub(crate) const CONTEXT_ISOLATION_CAPABILITY: &str = "context_isolation";
47
48#[derive(Debug, Clone)]
49pub struct CapabilityEntry {
50    pub outcome: CapabilityOutcome,
51    /// `Cow` so a statically-declared entry and one loaded from a provider fragment at dispatch are
52    /// the same type — a base-only profile and a base-⊕-provider-composed one must be, or they
53    /// cannot be merged. Same reason the profile is keyed by `String`.
54    pub via: Option<Cow<'static, str>>,
55    pub provided_by: ProvidedBy,
56    pub criticality: Criticality,
57    pub note: Option<Cow<'static, str>>,
58}
59
60pub type CapabilityProfile = HashMap<String, CapabilityEntry>;
61
62/// Build a profile from statically-declared entries, owning the keys. The declarations below stay
63/// `&'static str` literals; only the map they land in is owned.
64fn profile(
65    entries: impl IntoIterator<Item = (&'static str, CapabilityEntry)>,
66) -> CapabilityProfile {
67    entries
68        .into_iter()
69        .map(|(k, v)| (k.to_string(), v))
70        .collect()
71}
72
73/// The two claude-code targets: engine × mode.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TargetId {
76    Headless,
77    Interactive,
78    CodexInteractive,
79}
80
81impl TargetId {
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            TargetId::Headless => "claude-code:headless",
85            TargetId::Interactive => "claude-code:interactive",
86            TargetId::CodexInteractive => "codex:interactive",
87        }
88    }
89
90    /// The mode half of `engine × mode`, which is what a provider fragment's optional `mode:`
91    /// field is matched against.
92    pub fn mode(&self) -> &'static str {
93        match self {
94            TargetId::Headless => "headless",
95            TargetId::Interactive => "interactive",
96            // Named for symmetry with the other interactive target. Nothing consults it today:
97            // `codex:interactive` rejects `--provider` outright, since it realizes no capability
98            // of its own and a fragment would silently do nothing.
99            TargetId::CodexInteractive => "interactive",
100        }
101    }
102
103    pub fn parse(value: &str) -> Option<TargetId> {
104        match value {
105            "claude-code:headless" => Some(TargetId::Headless),
106            "claude-code:interactive" => Some(TargetId::Interactive),
107            "codex:interactive" => Some(TargetId::CodexInteractive),
108            _ => None,
109        }
110    }
111
112    pub fn profile(&self) -> CapabilityProfile {
113        match self {
114            TargetId::Headless => headless_profile(),
115            TargetId::Interactive => interactive_profile(),
116            TargetId::CodexInteractive => codex_interactive_profile(),
117        }
118    }
119}
120
121/// claude-code target's default mode when the caller doesn't specify one.
122pub const DEFAULT_TARGET: TargetId = TargetId::Headless;
123
124pub fn is_known_target(value: &str) -> bool {
125    TargetId::parse(value).is_some()
126}
127
128pub fn known_target_names() -> [&'static str; 3] {
129    [
130        TargetId::Headless.as_str(),
131        TargetId::Interactive.as_str(),
132        TargetId::CodexInteractive.as_str(),
133    ]
134}
135
136fn entry(
137    outcome: CapabilityOutcome,
138    via: Option<&'static str>,
139    provided_by: ProvidedBy,
140    criticality: Criticality,
141    note: Option<&'static str>,
142) -> CapabilityEntry {
143    CapabilityEntry {
144        outcome,
145        via: via.map(Cow::Borrowed),
146        provided_by,
147        criticality,
148        note: note.map(Cow::Borrowed),
149    }
150}
151
152fn headless_profile() -> CapabilityProfile {
153    use CapabilityOutcome::*;
154    use Criticality::*;
155    use ProvidedBy::{Runtime, Warble};
156    profile([
157        (
158            "sql_execution:read_only",
159            entry(Native, Some("bash-wren"), Runtime, Required, None),
160        ),
161        (
162            "genbi_build",
163            entry(Native, Some("bash-wren"), Runtime, Required, None),
164        ),
165        (
166            "semantic_introspection",
167            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
168        ),
169        // +Constitutive: reading the semantic model's structure to propose a context edit (models/
170        // metrics/knowledge) — realized the same way as semantic_introspection, via the `wren` CLI.
171        (
172            "schema_introspection",
173            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
174        ),
175        ("llm:strong", entry(Native, None, Runtime, Required, None)),
176        ("llm:cheap", entry(Native, None, Runtime, Required, None)),
177        (
178            "llm:per_step_tier",
179            entry(RealizeVia, Some("subagents"), Runtime, Required, None),
180        ),
181        // Per-step PROVIDER routing (cloud+local in one run) — distinct from per_step_tier, which is
182        // same-provider model selection. The whole-session `claude` process can't switch provider
183        // mid-run, so Warble realizes the LOCAL step two ways (--hybrid-realization): `bash-script`
184        // (a Bash-run local-inference script) or `mcp-server` (a `.mcp.json` registering `warble
185        // mcp-serve`, so the local call is an MCP tool — no bash widening). Cloud steps stay the
186        // driver's own `wren` work either way.
187        (
188            "llm:per_step_provider",
189            entry(
190                RealizeVia,
191                Some("bash-script|mcp-server"),
192                Warble,
193                Required,
194                Some("local step via a bash-script script or an mcp-server (warble mcp-serve); cloud steps are the driver's own wren work"),
195            ),
196        ),
197        (
198            "render_contract",
199            entry(RealizeVia, Some("html-file"), Runtime, BestEffort, None),
200        ),
201        (
202            "structured_output_capture",
203            entry(Native, Some("stream-json"), Runtime, Required, None),
204        ),
205        (
206            "human_approval",
207            entry(
208                Fail,
209                None,
210                ProvidedBy::None,
211                SafetyCritical,
212                Some("no human in the loop in headless mode"),
213            ),
214        ),
215        (
216            "write_authz",
217            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
218        ),
219        // +Constitutive: the path-scoped authorization gate for a `context`-target mutation (models/
220        // metrics/knowledge), distinct from `write_authz` (data writes) and `artifact_write` (render
221        // writes) — scopes must never cross. Realized via the same filesystem, scoped to a path.
222        (
223            "context_write_authz",
224            entry(RealizeVia, Some("scoped-fs"), Runtime, SafetyCritical, None),
225        ),
226        (
227            "artifact_write",
228            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
229        ),
230        // A component may need its working-out kept out of the caller's context — the delegated
231        // steps, the queries, the repairs. Realized here as a subagent: the child holds the whole
232        // component, the caller sees one delegation and one result. A target with no child-context
233        // mechanism must loud-fail rather than run the work in the open.
234        (
235            CONTEXT_ISOLATION_CAPABILITY,
236            entry(RealizeVia, Some("subagent"), Runtime, Required, None),
237        ),
238        // +Assertive borrows the scheduling / event / notify transports from the runtime (OS cron,
239        // pub/sub, MCP) — the IR names the capability + criticality only; the mechanism (cron / slack)
240        // is legalized here, never in the IR (capability-model §6/§7). A target with no such mechanism
241        // wired keeps these `fail` (loud, never silent).
242        (
243            "scheduler",
244            entry(RealizeVia, Some("os-cron"), Runtime, Required, None),
245        ),
246        (
247            "event_bus",
248            entry(RealizeVia, Some("pub-sub"), Runtime, Required, None),
249        ),
250        (
251            "notify_channel",
252            entry(RealizeVia, Some("mcp-notify"), Runtime, Required, None),
253        ),
254        (
255            "blast_radius",
256            entry(
257                Fail,
258                None,
259                Warble,
260                SafetyCritical,
261                Some("requires fine_grained_binding"),
262            ),
263        ),
264        // +Mutating borrows version control (git) from the runtime as the apply-time checkpoint a
265        // rollback restores from. Warble declares the requirement; git itself is never owned here.
266        (
267            "version_control",
268            entry(RealizeVia, Some("git"), Runtime, Required, None),
269        ),
270    ])
271}
272
273fn interactive_profile() -> CapabilityProfile {
274    use CapabilityOutcome::*;
275    use Criticality::*;
276    use ProvidedBy::{Runtime, Warble};
277    profile([
278        (
279            "sql_execution:read_only",
280            entry(Native, Some("bash-wren"), Runtime, Required, None),
281        ),
282        (
283            "genbi_build",
284            entry(Native, Some("bash-wren"), Runtime, Required, None),
285        ),
286        (
287            "semantic_introspection",
288            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
289        ),
290        (
291            "source_connect",
292            entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None),
293        ),
294        // +Constitutive: see headless_profile — same mechanism, not a differentiator across modes.
295        (
296            "schema_introspection",
297            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
298        ),
299        ("raw_material_read", entry(Native, Some("Read"), Runtime, Required, Some("Claude Code's cwd-scoped Read tool; prompts forbid credentials and raw excerpts"))),
300        ("context_validate", entry(RealizeVia, Some("Bash(wren:*)"), Runtime, Required, None)),
301        ("context_build", entry(RealizeVia, Some("Bash(wren:*)"), Runtime, Required, None)),
302        ("enrichment_apply:deterministic", entry(RealizeVia, Some("native-interactive-approved-tool"), Runtime, SafetyCritical, Some("only after the native human-approval gate; Warble does not run it"))),
303        ("llm:strong", entry(Native, None, Runtime, Required, None)),
304        ("llm:cheap", entry(Native, None, Runtime, Required, None)),
305        (
306            "llm:per_step_tier",
307            entry(RealizeVia, Some("subagents"), Runtime, Required, None),
308        ),
309        // See headless_profile: per-step provider routing (hybrid) is realized via an emitted
310        // local-inference script (bash-script); same in interactive mode.
311        (
312            "llm:per_step_provider",
313            entry(
314                RealizeVia,
315                Some("bash-script|mcp-server"),
316                Warble,
317                Required,
318                Some("local step via an emitted local-inference script (Bash); cloud steps stay native subagents"),
319            ),
320        ),
321        (
322            "render_contract",
323            entry(
324                Degrade,
325                Some("terminal-markdown"),
326                Runtime,
327                BestEffort,
328                None,
329            ),
330        ),
331        (
332            "structured_output_capture",
333            entry(
334                Degrade,
335                None,
336                ProvidedBy::None,
337                BestEffort,
338                Some("not captured in the interactive TUI"),
339            ),
340        ),
341        (
342            "human_approval",
343            entry(Native, None, Runtime, SafetyCritical, None),
344        ),
345        (
346            "write_authz",
347            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
348        ),
349        // +Constitutive: see headless_profile — same path-scoped gate, not a differentiator across
350        // modes.
351        (
352            "context_write_authz",
353            entry(RealizeVia, Some("scoped-fs"), Runtime, SafetyCritical, None),
354        ),
355        (
356            "artifact_write",
357            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
358        ),
359        // A component may need its working-out kept out of the caller's context — the delegated
360        // steps, the queries, the repairs. Realized here as a subagent: the child holds the whole
361        // component, the caller sees one delegation and one result. A target with no child-context
362        // mechanism must loud-fail rather than run the work in the open.
363        (
364            CONTEXT_ISOLATION_CAPABILITY,
365            entry(RealizeVia, Some("subagent"), Runtime, Required, None),
366        ),
367        // Same borrowed transports as headless (+Assertive): scheduler/event/notify are runtime-
368        // supplied on both modes; only render_contract + structured_output_capture + human_approval
369        // differ between the two.
370        (
371            "scheduler",
372            entry(RealizeVia, Some("os-cron"), Runtime, Required, None),
373        ),
374        (
375            "event_bus",
376            entry(RealizeVia, Some("pub-sub"), Runtime, Required, None),
377        ),
378        (
379            "notify_channel",
380            entry(RealizeVia, Some("mcp-notify"), Runtime, Required, None),
381        ),
382        (
383            "blast_radius",
384            entry(
385                Fail,
386                None,
387                Warble,
388                SafetyCritical,
389                Some("requires fine_grained_binding"),
390            ),
391        ),
392        // +Mutating borrows version control (git) from the runtime as the apply-time checkpoint a
393        // rollback restores from. Warble declares the requirement; git itself is never owned here.
394        (
395            "version_control",
396            entry(RealizeVia, Some("git"), Runtime, Required, None),
397        ),
398    ])
399}
400
401/// Codex's repository-scoped TUI target. This is intentionally a discovery/materialization
402/// target, not the existing app-server based `codex:local` runtime.
403fn codex_interactive_profile() -> CapabilityProfile {
404    use CapabilityOutcome::*;
405    use Criticality::*;
406    use ProvidedBy::{Runtime, Warble};
407    profile([
408        ("sql_execution:read_only", entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None)),
409        ("genbi_build", entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None)),
410        ("source_connect", entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None)),
411        ("semantic_introspection", entry(RealizeVia, Some("codex-repository-read"), Runtime, Required, None)),
412        ("raw_material_read", entry(RealizeVia, Some("codex-repository-read"), Runtime, Required, Some("repository-scoped native TUI reads only; prompts forbid credentials and raw excerpts"))),
413        ("llm:strong", entry(Native, None, Runtime, Required, None)),
414        ("llm:cheap", entry(Native, None, Runtime, Required, None)),
415        ("llm:per_step_tier", entry(RealizeVia, Some("native-interactive-subagents"), Runtime, Required, None)),
416        ("context_write_authz", entry(RealizeVia, Some("native-interactive-human-approval"), Runtime, SafetyCritical, None)),
417        ("context_validate", entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None)),
418        ("context_build", entry(RealizeVia, Some("native-interactive-command"), Runtime, Required, None)),
419        ("version_control", entry(RealizeVia, Some("git"), Runtime, Required, None)),
420        ("human_approval", entry(Native, Some("native-interactive-human"), Runtime, SafetyCritical, None)),
421        ("enrichment_apply:deterministic", entry(RealizeVia, Some("native-interactive-approved-tool"), Runtime, SafetyCritical, Some("only after the native human-approval gate; Warble does not run it"))),
422        ("render_contract", entry(Degrade, Some("terminal-markdown"), Runtime, BestEffort, None)),
423        ("artifact_write", entry(RealizeVia, Some("session-scoped-artifact-api"), Runtime, SafetyCritical, Some("the host owns the artifact API and persistence"))),
424        ("blast_radius", entry(Fail, None, Warble, SafetyCritical, Some("requires fine_grained_binding"))),
425    ])
426}