Skip to main content

supercode_harness/
profiles_control.rs

1//! ORCH-21 — the `profile` noun at the CONTROLLED tier: create and delete a
2//! routed config home through the HARNESS'S OWN verb, with supercode as the
3//! uniform client.
4//!
5//! Charter (`docs/plans/orchestration-domain-11-2026-09-02.md` §0.4):
6//! **supercode never owns a harness's config plane.** It makes no directory,
7//! writes no config file, and removes nothing itself. Each verb below is a
8//! subprocess of the harness's own CLI, run in the harness's own home:
9//!
10//! * **Hermes** — `hermes profile create <name>` / `hermes profile delete
11//!   --yes <name>` with `HERMES_HOME` pointing at the ROOT home the caller
12//!   addressed (`HERMES_HOME/profiles/<name>` is what the verb makes;
13//!   upstream `hermes_constants.get_default_hermes_root` derives the profiles
14//!   root from that variable). `--clone-from` carries the uniform `from`.
15//! * **OpenClaw** — `openclaw agents add <id> --workspace <dir>
16//!   --non-interactive --json` / `openclaw agents delete <id> --force --json`,
17//!   pointed at the caller's state dir through openclaw's own environment
18//!   contract. At the pin these edit `openclaw.json` directly (`agents.list`);
19//!   `agents delete` additionally offers the change to a RUNNING gateway when
20//!   one is reachable, resolving that endpoint itself — neither verb takes a
21//!   `--url`/`--token` flag, so supercode passes no credential here.
22//! * **The orchestrator** — its own package (ORC-13). A profile IS a folder
23//!   whose files are the model's serialization (`docs/ORCHESTRATOR-IR.md`
24//!   §6), so `create` is a new folder record followed by the package's own
25//!   `save()`, and `delete` removes the record and has the daemon RENAME the
26//!   folder into `profiles/.trash/<name>-<stamp>/` — nothing is ever
27//!   unlinked. Both go through [`crate::orchestrator_door`]: the daemon's
28//!   socket while it is up, `node bin/orchestrator.mjs profiles.create|delete`
29//!   when it is down. supercode still writes no file of that folder itself.
30//! * **Codex** — refused. A Codex profile is a `[profiles.<name>]` TABLE that
31//!   a human (or a tool) authors in `$CODEX_HOME/config.toml`; Codex publishes
32//!   no `codex profile create|delete` verb, so supercode names that door
33//!   rather than editing another harness's config file behind its back.
34//! * **supercode presets** — refused. A preset is compiled-in CODE
35//!   ([`crate::presets::RESERVED_PRESET_NAMES`]), not a directory a verb can
36//!   make.
37//!
38//! The three tier rules inherited from ORCH-18 hold here unchanged:
39//!
40//! 1. **The harness's answer is the answer.** After the verb exits 0 the row
41//!    is re-read through the ORCH-10 loader ([`crate::profiles`]) and
42//!    returned. A non-zero exit surfaces the harness's own stderr as the
43//!    error — never a silent success, never a supercode-invented row.
44//! 2. **The command is narrated.** Every outcome carries `ran`: the exact
45//!    argv that was executed, credentials redacted.
46//! 3. **A verb the harness does not publish is refused**
47//!    ([`ProfileControlError::Unsupported`] → `UnsupportedAction`), never
48//!    faked.
49//!
50//! One deliberate flag supercode does NOT let the harness default: nothing
51//! here writes outside the home the caller addressed. `hermes profile create`
52//! also drops a wrapper script into `~/.local/bin/<name>` (upstream
53//! `profiles.py::_get_wrapper_dir`, which ignores `HERMES_HOME`), so the
54//! non-interactive client form passes `--no-alias` — a programmatic
55//! `profiles.create` against an isolated home must not install an executable
56//! on the caller's PATH. The flag is in `ran`, so the choice is visible.
57
58use std::path::{Path, PathBuf};
59
60use serde::{Deserialize, Serialize};
61
62use crate::harness_command::HarnessCommand;
63use crate::profiles::ProfileRow;
64use crate::{HarnessHomes, HarnessId};
65
66/// Harnesses whose profiles supercode can MUTATE through their own CLI verb.
67/// Strictly narrower than [`crate::profiles::PROFILE_HARNESSES`]: Codex
68/// profiles and supercode presets are readable but not controllable.
69pub const CONTROLLED_PROFILE_HARNESSES: &[&str] = &[
70    HarnessId::HERMES,
71    HarnessId::OPENCLAW,
72    HarnessId::ORCHESTRATOR,
73];
74
75/// Why Codex refuses `profiles.create` / `profiles.delete`.
76pub const CODEX_REFUSAL: &str =
77    "codex profiles are file-authored: a profile IS a `[profiles.<name>]` table in \
78     `$CODEX_HOME/config.toml`, created by adding that table and deleted by removing it. Codex \
79     publishes no `codex profile create|delete` verb a client can call, so supercode names the \
80     door rather than editing another harness's config file behind its back";
81
82/// Why supercode's own presets refuse the same two verbs.
83pub const PRESET_REFUSAL: &str =
84    "a supercode preset is CODE — one of the compiled-in preset bundles, not a config home a verb \
85     can make or remove. supercode publishes no preset create/delete verb, so the profile noun \
86     refuses rather than inventing one";
87
88/// One uniform mutating verb over the profile noun.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ProfileVerb {
92    /// Make a new named config home.
93    Create,
94    /// Remove a named config home.
95    Delete,
96}
97
98impl ProfileVerb {
99    /// Uniform spelling used in the RPC method and in outcomes.
100    pub const fn as_str(self) -> &'static str {
101        match self {
102            Self::Create => "create",
103            Self::Delete => "delete",
104        }
105    }
106}
107
108/// One mutating request, in the uniform Domain 11 vocabulary.
109#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ProfileMutation {
111    /// Harness that owns the profile.
112    pub harness: String,
113    /// Profile / agent name to create or delete.
114    pub name: String,
115    /// Existing profile the new one is cloned from, where the harness has a
116    /// verb for it (Hermes `--clone-from`). Refused elsewhere, never dropped.
117    #[serde(default, alias = "template")]
118    pub from: Option<String>,
119    /// Workspace directory for the new agent — required by
120    /// `openclaw agents add` in non-interactive mode.
121    #[serde(default)]
122    pub workspace: Option<String>,
123    /// Storage roots, so an isolated home is addressed the same way the read
124    /// side addresses it.
125    #[serde(default)]
126    pub homes: HarnessHomes,
127}
128
129/// What one mutation did, with the harness's own row read back afterwards.
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ProfileMutationOutcome {
132    /// Harness that ran the verb.
133    pub harness: String,
134    /// Uniform verb that was asked for.
135    pub verb: String,
136    /// The exact harness command that ran, credentials redacted.
137    pub ran: String,
138    /// Affected profile name, as the harness's own store spells it.
139    pub name: String,
140    /// The profile as the harness's own store reports it AFTER the verb.
141    /// Absent for `delete`.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub profile: Option<ProfileRow>,
144    /// `true` on a successful `delete`.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub deleted: Option<bool>,
147}
148
149/// Why a profile mutation could not be performed.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum ProfileControlError {
152    /// The harness has no verb for what was asked (refused, never faked).
153    Unsupported(String),
154    /// The request itself is incoherent.
155    Invalid(String),
156    /// The harness verb ran and failed; the message carries its stderr.
157    Failed(String),
158}
159
160impl std::fmt::Display for ProfileControlError {
161    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        match self {
163            Self::Unsupported(message) | Self::Invalid(message) | Self::Failed(message) => {
164                formatter.write_str(message)
165            }
166        }
167    }
168}
169
170impl std::error::Error for ProfileControlError {}
171
172type Result<T> = std::result::Result<T, ProfileControlError>;
173
174/// Whether `harness` can have its profiles mutated at all.
175pub fn supports_profile_control(harness: &str) -> bool {
176    CONTROLLED_PROFILE_HARNESSES.contains(&harness)
177}
178
179/// The refusal sentence for a harness that cannot be controlled: the reason
180/// the HARNESS gives, when it has profiles, and otherwise the same sentence
181/// `profiles.list` answers with.
182fn unsupported_harness(harness: &str) -> String {
183    match harness {
184        HarnessId::CODEX => CODEX_REFUSAL.to_string(),
185        HarnessId::SUPERCODE => PRESET_REFUSAL.to_string(),
186        other => crate::profiles::ProfileError::UnsupportedHarness {
187            harness: other.to_string(),
188        }
189        .to_string(),
190    }
191}
192
193/// The harness's own executable, refused in the profile noun's words.
194fn harness_program(harness: &str) -> Result<String> {
195    crate::harness_command::harness_program(harness).map_err(|detail| {
196        ProfileControlError::Unsupported(detail.unwrap_or_else(|| unsupported_harness(harness)))
197    })
198}
199
200/// `HERMES_HOME` for a profile mutation: the ROOT home, never a profile's own
201/// home — `HERMES_HOME/profiles/<name>` is what the verb creates or removes,
202/// and upstream derives that profiles root from this variable.
203fn hermes_home(homes: &HarnessHomes) -> PathBuf {
204    // `HarnessHomes::hermes` addresses `state.db`; HERMES_HOME is its parent,
205    // the same derivation the read side uses.
206    homes
207        .hermes
208        .parent()
209        .map_or_else(|| PathBuf::from("."), Path::to_path_buf)
210}
211
212/// Perform one mutation: translate to the harness's own verb, run it, then
213/// re-read the row through the ORCH-10 loader.
214pub fn mutate(verb: ProfileVerb, mutation: &ProfileMutation) -> Result<ProfileMutationOutcome> {
215    if !supports_profile_control(&mutation.harness) {
216        return Err(ProfileControlError::Unsupported(unsupported_harness(
217            &mutation.harness,
218        )));
219    }
220    let name = mutation.name.trim();
221    if name.is_empty() {
222        return Err(ProfileControlError::Invalid(format!(
223            "`profiles.{}` needs the profile name to act on",
224            verb.as_str()
225        )));
226    }
227    if matches!(verb, ProfileVerb::Delete)
228        && (mutation.from.is_some() || mutation.workspace.is_some())
229    {
230        return Err(ProfileControlError::Invalid(
231            "`profiles.delete` sets no fields; pass definition fields to `profiles.create`".into(),
232        ));
233    }
234    // ORC-13: the orchestrator's verb is not a CLI subprocess but its own
235    // package's operator door, so it branches before the command table.
236    if mutation.harness == HarnessId::ORCHESTRATOR {
237        return orchestrator_mutate(verb, name, mutation);
238    }
239    let command = match mutation.harness.as_str() {
240        HarnessId::HERMES => hermes_command(verb, name, mutation)?,
241        HarnessId::OPENCLAW => openclaw_command(verb, name, mutation)?,
242        other => return Err(ProfileControlError::Unsupported(unsupported_harness(other))),
243    };
244    let ran = command.narrate();
245    command.run().map_err(ProfileControlError::Failed)?;
246    // The harness's own store is the answer: re-read, never echo the request.
247    let read = read_back(&mutation.harness, name, &mutation.homes, &ran)?;
248    match verb {
249        ProfileVerb::Delete => {
250            if read.is_some() {
251                return Err(ProfileControlError::Failed(format!(
252                    "`{ran}` reported success but `{name}` is still a {} profile",
253                    mutation.harness
254                )));
255            }
256            Ok(ProfileMutationOutcome {
257                harness: mutation.harness.clone(),
258                verb: verb.as_str().to_string(),
259                ran,
260                name: name.to_string(),
261                profile: None,
262                deleted: Some(true),
263            })
264        }
265        ProfileVerb::Create => {
266            let profile = read.ok_or_else(|| {
267                ProfileControlError::Failed(format!(
268                    "`{ran}` reported success but `{}` has no profile `{name}` afterwards",
269                    mutation.harness
270                ))
271            })?;
272            Ok(ProfileMutationOutcome {
273                harness: mutation.harness.clone(),
274                verb: verb.as_str().to_string(),
275                ran,
276                name: profile.name.clone(),
277                profile: Some(profile),
278                deleted: None,
279            })
280        }
281    }
282}
283
284// ---------------------------------------------------------------------------
285// The orchestrator — its own package's operator door (ORC-13)
286// ---------------------------------------------------------------------------
287
288/// One orchestrator profile mutation: through the package's door, then
289/// re-read through the ORCH-10 loader like every other harness's row.
290fn orchestrator_mutate(
291    verb: ProfileVerb,
292    name: &str,
293    mutation: &ProfileMutation,
294) -> Result<ProfileMutationOutcome> {
295    if mutation.from.is_some() {
296        return Err(ProfileControlError::Unsupported(
297            "an orchestrator profile is a FOLDER the package's `save()` writes from an empty \
298             record (`docs/ORCHESTRATOR-IR.md` §6); the operator door has no clone verb, so \
299             supercode refuses rather than dropping `from`"
300                .into(),
301        ));
302    }
303    if mutation.workspace.is_some() {
304        return Err(ProfileControlError::Unsupported(
305            "an orchestrator profile IS its own home (`<root>/profiles/<name>`), and where its \
306             worker runs is the `worker.cwd` key inside that folder's `config.yaml`, not a \
307             creation argument; supercode refuses rather than dropping `workspace`"
308                .into(),
309        ));
310    }
311    let root = mutation.homes.orchestrator.clone();
312    let op = match verb {
313        ProfileVerb::Create => "profiles.create",
314        ProfileVerb::Delete => "profiles.delete",
315    };
316    let args = serde_json::json!({ "name": name });
317    // Profile verbs act on the HOME, not inside a profile; `default` is the
318    // root folder and always exists, so it is the door's context.
319    let answer =
320        crate::orchestrator_door::call(&root, op, &args, "default").map_err(
321            |error| match error {
322                crate::orchestrator_door::DoorError::Refused(message) => {
323                    ProfileControlError::Failed(message)
324                }
325                crate::orchestrator_door::DoorError::Failed(message) => {
326                    ProfileControlError::Failed(message)
327                }
328            },
329        )?;
330    let ran = format!("{} [{}]", answer.ran, answer.door.as_str());
331    // The FOLDER is the answer, re-read through the same loader
332    // `profiles list` uses — never the door's echo of what it wrote.
333    let read = read_back(&mutation.harness, name, &mutation.homes, &ran)?;
334    match verb {
335        ProfileVerb::Delete => {
336            if read.is_some() {
337                return Err(ProfileControlError::Failed(format!(
338                    "`{ran}` reported success but `{name}` is still an orchestrator profile"
339                )));
340            }
341            Ok(ProfileMutationOutcome {
342                harness: mutation.harness.clone(),
343                verb: verb.as_str().to_string(),
344                ran,
345                name: name.to_string(),
346                profile: None,
347                deleted: Some(true),
348            })
349        }
350        ProfileVerb::Create => {
351            let profile = read.ok_or_else(|| {
352                ProfileControlError::Failed(format!(
353                    "`{ran}` reported success but the orchestrator has no profile `{name}` \
354                     afterwards"
355                ))
356            })?;
357            Ok(ProfileMutationOutcome {
358                harness: mutation.harness.clone(),
359                verb: verb.as_str().to_string(),
360                ran,
361                name: profile.name.clone(),
362                profile: Some(profile),
363                deleted: None,
364            })
365        }
366    }
367}
368
369/// The profile the harness's own store holds under `name` after the verb.
370///
371/// Both harnesses normalize the id they are handed (Hermes lowercases in
372/// `normalize_profile_name`, OpenClaw in `normalizeAgentId`), so an exact miss
373/// falls back to a case-insensitive match and the ROW's own spelling is what
374/// the outcome reports.
375fn read_back(
376    harness: &str,
377    name: &str,
378    homes: &HarnessHomes,
379    ran: &str,
380) -> Result<Option<ProfileRow>> {
381    let rows = crate::profiles::list_profiles(homes, Some(harness)).map_err(|error| {
382        ProfileControlError::Failed(format!(
383            "`{ran}` succeeded but the profile store could not be re-read: {error}"
384        ))
385    })?;
386    Ok(rows
387        .iter()
388        .find(|row| row.name == name)
389        .or_else(|| rows.iter().find(|row| row.name.eq_ignore_ascii_case(name)))
390        .cloned())
391}
392
393// ---------------------------------------------------------------------------
394// Hermes — `hermes profile create | delete` over HERMES_HOME
395// ---------------------------------------------------------------------------
396
397fn hermes_command(
398    verb: ProfileVerb,
399    name: &str,
400    mutation: &ProfileMutation,
401) -> Result<HarnessCommand> {
402    if mutation.workspace.is_some() {
403        return Err(ProfileControlError::Unsupported(
404            "a hermes profile IS its own home (`HERMES_HOME/profiles/<name>`); `hermes profile \
405             create` has no workspace flag, so supercode refuses rather than dropping the field"
406                .into(),
407        ));
408    }
409    let mut command = HarnessCommand::new(harness_program(HarnessId::HERMES)?);
410    command.env(
411        "HERMES_HOME",
412        hermes_home(&mutation.homes).to_string_lossy(),
413    );
414    command.arg("profile");
415    match verb {
416        ProfileVerb::Create => {
417            command.arg("create");
418            if let Some(from) = mutation
419                .from
420                .as_deref()
421                .map(str::trim)
422                .filter(|from| !from.is_empty())
423            {
424                command.args(["--clone-from", from]);
425            }
426            // The wrapper script lands in `~/.local/bin`, OUTSIDE the home the
427            // caller addressed; a programmatic create never installs an
428            // executable on the caller's PATH.
429            command.arg("--no-alias");
430            command.arg(name);
431        }
432        ProfileVerb::Delete => {
433            // `hermes profile delete` prompts for the profile name typed back
434            // unless `--yes` is passed; a prompt on a null stdin would hang.
435            command.args(["delete", "--yes", name]);
436        }
437    }
438    Ok(command)
439}
440
441// ---------------------------------------------------------------------------
442// OpenClaw — `openclaw agents add | delete` over its own state dir
443// ---------------------------------------------------------------------------
444
445fn openclaw_command(
446    verb: ProfileVerb,
447    name: &str,
448    mutation: &ProfileMutation,
449) -> Result<HarnessCommand> {
450    if mutation.from.is_some() {
451        return Err(ProfileControlError::Unsupported(
452            "`openclaw agents add` takes a workspace, a model and bindings; it has no clone / \
453             template source, so supercode refuses rather than dropping `from`"
454                .into(),
455        ));
456    }
457    let mut command = HarnessCommand::new(harness_program(HarnessId::OPENCLAW)?);
458    // Point the spawned CLI at the SAME state the read side addresses, using
459    // openclaw's own environment contract (`OPENCLAW_STATE_DIR` names the
460    // state dir; `OPENCLAW_CONFIG_PATH` names the config file inside it).
461    command.env(
462        "OPENCLAW_STATE_DIR",
463        mutation.homes.openclaw.to_string_lossy(),
464    );
465    command.env(
466        "OPENCLAW_CONFIG_PATH",
467        mutation
468            .homes
469            .openclaw
470            .join("openclaw.json")
471            .to_string_lossy(),
472    );
473    command.arg("agents");
474    match verb {
475        ProfileVerb::Create => {
476            let workspace = mutation
477                .workspace
478                .as_deref()
479                .map(str::trim)
480                .filter(|workspace| !workspace.is_empty())
481                .ok_or_else(|| {
482                    ProfileControlError::Invalid(
483                        "`openclaw agents add` requires the new agent's workspace directory in \
484                         non-interactive mode (its own message: \"Non-interactive agent creation \
485                         requires --workspace\"), so `profiles.create --harness openclaw` needs \
486                         `workspace`"
487                            .into(),
488                    )
489                })?;
490            command.args(["add", name, "--workspace", workspace]);
491            command.args(["--non-interactive", "--json"]);
492        }
493        ProfileVerb::Delete => {
494            // Without `--force` the verb prompts on a TTY and refuses off one
495            // ("Non-interactive session. Re-run with --force.").
496            command.args(["delete", name, "--force", "--json"]);
497        }
498    }
499    Ok(command)
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    fn homes(root: &Path) -> HarnessHomes {
507        HarnessHomes {
508            hermes: root.join("hermes_home/state.db"),
509            openclaw: root.join("openclaw_home"),
510            ..HarnessHomes::default()
511        }
512    }
513
514    #[test]
515    fn hermes_translates_the_uniform_row_onto_its_own_verb() {
516        let root = PathBuf::from("/tmp/orch21-unit");
517        let command = hermes_command(
518            ProfileVerb::Create,
519            "coder",
520            &ProfileMutation {
521                harness: HarnessId::HERMES.into(),
522                name: "coder".into(),
523                from: Some("default".into()),
524                homes: homes(&root),
525                ..ProfileMutation::default()
526            },
527        )
528        .unwrap();
529        assert_eq!(
530            command.narrate(),
531            "hermes profile create --clone-from default --no-alias coder"
532        );
533        // The ROOT home, never `profiles/<name>`: the verb makes that dir.
534        assert_eq!(
535            command.env,
536            vec![(
537                "HERMES_HOME".to_string(),
538                root.join("hermes_home").to_string_lossy().into_owned()
539            )]
540        );
541    }
542
543    #[test]
544    fn hermes_delete_is_non_interactive() {
545        let command = hermes_command(
546            ProfileVerb::Delete,
547            "coder",
548            &ProfileMutation {
549                harness: HarnessId::HERMES.into(),
550                name: "coder".into(),
551                homes: homes(&PathBuf::from("/tmp/orch21-unit")),
552                ..ProfileMutation::default()
553            },
554        )
555        .unwrap();
556        assert_eq!(command.narrate(), "hermes profile delete --yes coder");
557    }
558
559    #[test]
560    fn openclaw_carries_the_workspace_its_own_verb_demands() {
561        let root = PathBuf::from("/tmp/orch21-unit");
562        let command = openclaw_command(
563            ProfileVerb::Create,
564            "ops",
565            &ProfileMutation {
566                harness: HarnessId::OPENCLAW.into(),
567                name: "ops".into(),
568                workspace: Some("/tmp/orch21-unit/ws".into()),
569                homes: homes(&root),
570                ..ProfileMutation::default()
571            },
572        )
573        .unwrap();
574        assert_eq!(
575            command.narrate(),
576            "openclaw agents add ops --workspace /tmp/orch21-unit/ws --non-interactive --json"
577        );
578        assert!(
579            command.secrets.is_empty(),
580            "these verbs carry no credential"
581        );
582    }
583
584    #[test]
585    fn openclaw_create_without_a_workspace_is_refused_in_the_harnesss_own_words() {
586        let error = openclaw_command(
587            ProfileVerb::Create,
588            "ops",
589            &ProfileMutation {
590                harness: HarnessId::OPENCLAW.into(),
591                name: "ops".into(),
592                homes: homes(&PathBuf::from("/tmp/orch21-unit")),
593                ..ProfileMutation::default()
594            },
595        )
596        .unwrap_err();
597        assert!(matches!(error, ProfileControlError::Invalid(_)), "{error}");
598        assert!(error.to_string().contains("--workspace"), "{error}");
599    }
600
601    #[test]
602    fn openclaw_refuses_a_field_it_has_no_verb_for() {
603        let error = openclaw_command(
604            ProfileVerb::Create,
605            "ops",
606            &ProfileMutation {
607                harness: HarnessId::OPENCLAW.into(),
608                name: "ops".into(),
609                from: Some("main".into()),
610                workspace: Some("/tmp/ws".into()),
611                homes: homes(&PathBuf::from("/tmp/orch21-unit")),
612                ..ProfileMutation::default()
613            },
614        )
615        .unwrap_err();
616        assert!(
617            matches!(error, ProfileControlError::Unsupported(_)),
618            "{error}"
619        );
620    }
621
622    /// ORC-13: the orchestrator is CONTROLLED, and the two fields its own
623    /// verb has no home for are refused rather than dropped.
624    #[test]
625    fn the_orchestrator_is_controlled_and_refuses_the_fields_it_has_no_home_for() {
626        assert!(supports_profile_control(HarnessId::ORCHESTRATOR));
627        for (mutation, needle) in [
628            (
629                ProfileMutation {
630                    harness: HarnessId::ORCHESTRATOR.into(),
631                    name: "ops".into(),
632                    from: Some("default".into()),
633                    ..ProfileMutation::default()
634                },
635                "no clone verb",
636            ),
637            (
638                ProfileMutation {
639                    harness: HarnessId::ORCHESTRATOR.into(),
640                    name: "ops".into(),
641                    workspace: Some("/tmp/ws".into()),
642                    ..ProfileMutation::default()
643                },
644                "`worker.cwd` key",
645            ),
646        ] {
647            let error = orchestrator_mutate(ProfileVerb::Create, "ops", &mutation).unwrap_err();
648            assert!(
649                matches!(error, ProfileControlError::Unsupported(_)),
650                "{error}"
651            );
652            assert!(error.to_string().contains(needle), "{error}");
653        }
654    }
655
656    #[test]
657    fn codex_and_presets_refuse_every_mutating_verb() {
658        for (harness, needle) in [
659            (HarnessId::CODEX, "[profiles.<name>]"),
660            (HarnessId::SUPERCODE, "CODE"),
661        ] {
662            for verb in [ProfileVerb::Create, ProfileVerb::Delete] {
663                let error = mutate(
664                    verb,
665                    &ProfileMutation {
666                        harness: harness.into(),
667                        name: "review".into(),
668                        ..ProfileMutation::default()
669                    },
670                )
671                .unwrap_err();
672                assert!(
673                    matches!(error, ProfileControlError::Unsupported(_)),
674                    "{harness}: {error}"
675                );
676                assert!(error.to_string().contains(needle), "{harness}: {error}");
677            }
678        }
679    }
680
681    #[test]
682    fn a_harness_without_profiles_refuses_with_the_read_sides_sentence() {
683        let error = mutate(
684            ProfileVerb::Create,
685            &ProfileMutation {
686                harness: HarnessId::CLAUDE_CODE.into(),
687                name: "coder".into(),
688                ..ProfileMutation::default()
689            },
690        )
691        .unwrap_err();
692        assert!(
693            matches!(error, ProfileControlError::Unsupported(_)),
694            "{error}"
695        );
696        assert!(
697            error.to_string().contains("has no profile concept"),
698            "{error}"
699        );
700    }
701}