June 3, 2026
policy flat gate, dispatch refactor, all timeouts gone
Massive policy refactor lands: PolicyManager is now the sole gate against a flat policy(channel, permission, setting) table, and PolicyService plus policy_rules
Massive policy refactor lands: PolicyManager is now the sole gate against a flat policy(channel, permission, setting) table, and PolicyService plus policy_rules are deleted. A static policy_defaults.json seed is applied via apply_seed() AFTER schema convergence, with a boot migration that creates+copies+seeds the table. A regression that persisted the REST api_key in cleartext on fresh installs (introduced when the policy seed ran before convergence so schema.sql’s INSERT OR IGNORE pass was skipped) is fixed by deferring the seed to post-converge — fresh installs now seed api_key as is_sensitive=1 so SettingsService.set takes the vault branch. The Brain Policies UI renders flat rows with no client pivot/meta, backed by new flat GET, single-upsert PUT, and reset REST endpoints; MCP seeding is deleted and MCP is gated lazily via _MCPAbility and PolicyManager.wrap. Ability.dispatch is deleted; tool calls now route use → match → wrap → execute.
Permission defaults shrink and clarify: an INTERNAL frozenset (13 tools — browser, chalie_docs, find_skills, find_tools, memory, read, review_tool_calls, review_transcript, save_graph, save_pattern, search, skill_manager, web_download) always bypasses the gate with no DB lookup and no UI surface, and 66 corresponding seed rows are removed (297 → 231). The reserved SUBAGENT policy channel goes too (no ProcessorConfig ever routed to it), taking 101 dormant rows with it (404 → 303). policy_defaults.json is reformatted from 5 lines/row to one compact JSON object per line to quiet single-row diff noise; data is byte-for-byte equivalent on JSON round-trip.
Delegate abilities are slimmed: research and summariser are deleted end-to-end (ability files, the abilities.sqlite rebuild, the policy seed rows, docs, and tests). The remaining web_search and web_browse become typed ProcessorConfig subclasses (WebSearchConfig, WebBrowseConfig) matching the TKT-803 pattern, each living alongside its ability, with policy_channel staying SUBCONSCIOUS (the outer tool is the permission boundary). Delegates gain memory in always_available and web_search also gains web_download; their inert recursion-guard blocked set is dropped (build_blocked and DELEGATE_TOOL_NAMES are removed since no find_tools + discoverable=[] path remains). find_tools loops on user/DMN/EAMP channels now block pattern-write tools (save_pattern, save_graph — exclusive to Pattern/GeoConfig) and the raw web tools (browser, search — exclusive to the delegates), and DmnConfig additionally blocks both delegates. New shared constants PATTERN_WRITE_TOOLS, DELEGATE_TOOLS, DELEGATE_INTERNAL_TOOLS in configs/channels/_common.py centralise the scope.
Dispatch refactor threads MessageProcessor directly through the ACT loop: the current_processor() ContextVar is deleted, Ability.use() binds a fresh per-call instance with .MessageProcessor = mp, and the Ability.run() signature collapses to run(self, params). All 36 abilities plus providers and the embedding facade take mp explicitly. Every framework execution timeout is removed per the no-timeouts rule — the _run_with_timeout daemon-thread watchdog becomes synchronous _run_ability (telemetry, VaultLockedError message, and result normalisation kept), base Ability.TIMEOUT plus all 30 per-ability TIMEOUT attrs are dropped, DELEGATE_DEADLINE_SECONDS and the ACT-loop _should_stop deadline branch are gone, and process(deadline=…) is removed. search_files’ walk budget rebases onto a standalone _WALK_BUDGET_S. Network/subprocess socket timeouts stay (a read on a dead peer would hang forever). code_eval keeps a hard 10-minute cap via a spawn subprocess the parent force-terminates — the only way to kill arbitrary CPU-bound code; spawn (not fork) avoids inheriting the multithreaded server’s locks, and the result is read before join so large untruncated output never deadlocks the queue feeder.
The remaining ProcessorConfig factories and module constants convert to typed subclasses: make_geo_config → GeoConfig, make_pattern_config → PatternConfig, make_super_episode_config → SuperEpisodeConfig, make_eamp_config → EAMPConfig, make_user_config → UserConfig, make_user_summary_config → UserSummaryConfig, plus DMN_CONFIG → DmnConfig, EPISODE_ENCODER_CONFIG → EpisodeEncoderConfig, COMPACTION_CONFIG → CompactionConfig, SKILL_SUGGESTION_CONFIG → SkillSuggestionConfig. Each subclass has an explicit typed init that calls super().init(…) against the frozen base. SuperEpisodeConfig preserves the prior factory quirk where the channel param was accepted but ignored (hardcoded ‘super_episode_encoder’); that is now documented in the docstring. UserConfig.metadata stays a constructor-only arg used to derive skip_input_row, not stored on the frozen base.
Bug fixes: the ACT-loop refactor’s per-provider content-field substitution is restored on the user channel after MessageProcessor._substitute_provider_placeholders was dropped — the user channel was emitting the literal token instead of the active provider’s response field (Anthropic content[].text, OpenAI choices[].message.content, Gemini candidates[].content.parts[].text, Ollama message.content). Substitution lives as a shared helper in configs/channels/_common.py, wired into the user channel and deduped from the external-agent channel; best-effort passthrough on missing placeholder or lookup failure. Frontend act-trail tool pills were silently dropped because chat.js read msg.call_id and msg.act_summary (undefined) while backend emits msg.id and msg.summary — renderer.appendToolPill guards on if (!callId) return, so EVERY tool pill vanished and web-search activity was invisible in chat. renderer.js also falls back to client-measured elapsed since the backend sends no ms. SonarCloud quality-gate blockers for rc-0.9.0 → main are cleared: hardcoded /tmp replaced with the OS temp dir via a new single source of truth services/tmp_storage.py (wired into api/upload.py write, services/message_processor.py read-guard, and workers/tmp_cleanup_worker.py sweep — Linux server behaviour unchanged, macOS now uses per-user $TMPDIR), Object.keys().sort() in frontend/brain/policies.js uses an explicit localeCompare comparator, and four float == 0.0 → pytest.approx(0.0) in test_provider_calculate.py.
Docs reconciled across 04-ARCHITECTURE.md and 09-TOOLS.md to match the dispatch refactor (per-ability surface is run(self, params); state read via self.MessageProcessor; policy_channel is caller-inherited directly from the bound processor; sync delegate path runs via _run_ability), the timeout removal (state abilities run to completion with no framework timeout except code_eval’s 10-minute spawn-subprocess cap), the delegate tool scope + channel blocked sets, the Ability.dispatch → Ability.use rename, the boot migration ordering constraint (policy seed runs after convergence so api_key stays sensitive), and the tmp_storage temp-path source of truth. Gate: 1384 unit tests passing, 0 failed; ruff clean.
-
PolicyManager becomes the sole gate against a flat policy table; PolicyService and policy_rules deleted
-
INTERNAL frozenset (13 tools) always bypasses the gate with no DB lookup; 66 seed rows removed (297 → 231)
-
research and summariser delegates deleted; web_search and web_browse become typed ProcessorConfig subclasses (TKT-803)
-
All framework execution timeouts removed; code_eval keeps a hard 10-minute spawn-subprocess cap as the sole exception
-
current_processor() ContextVar deleted; Ability.use() binds per-call .MessageProcessor, collapsing run() to run(self, params)
-
Policy seed deferral past convergence fixes the fresh-install regression that persisted api_key in cleartext