diff --git a/crates/botloftd/src/agent/codex_decoder.rs b/crates/botloftd/src/agent/codex_decoder.rs index 17d6a6bd..3e8a5bda 100644 --- a/crates/botloftd/src/agent/codex_decoder.rs +++ b/crates/botloftd/src/agent/codex_decoder.rs @@ -86,14 +86,24 @@ impl OutputDecoder for CodexDecoder { } } -/// Whether `text` names a folder of `fenced` (slashes and case aside). +/// Whether `text` names a folder of `fenced` (slashes and case aside). A +/// folder whose name only starts like a fenced one (`Docs` and `Docs2`, +/// `Ana` and `Ana2`) is another folder. fn touches(fenced: &[PathBuf], text: &str) -> bool { let flat = |value: &str| value.replace('\\', "/").to_lowercase(); let text = flat(text); fenced .iter() .map(|folder| flat(&folder.to_string_lossy())) - .any(|folder| !folder.is_empty() && text.contains(&folder)) + .filter(|folder| !folder.is_empty()) + .any(|folder| { + text.match_indices(&folder).any(|(at, _)| { + text[at + folder.len()..] + .chars() + .next() + .is_none_or(|next| !(next.is_alphanumeric() || matches!(next, '_' | '-' | '.'))) + }) + }) } impl CodexDecoder { @@ -508,4 +518,19 @@ mod tests { assert!(!touches(&fenced, r"C:\Users\Ana\Botloft\Mine\a.txt")); assert!(!touches(&[], "anything")); } + + #[test] + fn a_folder_that_only_starts_like_a_fenced_one_is_not_caught() { + let fenced = vec![PathBuf::from(r"C:\Users\Ana\Botloft\Suite")]; + assert!(!touches(&fenced, r"C:\Users\Ana\Botloft\Suite2\Ana\a.txt")); + assert!(!touches(&fenced, r"C:\Users\Ana\Botloft\Suite.old\a.txt")); + assert!(touches(&fenced, r"C:\Users\Ana\Botloft\Suite\Ana\a.txt")); + assert!(touches(&fenced, r"dir C:\Users\Ana\Botloft\Suite")); + assert!(touches(&fenced, r"type 'C:\Users\Ana\Botloft\Suite'")); + // A fenced mention still counts after a harmless look-alike. + assert!(touches( + &fenced, + r"copy C:\Users\Ana\Botloft\Suite2\a C:\Users\Ana\Botloft\Suite\b" + )); + } } diff --git a/docs/spec.md b/docs/spec.md index 92a5d64d..5029d08b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -2724,4 +2724,7 @@ Um agente só roda bots quando o Botloft impõe o que a seção 7.5 impõe no Cl | `codex`: MCP por conversa | `thread/start { config: { mcp_servers: { botloft: { url, http_headers: { Authorization: "Bearer " } } } } } }`: o servidor viu `initialize`, `tools/list` e `tools/call` com o cabeçalho; a ferramenta voltou na resposta. O MCP do dono fica de fora com `-c mcp_servers..enabled=false` e `-c features.apps=false` (o `mcp_servers={}` **não** remove) | feito | | `codex`: instruções e imagem | `AGENTS.md` na pasta de trabalho foi lido (o bot disse o codinome); `localImage` deixou o bot ver uma imagem (respondeu "Blue") | feito | | `codex`: configuração pessoal | O app-server carrega `~/.codex/config.toml` do dono: o MCP `node_repl`, os plugins e a configuração de `notify`. `-c features.apps=false -c features.hooks=false -c features.plugins=false -c mcp_servers..enabled=false -c notify=[]` tirou todos os MCP e plugins; ficaram as skills da pasta pessoal (44 para 36). Cada pasta de trabalho nova foi **gravada em `~/.codex/config.toml`** como `[projects.'']`. `CODEX_HOME` numa pasta do bot sem o `config.toml` do dono tira tudo isso, mas leva o login junto: um `auth.json` ligado por hard link funcionou, e uma cópia arriscaria invalidar o token do dono ao renovar (não testado) | feito | -| `codex`: retomar conversa, erros de login e de limite, `turn/steer`, compactação | **Não visto** | a fazer (A3) | +| `codex`: retomar conversa, erros de login e de limite, compactação | Feitos e vistos com o `codex` real (A3b a A3d): retomada pelo `thread/resume`, `unauthorized` vira erro de login, `usageLimitExceeded` vira limite (o plano gratuito chegou a ele em 2026-10-10 e o bot ficou em limite, como deve), compactação por `thread/compact/start`. `turn/steer` **não visto** | feito | +| `codex`: o que o dono usa pelo Botloft | Visto em 2026-10-10 com o `codex-cli` 0.148.0-alpha.9 e o plano gratuito: o chefe sugere um agente com `suggest_bot` (cartão, aprovação e agente criado em Codex); o navegador abre uma página depois da aprovação do site; `desktop_windows` sem permissão dada não age; mensagens e tarefas entre dois agentes Codex chegam e são concluídas; recusar uma edição não deixa arquivo, permitir deixa, o cartão traz "sempre permitir" (a regra vale para aquele arquivo e aquela ferramenta, como no Claude Code); o modo `plan` não escreve. O modo `accept_edits` e o comando só de leitura **não foram vistos**: o limite do plano acabou antes | feito, com esses dois a ver | +| `codex`: leitura de arquivos | Segue **aberta**: o sandbox `read-only` deixa ler qualquer arquivo sem perguntar (`C:\Windows\win.ini` foi lido por comando). Um perfil de permissão (`default_permissions` e `permissions..filesystem` com `deny`) só vale com `windows.sandbox = "elevated"`, que precisa da configuração de administrador do Codex; mesmo onde ela existe, pastas novas dentro do perfil do usuário ficam ilegíveis para o usuário do sandbox (a pasta de trabalho falhou com `CreateProcessWithLogonW failed: 267`). Por isso o aviso do diálogo continua ("ainda pode ler qualquer arquivo") | não resolvido | +| `codex`: cerca por nome parecido | A cerca de pastas casava por texto: uma crew `Suite` fechava a pasta `Suite2\Ana` e a edição era negada sem cartão. Agora o nome da pasta tem que terminar ali (`Suite2` e `Suite.old` são outras pastas) | feito |