June 10, 2026
ToolResult contract lands across the ability fleet
A coordinated migration closes the TKT-882 ToolResult contract sweep across the ability surface
A coordinated migration closes the TKT-882 ToolResult contract sweep across the ability surface. Every ability now returns ok()/err() only — the code=“error” mechanical-wrap marker is gone, stable kebab-case codes carry hints and valid= ladders, and success bodies are structured JSON (or text passthrough) rather than prose-dressed blobs. The dispatcher owns ToolResult rendering, ordinal injection for rich cards, and the unhandled-exception mapping. Affected abilities in this drop include save_graph, save_pattern, find_skills, find_tools, mcp_manager, file_permissions, file_write, search_files, list, timer, news, contacts, document, calendar, email, ubiquiti, chalie_docs, web_search, web_browse, browser, search, place, schedule, memory, programming_docs_search, mcp* passthrough, and skill_manager. The _MCPAbility synthetic proxy maps transport failures to mcp-unreachable (naming the server), unknown tool names to mcp-unknown-tool, and isError=true to mcp-tool-error instead of silently rendering remote failures as status=success prose. web_download drops its redundant local is_private_url pre-check and adopts web_fetch’s single SSRF guard behind stream_to_file with a 100MB in-stream cap mapped to code=too-large.
The shared CapabilityAbility base (TKT-883) replaces the copy-pasted delegation block in home/ubiquiti/contacts/email/calendar. The base owns capability loading, not-connected / unknown-action / handler-unavailable errors, handler dispatch, and ToolResult.ok(dict) wrapping; concrete tools declare CAPABILITY_KEY, ACTION_HANDLERS, DEFAULT_ACTION, and NOT_CONNECTED_HINT. Contacts is the exemplar — ~140 lines of bespoke delegation and a dead _rich_media_ordinal rich path collapse to four ClassVars with metadata byte-identical so abilities.sqlite is NOT rebuilt. The home ability adds an entity guardrail that resolves entity_id / automation_id against the live HA states list BEFORE delegating; an unknown id returns code=unknown-entity with the closest real ids as candidates so a weak model self-corrects instead of no-op’ing on a guessed-wrong entity_id.
Service-layer extractions move engines out of the ability module. Memory recall (handlers, episode engine, dynamic radius, data-graph search, reflection expansion, telemetry) lives in services/memory_retrieval.py with the ability as a thin adapter; api/updates.py now imports handle_store from the service. Document chunking + create_document_artifacts move to services/document_chunking.py, re-pointed in api/documents.py and folder_watcher_service.py. read and web_download collapse behind services/web_fetch.py (three named FetchProfiles — BROWSER, API, DOWNLOAD) and the SSRF guard unifies into services/ssrf.py with a single BLOCKED_NETS union and drift-proof identity assertions. A BudgetCappedAbility mixin backs save_graph and save_pattern’s identical per-turn cap scaffolding (same caps 50/20, same processor counters, same payload shape). A CompactionConfig base collapses the line-identical processor shape shared by ToolChainCompactionConfig and ChatHistoryCompactionConfig. SkillManagerAbility merges into skill_builder.py — a blanket content.replace(‘skill_builder’,‘skill_manager’) that silently corrupted skill bodies is gone.
Concrete data-corruption and security fixes ride along on the migration. calendar resolves dtstart/dtend through dateparser into clean UTC ISO BEFORE any CalDAV write; the old parse_utc year-0001 sentinel path is closed and unparseable values return code=invalid-time. document delete addressing is now consent-safe: an exact id always proceeds, a name proceeds only on a single exact-name match, and ambiguity returns code=ambiguous-match with candidate rows instead of soft-deleting the first fuzzy hit. place was functionally mute pre-contract — every action returned an empty status dict; now every action returns a readable ToolResult. bash gates policy on a command-derived risk class via Ability.classify_action(params) so a prompt-injected action=“read” can no longer let rm -rf slip past the ask/deny gate; the forgeable 7-value action enum is removed from get_parameters, unreachable bash.execute rows are dropped from policy_defaults (seed drops 234→231 rows), and bash run() returns a structured {exit_code, stdout, stderr} body with 100KB clipping. The dispatcher fires ACTION_REQUIRED BEFORE the policy gate, so a malformed call never seeds a bogus ‘
Loud-error guardrails replace silent fallback paths. search_files stops swallowing validation failures into ok() prose — stable kebab codes (unknown-action/empty-query/invalid-param/directory-not-found/invalid-regex) with hints and structured glob {files:[…]} or grep {file,line,text} rows; truncation (max_files or 30s walk budget) surfaces as meta truncated=true. search rejects an unknown forced provider instead of silently masking with the DDG fallback — get_parameters sources the enum LIVE from the real registry (+ddg) so the schema cannot advertise a provider the runtime rejects; schema enum honesty matches runtime. find_tools kills silent partial success — a name blocked on the invoking channel lands in not_found, never injected, and every-unusable errors loudly with a valid: ladder of real selectable names. find_skills adds an _probe_index guardrail (SELECT 1 against skills + FTS5 before searching) so a corrupt index cannot masquerade as zero hits. news fetch_google_news now raises NewsFetchError on transport failure instead of swallowing into [], mapped to code=provider-unreachable. web_browse hands screenshot doc_ids to the caller mechanically via a ‘Screenshots saved as documents (view one with vision(image=<doc_id>))’ trailer with screenshots=N meta. chalie_docs resolves queries and fetches URLs server-side through the SSRF-guarded web_fetch stack instead of instructing a read round-trip. programming_docs_search stops fabricating fallback page text — 23 copy-paste _Source subclasses collapse into one declarative SOURCES table of flat Source(…) entries consumed by a generic lookup() engine with three reusable resolver-strategy factories (929→592 lines); a source that yields no candidate on an unreachable host returns code=source-unavailable. read now permits empty-file writes; contents=“” yields {path, bytes: 0, created: true} and a blocked overwrite leaves the file untouched. mcp_manager’s add/enable on a dead host keeps the row (enabled where applicable) and returns mcp-unreachable or auth-failed via _classify_sync_error on 401/403/unauthorized/forbidden markers. memory recall surfaces backend-error loudly — every lane errored returns code=memory-backend-error (not silent empty), partial lane errors succeed with meta degraded=true; api/updates.py is fixed to read result.status off the ToolResult instead of splitting the string.
Docs in 04-ARCHITECTURE are rewritten to match shipped reality: per-delegate max_iterations (web_search=50, web_browse=200, vision=1), the file_write empty-write contract and read-required guard, the search_files ToolResult and structured match rows, the home CapabilityAbility base and unknown-entity guardrail, the web_browse screenshot ledger handoff, the contacts get precision contract with inline local-index reads, the news structured rows and rich-via-contract, the timer rich-media trailer as dispatcher-owned, the list name addressing and disambiguation, chalie_docs server-side fetch, the web_download INTERNAL single-guard reality (replacing stale claims about retry, SSL verify-then-fallback, is_private_url local check), the read fetch_page behind the single SSRF guard with text/* passthrough fixing .diff/.patch and the param alias ladder, the browser canonical ToolResult codes, the merged skill_builder module, the mcp* proxy contract, find_tools structured body + loud blocked-on-channel errors, the document consent-safe delete + structured upload body, programming_docs_search’s declarative source table + truthfulness contract, search rejecting unknown forced providers, calendar’s create/delete + dtstart validation + fuzzy title, the policy seed count 231/77-per-channel after dropping bash.execute, document chunking moved to services, the memory engine at services/memory_retrieval, the budget/compaction mixin, contacts delegating via the shared base, and read + web_download sharing services/web_fetch with named profiles. A scrub pass drops residual internal/personal references from comments, fixtures, and ops scripts; the internal Spec sec-N / AC-N comment convention is intentionally left pending a separate decision. TKT-882 contract tests left untracked from the contract migration are committed and vision.py is added to phase4 module invariant stem set. New feature tests run the real ToolDispatcher.dispatch against real DB rows / real policy / real ActTrail with zero mocks; the prior ‘abilities.sqlite’ rows for contacts (TKT-883 exemplar) and the metadata byte-identity invariants are preserved verbatim.
-
Every ability now returns ToolResult only via ok()/err() with stable kebab-case codes, hints, and valid= ladders; success bodies are structured JSON or text passthrough and the code=“error” mechanical-wrap marker is gone.
-
CapabilityAbility base owns capability loading, not-connected / unknown-action / handler-unavailable errors, dispatch, and ToolResult wrapping; contacts is the exemplar with ~140 lines of bespoke delegation collapsing to four ClassVars.
-
Service extractions land memory_retrieval, document_chunking, web_fetch (BROWSER/API/DOWNLOAD profiles), and ssrf as single-source modules; web_download drops its redundant local is_private_url pre-check behind stream_to_file’s guard with a 100MB cap mapped to code=too-large.
-
Hardening fixes ride along: bash policy now gates on command-derived risk class (not the model’s self-declared action); calendar resolves dtstart/dtend through dateparser closing the year-0001 sentinel write path; document delete is consent-safe with fuzzy-ambiguity returning candidates instead of a silent first-hit; place returns readable results on every action.
-
Loud-error guardrails replace silent fallback: search_files returns structured rows with truncation meta; search rejects an unknown forced provider with the real enum sourced from the registry; find_tools never dresses a channel-blocked name as injected; find_skills probes the index before searching so a corrupt DB cannot masquerade as zero hits; memory recall surfaces backend-error when every lane errors.
-
Dispatcher fires ACTION_REQUIRED BEFORE the policy gate so a malformed call never seeds a bogus ‘
. ’ permission or freezes the turn waiting on human approval of a nonexistent action.