AgentHistory._filter_sensitive_data_from_dict only recursed one level into
lists: a string or dict directly inside a list was filtered, but a list inside
a list (or any deeper container) was returned untouched. An action parameter
shaped like {"input": {"rows": [["token-123"]]}} was therefore written to the
history file with the secret intact.
Replace the hand-rolled one-level walk with a single recursive value
dispatcher that handles str/dict/list/tuple, mirroring how
Registry._replace_sensitive_data walks params on the injection side, so the
redaction and injection paths cover the same container shapes.
Fixes#5623
- Reject valid base64 that isn't actually an image (e.g. aGVsbG8= -> b'hello'),
and reject correct image bytes under the wrong extension (magic mismatch), so
upload flows never receive a corrupt file.
- Register a new file in self.files only after a successful write, so a rejected
binary write leaves no ghost entry in list_files()/state.
- Binary files cannot be appended (base64 fragments would corrupt).
- Strengthen tests: content-integrity round-trip, non-image base64, wrong-magic,
and no-ghost assertions.
Upload-validation flows were unwinnable: the agent's file tools are text-only, so
'upload a photo/gif' tasks could never be attempted. write_file now accepts small
image extensions where content is the base64 of a valid image (a 1x1 PNG is ~92
base64 chars, ~23 tokens); the bytes are decoded and written to disk so the file
is a real, uploadable image.
- Base64BinaryFile writes decoded BYTES (not base64 text) to disk; read() returns
a '[binary png file, N bytes]' stub so base64 never enters the prompt (describe()
runs every step).
- Strict base64 decode: invalid content is rejected with an error and no file is
created, so a corrupt 'image' can't slip through upload's file_size>0 check.
- Registered in _file_types (so upload_file.get_file resolves it by basename) and
from_state (so restored sessions keep it); pruned from UNSUPPORTED_BINARY_EXTENSIONS.
- Text write_file, PDF/DOCX rendering, and other binary rejections unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: password, file, hidden, payment (cc-*) and one-time-code
fields never get their live value stored on EnhancedSnapshotNode, so it
cannot leak through __json__. Checkbox and radio inputs now show their live
checked state as the checked attribute.
DOM attributes only carry the static value=... written in the HTML. When
JavaScript, autofill, or a framework fills a field, the value lives in the
element property, which DOMSnapshot exposes as inputValue/textValue. The
agent saw such fields as empty and retyped or skipped them (#5647).
Read inputValue, textValue, and inputChecked from the snapshot, keep them on
EnhancedSnapshotNode, and surface the live value as the value attribute for
input and textarea nodes. Password, file, and hidden inputs are left alone.
Fixes#5647
AsyncOpenAI falls back to OPENAI_API_KEY when api_key is unset, so
ChatOrcaRouter(model=...) with no key authenticated its requests to
api.orcarouter.ai with the user's OpenAI credentials. The shipped example
hit this path whenever ORCAROUTER_API_KEY was unset, and .env.example
documented a variable that nothing read.
Resolve api_key from the constructor then ORCAROUTER_API_KEY, and raise
ModelProviderError(401) when neither is set, matching ChatMistral.