From f9fc7a4a91035f928685501b12d0688815565b6a Mon Sep 17 00:00:00 2001
From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com>
Date: Sat, 10 Oct 2026 21:29:33 -0300
Subject: [PATCH] feat(settings): a switch to choose Antigravity and Codex
Settings has "Let me choose Antigravity and Codex", so turning the
experimental agents on no longer means editing config.toml. It writes
experimental_agents, applies at once, says what the risks are, and the
default agent goes back to Claude Code when it is switched off.
---
README.md | 7 +--
app/src/features/account/GeneralSettings.tsx | 9 ++++
app/src/features/account/settings.test.tsx | 21 ++++++++
app/src/features/account/useDaemonSettings.ts | 13 +++--
app/src/i18n/en/account.ts | 5 ++
app/src/i18n/es/account.ts | 5 ++
app/src/i18n/pt-BR/account.ts | 5 ++
app/src/lib/fake.ts | 9 ++++
app/src/lib/protocol.gen.ts | 9 +++-
app/src/store/app.ts | 2 +
crates/botloft-core/src/protocol/settings.rs | 6 +++
crates/botloftd/src/service/settings.rs | 27 +++++++++-
crates/botloftd/src/settings.rs | 47 +++++++++++++++++
crates/botloftd/src/supervisor/mod.rs | 40 ++++++++------
crates/botloftd/tests/settings.rs | 52 +++++++++++++++++--
docs/spec.md | 6 +--
16 files changed, 233 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index d919825e..ca1e24ef 100644
--- a/README.md
+++ b/README.md
@@ -117,9 +117,10 @@ like this:
| Usage meter and "Compact" | yes | an estimate of the conversation size; no compaction |
| Isolation between crews | yes | folders are fenced for its file tools, not for commands you allow |
-Claude Code is the full experience. Antigravity and Codex are off until you list them in Botloft's
-`config.toml` (`experimental_agents = ["agy"]`, `["codex"]` or both); then a new agent can be made on
-them, and Settings has "New agents run on". A Codex agent asks you before it changes a file or runs a
+Claude Code is the full experience. Antigravity and Codex are off until you turn on "Let me choose
+Antigravity and Codex" in Settings (or list them in Botloft's `config.toml`, `experimental_agents =
+["agy"]`, `["codex"]` or both); then a new agent can be made on them, and Settings has "New agents run
+on". A Codex agent asks you before it changes a file or runs a
command, like Claude Code, and shows the exact size of its conversation; it can still read any file.
It has its own model list and effort picker, and understands images. See [spec section 30](docs/spec.md) for what was
checked.
diff --git a/app/src/features/account/GeneralSettings.tsx b/app/src/features/account/GeneralSettings.tsx
index 054c988b..e28047e3 100644
--- a/app/src/features/account/GeneralSettings.tsx
+++ b/app/src/features/account/GeneralSettings.tsx
@@ -41,6 +41,15 @@ export function GeneralSettings() {
+
+ change({ showExperimentalAgents: on })}
+ />
+
{settings && enabledAgents.length > 1 && (
{
keepAwake: false,
approvalWaitMinutes: 60,
defaultAgent: "claude",
+ showExperimentalAgents: false,
}),
);
expect(
@@ -65,6 +66,26 @@ describe("settings", () => {
).toEqual([{ startWithWindows: false }, { keepAwake: false }]);
});
+ test("the experimental agents are switched on here, with what that means", async () => {
+ const { fake } = renderApp();
+ const dialog = await openSettings();
+ const show = within(dialog).getByRole("switch", {
+ name: "Let me choose Antigravity and Codex",
+ });
+ await waitFor(() => expect(show.hasAttribute("disabled")).toBe(false));
+ expect(dialog.textContent).toContain("new agents always run on Claude Code");
+ expect(within(dialog).queryByText("New agents run on")).toBeNull();
+
+ fireEvent.click(show);
+ expect(show.getAttribute("aria-checked")).toBe("true");
+ expect(dialog.textContent).toContain("can read any file on this computer");
+ // The choice of agent for new agents appears without waiting.
+ expect(await within(dialog).findByText("New agents run on")).toBeDefined();
+ expect(
+ fake.calls.filter((call) => call.method === "settings.update").map((c) => c.params),
+ ).toEqual([{ showExperimentalAgents: true }]);
+ });
+
test("the icon and the window at sign-in show only where they apply", async () => {
renderApp();
const dialog = await openSettings();
diff --git a/app/src/features/account/useDaemonSettings.ts b/app/src/features/account/useDaemonSettings.ts
index 08854d63..8cee1c54 100644
--- a/app/src/features/account/useDaemonSettings.ts
+++ b/app/src/features/account/useDaemonSettings.ts
@@ -15,6 +15,7 @@ export function useDaemonSettings(): {
const failed = useT().account.settings.saveFailed;
const settings = useApp((state) => state.settings);
const putSettings = useApp((state) => state.putSettings);
+ const putSystem = useApp((state) => state.putSystem);
const change = (update: SettingsUpdateParams) => {
const before = settings;
@@ -24,11 +25,17 @@ export function useDaemonSettings(): {
keepAwake: update.keepAwake ?? before.keepAwake,
approvalWaitMinutes: update.approvalWaitMinutes ?? before.approvalWaitMinutes,
defaultAgent: update.defaultAgent ?? before.defaultAgent,
+ showExperimentalAgents: update.showExperimentalAgents ?? before.showExperimentalAgents,
});
}
- attempt(failed, async () => putSettings(await api.call("settings.update", update))).then(
- (saved) => saved || putSettings(before),
- );
+ attempt(failed, async () => {
+ putSettings(await api.call("settings.update", update));
+ // The agents to choose from follow this setting; the status has no
+ // notification, so it is read again now.
+ if (update.showExperimentalAgents !== undefined) {
+ putSystem(await api.call("system.status"));
+ }
+ }).then((saved) => saved || putSettings(before));
};
return { settings, change };
diff --git a/app/src/i18n/en/account.ts b/app/src/i18n/en/account.ts
index 47c574aa..305de3a7 100644
--- a/app/src/i18n/en/account.ts
+++ b/app/src/i18n/en/account.ts
@@ -246,6 +246,11 @@ export const account = {
pages: "Parts of Settings",
general: "General",
defaultAgent: "New agents run on",
+ experimentalAgents: "Experimental agents",
+ showExperimental: "Let me choose Antigravity and Codex",
+ showExperimentalOff: "Off: new agents always run on Claude Code.",
+ showExperimentalOn:
+ "Still being tested. An agent that runs on Codex can read any file on this computer, and one that runs on Antigravity cannot ask you before it acts. Agents already made on them keep working either way.",
defaultAgentHint:
"Used for new agents, the chief of a new crew and the agents a template adds. Each agent keeps the engine it was made with.",
chat: "Chat",
diff --git a/app/src/i18n/es/account.ts b/app/src/i18n/es/account.ts
index ab87667c..131441ec 100644
--- a/app/src/i18n/es/account.ts
+++ b/app/src/i18n/es/account.ts
@@ -252,6 +252,11 @@ export const account: Messages["account"] = {
pages: "Partes de la configuración",
general: "General",
defaultAgent: "Los agentes nuevos funcionan con",
+ experimentalAgents: "Agentes experimentales",
+ showExperimental: "Poder elegir Antigravity y Codex",
+ showExperimentalOff: "Apagado: los agentes nuevos siempre funcionan con Claude Code.",
+ showExperimentalOn:
+ "Todavía en pruebas. Un agente que funciona con Codex puede leer cualquier archivo de este equipo, y uno que funciona con Antigravity no puede preguntarte antes de actuar. Los agentes ya creados con ellos siguen funcionando igual.",
defaultAgentHint:
"Vale para los agentes nuevos, el jefe de un equipo nuevo y los agentes que añade una plantilla. Cada agente conserva el motor con el que se creó.",
chat: "Chat",
diff --git a/app/src/i18n/pt-BR/account.ts b/app/src/i18n/pt-BR/account.ts
index 60a14949..99e64ad4 100644
--- a/app/src/i18n/pt-BR/account.ts
+++ b/app/src/i18n/pt-BR/account.ts
@@ -250,6 +250,11 @@ export const account: Messages["account"] = {
pages: "Partes das configurações",
general: "Geral",
defaultAgent: "Os agentes novos rodam em",
+ experimentalAgents: "Agentes experimentais",
+ showExperimental: "Deixar-me escolher Antigravity e Codex",
+ showExperimentalOff: "Desligado: os agentes novos sempre rodam no Claude Code.",
+ showExperimentalOn:
+ "Ainda em teste. Um agente que roda no Codex pode ler qualquer arquivo deste computador, e um que roda no Antigravity não consegue perguntar a você antes de agir. Os agentes que já foram feitos neles continuam funcionando de qualquer jeito.",
defaultAgentHint:
"Vale para agentes novos, o chefe de uma equipe nova e os agentes que um modelo de equipe adiciona. Cada agente guarda o motor com que foi criado.",
chat: "Conversa",
diff --git a/app/src/lib/fake.ts b/app/src/lib/fake.ts
index 7ca4d414..9e7e18ff 100644
--- a/app/src/lib/fake.ts
+++ b/app/src/lib/fake.ts
@@ -79,6 +79,7 @@ export class FakeBotloft implements BotloftApi {
keepAwake: true,
approvalWaitMinutes: 60,
defaultAgent: "claude",
+ showExperimentalAgents: false,
};
/** Folders the owner sent to the Recycle Bin with a delete, in order. */
readonly recycled: string[] = [];
@@ -279,7 +280,15 @@ export class FakeBotloft implements BotloftApi {
keepAwake: change.keepAwake ?? this.settings.keepAwake,
approvalWaitMinutes: change.approvalWaitMinutes ?? this.settings.approvalWaitMinutes,
defaultAgent: change.defaultAgent ?? this.settings.defaultAgent,
+ showExperimentalAgents:
+ change.showExperimentalAgents ?? this.settings.showExperimentalAgents,
};
+ if (change.showExperimentalAgents !== undefined) {
+ this.system = {
+ ...this.system,
+ enabledAgents: change.showExperimentalAgents ? ["claude", "agy", "codex"] : ["claude"],
+ };
+ }
return this.settings;
},
"archive.list": () => ({
diff --git a/app/src/lib/protocol.gen.ts b/app/src/lib/protocol.gen.ts
index f46a3769..a43d5322 100644
--- a/app/src/lib/protocol.gen.ts
+++ b/app/src/lib/protocol.gen.ts
@@ -1870,7 +1870,12 @@ approvalWaitMinutes: number,
* The agent bots are made for when nothing says otherwise: new bots,
* the chief of a new crew, the bots of a template (spec 30).
*/
-defaultAgent: AgentKind, };
+defaultAgent: AgentKind,
+/**
+ * The experimental agents (Antigravity and Codex) can be chosen for
+ * new bots. Off, bots that already use one keep working (spec 30).
+ */
+showExperimentalAgents: boolean, };
/**
* The settings to change; the ones left out stay as they are.
@@ -1879,7 +1884,7 @@ export type SettingsUpdateParams = { startWithWindows?: boolean, keepAwake?: boo
/**
* From 1 to [`APPROVAL_WAIT_MAX_MINUTES`].
*/
-approvalWaitMinutes?: number, defaultAgent?: AgentKind, };
+approvalWaitMinutes?: number, defaultAgent?: AgentKind, showExperimentalAgents?: boolean, };
/**
* When a routine runs (spec 20.2), kept as structure rather than cron text
diff --git a/app/src/store/app.ts b/app/src/store/app.ts
index 1a108199..0d1ac08c 100644
--- a/app/src/store/app.ts
+++ b/app/src/store/app.ts
@@ -102,6 +102,7 @@ export interface AppState {
putRoutine(routine: Routine): void;
putBrowser(browser: BrowserState): void;
putSettings(settings: Settings | null): void;
+ putSystem(system: SystemStatus): void;
}
export type AppStore = StoreApi;
@@ -202,6 +203,7 @@ export function createAppStore(api: BotloftApi): AppStore {
putRoutine: (routine) => set((state) => withRoutine(state, routine)),
putBrowser: (browser) => set((state) => withBrowser(state, browser)),
putSettings: (settings) => set({ settings }),
+ putSystem: (system) => set({ system }),
putDelivery: (delivery) =>
set((state) => ({ deliveries: { ...state.deliveries, [delivery.messageId]: delivery } })),
};
diff --git a/crates/botloft-core/src/protocol/settings.rs b/crates/botloft-core/src/protocol/settings.rs
index 8fcb124b..9fd00ac1 100644
--- a/crates/botloft-core/src/protocol/settings.rs
+++ b/crates/botloft-core/src/protocol/settings.rs
@@ -21,6 +21,9 @@ pub struct Settings {
/// The agent bots are made for when nothing says otherwise: new bots,
/// the chief of a new crew, the bots of a template (spec 30).
pub default_agent: AgentKind,
+ /// The experimental agents (Antigravity and Codex) can be chosen for
+ /// new bots. Off, bots that already use one keep working (spec 30).
+ pub show_experimental_agents: bool,
}
/// The settings to change; the ones left out stay as they are.
@@ -41,6 +44,9 @@ pub struct SettingsUpdateParams {
#[serde(default)]
#[cfg_attr(test, ts(optional))]
pub default_agent: Option,
+ #[serde(default)]
+ #[cfg_attr(test, ts(optional))]
+ pub show_experimental_agents: Option,
}
/// The longest a permission request may wait for the owner: a day.
diff --git a/crates/botloftd/src/service/settings.rs b/crates/botloftd/src/service/settings.rs
index 1b300af2..62cae9c8 100644
--- a/crates/botloftd/src/service/settings.rs
+++ b/crates/botloftd/src/service/settings.rs
@@ -1,7 +1,9 @@
//! `settings.get` and `settings.update` (spec 11.2): what the owner
//! changes in the app's Settings.
-use botloft_core::protocol::{APPROVAL_WAIT_MAX_MINUTES, Settings, SettingsUpdateParams};
+use botloft_core::protocol::{
+ APPROVAL_WAIT_MAX_MINUTES, AgentKind, Settings, SettingsUpdateParams,
+};
use tracing::info;
use super::{ApiError, ApiResult};
@@ -38,6 +40,29 @@ pub fn update(daemon: &Daemon, params: SettingsUpdateParams) -> ApiResult before.approval_wait_minutes {
daemon.supervisor.workspace_settings_changed();
}
diff --git a/crates/botloftd/src/settings.rs b/crates/botloftd/src/settings.rs
index f37bb1b6..1d523cdc 100644
--- a/crates/botloftd/src/settings.rs
+++ b/crates/botloftd/src/settings.rs
@@ -12,6 +12,10 @@ use tokio::sync::watch;
use crate::config::Config;
+/// The agents the "experimental agents" setting switches on, by their name
+/// in `config.toml`.
+pub const EXPERIMENTAL_AGENTS: [&str; 2] = ["agy", "codex"];
+
pub struct LiveSettings {
/// Where they are saved; `None` keeps them in memory (tests).
file: Option,
@@ -34,6 +38,7 @@ impl LiveSettings {
approval_wait_minutes: minutes,
// A name this build does not know is Claude Code.
default_agent: config.default_agent.parse().unwrap_or(AgentKind::Claude),
+ show_experimental_agents: !config.experimental_agents.is_empty(),
};
Self {
file,
@@ -91,6 +96,9 @@ impl LiveSettings {
if let Some(agent) = update.default_agent {
current.settings.default_agent = agent;
}
+ if let Some(show) = update.show_experimental_agents {
+ current.settings.show_experimental_agents = show;
+ }
Ok(current.settings)
}
@@ -125,6 +133,13 @@ fn write(file: &Path, update: SettingsUpdateParams) -> io::Result<()> {
if let Some(agent) = update.default_agent {
set(config.as_table_mut(), "default_agent", agent.as_str());
}
+ if let Some(show) = update.show_experimental_agents {
+ let mut names = toml_edit::Array::new();
+ if show {
+ names.extend(EXPERIMENTAL_AGENTS);
+ }
+ set(config.as_table_mut(), "experimental_agents", names);
+ }
if let Some(minutes) = update.approval_wait_minutes {
let bots = config["bots"]
.or_insert(toml_edit::table())
@@ -173,6 +188,7 @@ mod tests {
keep_awake: false,
approval_wait_minutes: 30,
default_agent: AgentKind::Claude,
+ show_experimental_agents: false,
}
);
assert!(awake.has_changed().expect("sender") && !*awake.borrow_and_update());
@@ -190,6 +206,7 @@ mod tests {
keep_awake: Some(true),
approval_wait_minutes: Some(240),
default_agent: None,
+ show_experimental_agents: None,
})
.expect("save");
let text = std::fs::read_to_string(&file).expect("read");
@@ -208,11 +225,41 @@ mod tests {
keep_awake: true,
approval_wait_minutes: 240,
default_agent: AgentKind::Claude,
+ show_experimental_agents: false,
}
);
assert_eq!(settings.approval_wait(), Duration::from_secs(4 * 3600));
}
+ #[test]
+ fn the_experimental_agents_are_listed_in_the_file_and_taken_out_again() {
+ let dir = tempfile::tempdir().expect("dir");
+ let file = dir.path().join("config.toml");
+ let settings = LiveSettings::new(Some(file.clone()), &Config::default());
+ assert!(!settings.get().show_experimental_agents);
+ let on = SettingsUpdateParams {
+ show_experimental_agents: Some(true),
+ ..SettingsUpdateParams::default()
+ };
+ assert!(settings.save(on).expect("save").show_experimental_agents);
+ let config = Config::parse(&std::fs::read_to_string(&file).expect("read")).expect("parse");
+ assert_eq!(config.experimental_agents, ["agy", "codex"]);
+ // A daemon started now shows them.
+ assert!(
+ LiveSettings::new(None, &config)
+ .get()
+ .show_experimental_agents
+ );
+
+ let off = SettingsUpdateParams {
+ show_experimental_agents: Some(false),
+ ..SettingsUpdateParams::default()
+ };
+ assert!(!settings.save(off).expect("save").show_experimental_agents);
+ let config = Config::parse(&std::fs::read_to_string(&file).expect("read")).expect("parse");
+ assert!(config.experimental_agents.is_empty());
+ }
+
#[test]
fn a_new_wait_goes_in_the_bots_table() {
let dir = tempfile::tempdir().expect("dir");
diff --git a/crates/botloftd/src/supervisor/mod.rs b/crates/botloftd/src/supervisor/mod.rs
index 5fcca153..3531bebf 100644
--- a/crates/botloftd/src/supervisor/mod.rs
+++ b/crates/botloftd/src/supervisor/mod.rs
@@ -48,6 +48,9 @@ pub struct Supervisor {
daemon: Weak,
runtime: Arc,
settings: SupervisorSettings,
+ /// The experimental agents the owner switched on: `settings` has what
+ /// `config.toml` said at start, the Settings screen changes this.
+ experimental: Mutex>,
events: broadcast::Sender,
inner: Mutex,
/// Generations are unique across bots and daemon restarts, so a client
@@ -96,6 +99,7 @@ impl Supervisor {
Self {
daemon,
runtime,
+ experimental: Mutex::new(settings.experimental_agents.clone()),
settings,
events,
inner: Mutex::default(),
@@ -132,23 +136,29 @@ impl Supervisor {
}
/// Whether bots of `kind` may be made: Claude Code always, an
- /// experimental agent only when the owner listed it in `config.toml`
- /// (spec 30).
+ /// experimental agent only when the owner switched it on, in Settings
+ /// or by listing it in `config.toml` (spec 30).
pub fn agent_enabled(&self, kind: botloft_core::protocol::AgentKind) -> bool {
use botloft_core::protocol::AgentKind;
- match kind {
- AgentKind::Claude => true,
- AgentKind::Agy => self
- .settings
- .experimental_agents
- .iter()
- .any(|name| name == "agy"),
- AgentKind::Codex => self
- .settings
- .experimental_agents
- .iter()
- .any(|name| name == "codex"),
- }
+ let name = match kind {
+ AgentKind::Claude => return true,
+ AgentKind::Agy => "agy",
+ AgentKind::Codex => "codex",
+ };
+ self.experimental
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ .iter()
+ .any(|listed| listed == name)
+ }
+
+ /// Switches the experimental agents on (the names listed) or off. Bots
+ /// that already exist keep their agent.
+ pub fn set_experimental_agents(&self, names: Vec) {
+ *self
+ .experimental
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner()) = names;
}
/// The `agy_path` of the config; empty looks in the usual places.
diff --git a/crates/botloftd/tests/settings.rs b/crates/botloftd/tests/settings.rs
index 8ec14d56..9350716a 100644
--- a/crates/botloftd/tests/settings.rs
+++ b/crates/botloftd/tests/settings.rs
@@ -15,7 +15,7 @@ async fn settings_are_read_and_changed_one_at_a_time() {
let settings = app.call("settings.get", json!(null)).await.expect("get");
assert_eq!(
settings,
- json!({ "startWithWindows": true, "keepAwake": true, "approvalWaitMinutes": 0, "defaultAgent": "claude" })
+ json!({ "startWithWindows": true, "keepAwake": true, "approvalWaitMinutes": 0, "defaultAgent": "claude", "showExperimentalAgents": false })
);
let changed = app
@@ -24,7 +24,7 @@ async fn settings_are_read_and_changed_one_at_a_time() {
.expect("update");
assert_eq!(
changed,
- json!({ "startWithWindows": true, "keepAwake": false, "approvalWaitMinutes": 0, "defaultAgent": "claude" })
+ json!({ "startWithWindows": true, "keepAwake": false, "approvalWaitMinutes": 0, "defaultAgent": "claude", "showExperimentalAgents": false })
);
assert!(!*t.daemon.settings.keep_awake().borrow());
@@ -34,7 +34,7 @@ async fn settings_are_read_and_changed_one_at_a_time() {
.expect("update");
assert_eq!(
changed,
- json!({ "startWithWindows": false, "keepAwake": false, "approvalWaitMinutes": 0, "defaultAgent": "claude" })
+ json!({ "startWithWindows": false, "keepAwake": false, "approvalWaitMinutes": 0, "defaultAgent": "claude", "showExperimentalAgents": false })
);
assert_eq!(
app.call("settings.get", json!(null)).await.expect("get"),
@@ -47,3 +47,49 @@ async fn settings_are_read_and_changed_one_at_a_time() {
);
}
}
+
+#[tokio::test]
+async fn the_experimental_agents_are_switched_on_and_off_in_settings() {
+ let t = TestDaemon::start().await;
+ std::fs::create_dir_all(&t.paths.home).expect("home");
+ let mut app = t.session().await;
+ let agents = |status: serde_json::Value| status["enabledAgents"].clone();
+ let status = app
+ .call("system.status", json!(null))
+ .await
+ .expect("status");
+ assert_eq!(agents(status), json!(["claude"]));
+
+ let on = app
+ .call("settings.update", json!({ "showExperimentalAgents": true }))
+ .await
+ .expect("on");
+ assert_eq!(on["showExperimentalAgents"], json!(true));
+ let status = app
+ .call("system.status", json!(null))
+ .await
+ .expect("status");
+ assert_eq!(agents(status), json!(["claude", "agy", "codex"]));
+
+ app.call("settings.update", json!({ "defaultAgent": "codex" }))
+ .await
+ .expect("default");
+ let off = app
+ .call(
+ "settings.update",
+ json!({ "showExperimentalAgents": false }),
+ )
+ .await
+ .expect("off");
+ assert_eq!(off["showExperimentalAgents"], json!(false));
+ assert_eq!(
+ off["defaultAgent"],
+ json!("claude"),
+ "no default that cannot be chosen"
+ );
+ let status = app
+ .call("system.status", json!(null))
+ .await
+ .expect("status");
+ assert_eq!(agents(status), json!(["claude"]));
+}
diff --git a/docs/spec.md b/docs/spec.md
index 92a5d64d..1b87d874 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -145,7 +145,7 @@ port = 45710 # só 127.0.0.1 no MVP
workspaces_root = "" # vazio = %USERPROFILE%\Botloft
claude_path = "" # vazio = resolver pelo PATH
agy_path = "" # vazio = %LOCALAPPDATA%\agy\bin e o PATH (seção 30)
-experimental_agents = [] # ["agy"] liga os bots Antigravity (seção 30)
+experimental_agents = [] # ["agy", "codex"] liga os agentes experimentais (seção 30); a tela de Configurações escreve isto
default_agent = "claude" # o agente dos bots novos; muda nas Configurações (seção 30)
start_with_windows = true # o daemon sobe quando o dono entra no Windows (14)
keep_awake = true # impede suspensão enquanto houver bot busy
@@ -2663,7 +2663,7 @@ Descoberta e saúde (binário, versão mínima, login, comando de entrar); lanç
### 30.3 Antigravity (`agy`), experimental
-- **Ligar:** `experimental_agents = ["agy"]` no `config.toml`; sem isso, `bots.create` com `agent: "agy"` dá `-32004`. O app ainda não tem o seletor (A4).
+- **Ligar:** `experimental_agents = ["agy"]` no `config.toml`, ou o interruptor "Deixar-me escolher Antigravity e Codex" em Configurações (`settings.update { showExperimentalAgents }`, que escreve `["agy", "codex"]` ou `[]` e vale na hora); sem isso, `bots.create` com `agent: "agy"` dá `-32004`. Desligar não mexe nos agentes que já existem, e o agente padrão volta para o Claude Code se era um experimental.
- **Casa própria:** o processo roda com `USERPROFILE` e `HOME` em `\.botloft\agy-home`. Lá o Botloft escreve a cada start `.gemini/config/mcp_config.json` (o servidor `botloft`, com o token do bot no cabeçalho) e `.gemini/antigravity-cli/settings.json`. Assim o `GEMINI.md`, os servidores MCP e as configurações pessoais do dono ficam de fora (o login não mora ali e continua valendo).
- **Regras do bot:** o texto que o Claude lê em `.claude/rules/botloft.md` é copiado para `AGENTS.md` na pasta do bot, que o `agy` carrega.
- **Permissões:** em `-p` o `agy` não pergunta nada (30.4), então o Botloft decide de antemão: `allow` só `mcp(botloft/*)`; `deny` de `read_file()` e `write_file()` para cada pasta cercada (a mesma lista da seção 7.5: dados do Botloft e pastas de outras crews). Comandos de shell são negados pelo próprio `agy`, e o `result` traz `denied_actions`. O dono ainda não tem como liberar um comando.
@@ -2680,7 +2680,7 @@ Descoberta e saúde (binário, versão mínima, login, comando de entrar); lanç
### 30.3.2 Codex (`codex app-server`), experimental
-- **Ligar:** `experimental_agents = ["codex"]` no `config.toml`; `codex_path` aponta para o executável (vazio procura em `%LOCALAPPDATA%\OpenAI\Codex\bin\\codex.exe` e no PATH).
+- **Ligar:** como o AGY (interruptor em Configurações ou `experimental_agents = ["codex"]` no `config.toml`); `codex_path` aponta para o executável (vazio procura em `%LOCALAPPDATA%\OpenAI\Codex\bin\\codex.exe` e no PATH).
- **Processo:** `codex app-server -c features.apps=false -c features.hooks=false -c features.plugins=false -c notify=[] -c mcp_servers..enabled=false` (um para cada MCP do `config.toml` do dono), na `CODEX_HOME` do dono: o login vale sem copiar o token. O invólucro `CodexControl` (`agent/codex_wire.rs`) recebe a linha neutra do Botloft e a vira `turn/start` quando a thread existe e nenhum turno roda; as mensagens esperam em fila.
- **Aperto de mão:** `initialize`, `initialized`, `thread/start` (ou `thread/resume` com o id guardado; se não existir, `thread/start`). A thread leva `cwd` (a pasta do bot), `approvalPolicy: "never"`, `sandbox: "read-only"`, `model` (`Bot.agentModel`) e o servidor MCP `botloft` com o token deste processo em `http_headers`.
- **Eventos:** `item/started` `userMessage` é o recibo de leitura da mensagem mais antiga escrita e grava a thread a retomar; `item/agentMessage/delta` é o texto ao vivo e `item/completed` `agentMessage` fecha a resposta; `commandExecution`, `fileChange` e `mcpToolCall` aparecem como `Bash`, `Write`/`Edit` e `mcp____`; `thread/tokenUsage/updated` dá o contexto (a última requisição e `modelContextWindow`) e os tokens do turno (o total da thread menos o que era no começo); `turn/completed` fecha o turno com `durationMs` e o erro.