Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions crates/botloftd/src/agent/codex_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
));
}
}
5 changes: 4 additions & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>" } } } } } }`: 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.<nome>.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.<nome>.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.'<pasta>']`. `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.<nome>.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 |
Loading