Skip to content

fix(kernel): guard the three unenforced DomainGroup consumers - #5364

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
senamakel:domain-axis-drift-guard
Aug 4, 2026
Merged

fix(kernel): guard the three unenforced DomainGroup consumers#5364
senamakel merged 2 commits into
tinyhumansai:mainfrom
senamakel:domain-axis-drift-guard

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

  • Adds drift guards for the three DomainGroup consumers the compiler does not check for coverage: tool_group(), StoreInitPlan, and DomainSubscriberPlan. Adding a variant currently compiles cleanly while leaving a tool ungated or a store unkeyed.
  • Fixes a live leak the guard found while being written: tool_group()'s Inference rule matched tokenjuice_, but that is a migration alias — the live tool is tinyjuice_retrieve. CCR retrieval was falling through to Platform.
  • Adds DomainGroup::{ALL, COUNT, index()} as the root of trust, so the guards' lists cannot silently rot.
  • 5 files, +343/−3. Tests and one 4-line rule change; no behaviour change beyond the leak fix.

Problem

DomainGroup is filtered against by five surfaces. Two are compiler-enforced (the DomainSet field and its allows() arm). Three are not:

Consumer Failure mode when a variant is missed
tool_group() (tools/ops.rs) Tools fall through to Platform — so they stay callable under DomainSet { platform: true, <family>: false } (advertised to the model while the family is gated off) and vanish under harness(), which sets platform: false.
StoreInitPlan (runtime/context.rs) Controllers register with no store behind them.
DomainSubscriberPlan (core/jsonrpc.rs) Event-bus subscribers silently never install.

This is not hypothetical — two bugs of exactly this shape have already shipped:

  1. harness_init sat in Platform, so DomainSet::harness() never registered it: an agent harness that does not run harness init. Fixed in feat(kernel): realign DomainGroup with the family directories #5332.
  2. tool_group()'s Inference rule matched the prefix tokenjuice_, but RETRIEVE_TOOL_NAME is tinyjuice_retrieve (vendor/tinyjuice/src/cache/marker.rs:11); tokenjuice_retrieve and retrieve_tool_output are migration aliases. So the live CCR retrieval tool leaked to Platform. I shipped that one in feat(kernel): realign DomainGroup with the family directories #5332 and this guard caught it.

Solution

Root of trust first. DomainGroup::index() is an exhaustive match, so a new variant is a compile error there before anything else; domain_group_all_lists_every_variant then fails until ALL and COUNT catch up. Every guard iterates ALL, so they are only as trustworthy as that one test — which is why it exists rather than hand-maintaining three independent lists.

Each guard forces a decision. Every variant must appear in exactly one of two lists — owns-a-store / storeless, registers-subscribers / none, owns-tools / tool-less — asserted with ^ (xor), so being in neither or both fails. Adding a family cannot compile-and-forget.

The tool guard tests the function, not a registry. My first version asserted over a built registry; that was wrong twice. default_tools() returns three tools (file_read, file_write, shell), so it proved nothing. Widening to the full registry was no better: which tools it contains depends on config flags, security tier and enabled integrations, so the assertion would pass or fail for reasons unrelated to group mapping. It now checks one representative tool name per family against tool_group() directly, with representative_tool_names_are_real asserting those names exist in the widest registry this build can assemble — so the table cannot rot into testing dead strings.

The Inference rule now matches the crate's own constant. RECOVERY_TOOL_NAMES covers the live name and both aliases, so it cannot drift to a stale prefix again. The general lesson, recorded in AGENTS.md: match tool names against the owning crate's constants, not a guessed prefix.

I verified the guard is not vacuous by reintroducing the tokenjuice_ bug and confirming it fails with the expected message, then restoring.

Submission Checklist

  • Tests added or updated — four new tests: domain_group_all_lists_every_variant, every_domain_group_is_accounted_for_in_store_init_plan, every_domain_group_is_accounted_for_in_subscriber_plan (all src/core/all_tests.rs), plus every_domain_group_is_accounted_for_in_tool_group and representative_tool_names_are_real (tools/ops_tests.rs). Failure path is the entire point of the change and was exercised directly: the tokenjuice_ bug was reintroduced to confirm the guard fails, then reverted.
  • Diff coverage ≥ 80% — the only non-test change is the four-line tool_group Inference rule, covered by every_domain_group_is_accounted_for_in_tool_group (which fails without it, proven above).
  • Coverage matrix updated — N/A: no feature rows added, removed, or renamed.
  • All affected feature IDs listed under ## RelatedN/A: no matrix feature IDs affected.
  • No new external network dependencies — N/A: no dependency change.
  • Manual smoke checklist — N/A: no release-cut surface touched. Under DomainSet::full() (the shipped desktop configuration) every group is enabled, so the tool list is unchanged either way; the leak fix only alters behaviour under narrowed sets.
  • Linked issue closed via Closes #NNNN/A: no dedicated tracking issue. Follow-up hardening for the kernelization program (Feature gates for core subsystems — tracking (lightweight harness builds) #4795refactor(kernel): collapse 124 flat domains into 31 gate-aligned families #5328feat(kernel): realign DomainGroup with the family directories #5332).

Impact

Runtime/platform: effectively none. tinyjuice_retrieve moves from Platform to Inference, which changes nothing under full() (both enabled) and is the intended behaviour under any narrowed set — CCR retrieval belongs to the inference family.

Compatibility: DomainGroup gains three public associated items (ALL, COUNT, index()). Purely additive.

Performance/security: none.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:
    • default_tools_omits_flows_tools_when_feature_off is weaker than it looks — it asserts flows tools are absent from the three-tool default_tools() list, which is trivially true whether or not the gate works. Noticed while building the tool guard; not fixed here to keep this PR to one concern.
    • runtime-node gate (sheds xz2 + the static liblzma C build — one of the four native builds standing between the current floor and the stated 222-names / 2-native target). Scoped: runtime::node is reached by always-compiled agent/harness_init, so it needs facade + stub, not a leaf gate; the stub surface is NodeBootstrap, NodeSource, ResolvedNode, ops::{classify_tool_call, execute_tool} and types::{ExecuteToolOutcome, RuntimeToolSummary}.
    • contacts gate (objc2-contacts) is ready and nearly free — address_book.rs already has a non-macOS imp stub returning empty. Deliberately not included here: the dependency is macOS-only, so the shed is unverifiable on a Linux dev box and would not move the kernel-floor ratchet, which is measured on Linux. It needs a macOS verification run to claim honestly.
    • memory-git (git2 + vendored libgit2) remains cross-repo: vendor/tinycortex must carve its inert memory::diff types out from behind git-diff first.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: domain-axis-drift-guard
  • Commit SHA: 5c1a88d9f

Validation Run

  • pnpm --filter openhuman-app format:check — Rust half verified directly: cargo fmt --all --check clean in both Cargo worlds. Prettier half N/A: no frontend files changed.
  • pnpm typecheckN/A: no TypeScript changed.
  • Focused tests: core:: 684 passed gates-on, 564 passed gates-off; openhuman::tools:: 868 passed with --test-threads=1. Negative check: reintroducing the tokenjuice_ prefix bug makes every_domain_group_is_accounted_for_in_tool_group fail with left: Platform, right: Inference.
  • Rust fmt/check: cargo check --all-targets and --no-default-features --features tokenjuice-treesitter clean; cargo clippy -p openhuman -- -D warnings clean.
  • Tauri fmt/check: cargo clippy --manifest-path app/src-tauri/Cargo.toml -- -D warnings clean.

Validation Blocked

  • command: none
  • error: n/a
  • impact: n/a

Behavior Changes

  • Intended behavior change: tinyjuice_retrieve now maps to DomainGroup::Inference instead of falling through to Platform.
  • User-visible effect: none under full(), the shipped desktop configuration. Under narrowed DomainSets the tool now follows its family, which is the fix.

Parity Contract

  • Legacy behavior preserved: no controller, store, subscriber or preset changed. The guards are assertions over existing behaviour; the single production change is one tool_group rule.
  • Guard/fallback/dispatch parity checks: core:: counts are unchanged from feat(kernel): realign DomainGroup with the family directories #5332's baseline apart from the new tests (684 vs 681 gates-on, 564 vs 561 gates-off — three of the four new tests live in core::).

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: N/A

`DomainGroup` has three consumers the compiler does not check for coverage:
`tool_group()` (tools/ops.rs), `StoreInitPlan` (runtime/context.rs) and
`DomainSubscriberPlan` (core/jsonrpc.rs). Adding a variant compiles cleanly
while leaving a tool ungated or a store unkeyed — and both failure modes have
now shipped:

- `harness_init` sat in `Platform`, so `DomainSet::harness()` never registered
  it: an agent harness that does not run harness init (fixed in tinyhumansai#5332).
- `tool_group`'s `Inference` rule matched `tokenjuice_`, but that is a
  migration alias — the live tool is `tinyjuice_retrieve`
  (vendor/tinyjuice/src/cache/marker.rs:11). CCR retrieval was therefore
  falling through to `Platform`, staying callable under
  `DomainSet { platform: true, inference: false }` and vanishing under
  `harness()`. Shipped by me in tinyhumansai#5332 and found by this guard while writing it.

The Inference rule now matches against the crate's own `RECOVERY_TOOL_NAMES`
constant instead of a guessed prefix, so it cannot drift to a stale alias again.

Enforcement chain, rooted so the lists cannot silently rot:
`DomainGroup::index()` is an exhaustive match, so a new variant is a compile
error there first; `domain_group_all_lists_every_variant` then fails until
`ALL` and `COUNT` catch up. The three guards iterate `ALL` and force every
variant into exactly one of two lists (owns-a-store / storeless,
registers-subscribers / none, owns-tools / tool-less), so adding a family
cannot compile-and-forget.

The tool guard deliberately tests the FUNCTION, not a built registry. My first
version asserted over `default_tools()`, which returns three tools, and then
over the widest registry — but registry contents depend on config flags,
security tier and enabled integrations, so it passed or failed for reasons
unrelated to group mapping. It now checks one representative name per family,
with `representative_tool_names_are_real` keeping that table honest against the
real registry.

Verified the guard is not vacuous by reintroducing the `tokenjuice_` bug and
confirming it fails with the expected message, then restoring.

Verified: default, --all-targets, gates-off, Tauri-shell builds clean; clippy
-D warnings clean in both Cargo worlds; fmt clean; core:: 684 gates-on / 564
gates-off; tools 868 passed with --test-threads=1.

Noted while here, not fixed: `default_tools_omits_flows_tools_when_feature_off`
asserts absence from the three-tool `default_tools()` list, so it is trivially
true and proves less than it appears to.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel
senamakel requested a review from a team August 4, 2026 09:46

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

senamakel has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30dedc7f-7480-4b5d-a63a-9f82dc408025

📥 Commits

Reviewing files that changed from the base of the PR and between 5efb24a and 209b680.

📒 Files selected for processing (5)
  • AGENTS.md
  • src/core/all.rs
  • src/core/all_tests.rs
  • src/openhuman/tools/ops.rs
  • src/openhuman/tools/ops_tests.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c1a88d9f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1510 to 1511
if crate::openhuman::inference::tokenjuice::RECOVERY_TOOL_NAMES.contains(&name) {
return DomainGroup::Inference;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep recovery available while compaction remains global

When a custom DomainSet enables Platform but disables Inference, this reclassifies all recovery names—including retrieve_tool_output—from Platform to the disabled group, while register_domain_subscribers still installs TokenJuice globally because compaction runs on every agent's tool output. Large results can therefore be compacted into markers that the agent has no registered tool to recover; either disable compaction with the Inference domain or keep its required recovery surface available.

AGENTS.md reference: AGENTS.md:L253-L256

Useful? React with 👍 / 👎.

Comment thread src/core/all_tests.rs
DomainGroup::Inference,
DomainGroup::Automation,
DomainGroup::Runtimes,
DomainGroup::Hosted,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify Hosted as registering subscribers

DomainSubscriberPlan has a hosted field and register_domain_subscribers uses it to install the hosted orchestration-ingest subscriber, but this guard declares Hosted subscriber-less. Consequently, removing or mis-keying the Hosted subscriber path would not be caught by the new accounting test; move Hosted into REGISTERS and verify its plan field.

AGENTS.md reference: AGENTS.md:L265-L268

Useful? React with 👍 / 👎.

Comment thread src/openhuman/tools/ops_tests.rs Outdated
Comment on lines +2805 to +2809
let verified = REPRESENTATIVE.len() - unverified.len();
assert!(
verified >= REPRESENTATIVE.len() / 2,
"only {verified}/{} representative tool names were found in the widest \
registry ({unverified:?} missing) — the table has likely rotted",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require every representative tool name to be verified

When any representative tool is renamed, removed, or omitted by this test configuration, it is merely added to unverified and the test still passes while half of the other names remain. The corresponding tool_group guard then exercises a dead string indefinitely and cannot detect real classifier drift, including another rename of tinyjuice_retrieve; validate each representative individually or explicitly verify conditional names through their owning constants/registries.

AGENTS.md reference: AGENTS.md:L267-L270

Useful? React with 👍 / 👎.

CI's gates-off lane caught two defects in `representative_tool_names_are_real`
that my local run missed, because I ran `core::` gates-off but not
`openhuman::tools::` gates-off.

1. It asserted "at least half the representative names are registered" — an
   arbitrary threshold that is feature-dependent by construction. Gates-off
   only 8 of 18 resolve (the rest belong to compiled-out families), so it
   failed for a reason that has nothing to do with drift.

2. Worse, building a registry via `integration_tools_for_config` mutates
   process-global state, which broke a NEIGHBOURING test:
   `all_tools_executes_google_places_family_against_fake_backend` started
   failing with "tool `google_places_search` not registered" in the gates-off
   build. Verified as my regression, not pre-existing — it passes on clean
   `origin/main` gates-off and failed on this branch.

Removed. The main guard already catches table rot for every non-`Platform`
entry by construction: rename a tool and `tool_group` stops matching it,
returns `Platform`, and the `assert_eq!` fails. The one blind spot is the
`("shell", Platform)` row, where a rename still "matches" the fallthrough —
documented in the guard's doc comment along with why building a real registry
to close it is not worth the global-state pollution.

Verified in BOTH feature configurations this time: default → tools 867, core
684; gates-off → tools 767 (serial and parallel), core 564. Builds,
clippy -D warnings, and fmt clean.

Co-authored-by: Medulla <medulla@tinyhumans.ai>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

senamakel has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 209b6808da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/all_tests.rs
to one but not the other"
);
let mut seen = vec![false; DomainGroup::COUNT];
for g in DomainGroup::ALL {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make ALL exhaustive from the enum itself

When a new DomainGroup is added, the compiler forces an index() arm, but adding that arm alone does not make this loop observe the variant: ALL and COUNT can both remain at 22, the old variants still fill every seen slot, and this test passes. Since every subsequent drift guard iterates the same incomplete ALL, the exact compile-and-forget scenario these guards target remains undetected; generate the enum/list/count from one source or otherwise make omission from ALL fail.

AGENTS.md reference: AGENTS.md:L265-L267

Useful? React with 👍 / 👎.

/// Families with no agent tools of their own.
const TOOL_LESS: &[crate::core::all::DomainGroup] = {
use crate::core::all::DomainGroup as G;
&[G::Config, G::Security, G::Meet, G::Medulla]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise Config and Security in the tool guard

Config and Security are not tool-less: the registry installs tools such as config_snapshot, workspace_init, security_policy_info, credential_list, and session_state, and tool_group() has explicit rules for them. Putting both groups in TOOL_LESS means this new guard never exercises those rules, so removing or drifting either classifier still leaves the guard green; give each family a real representative instead.

AGENTS.md reference: AGENTS.md:L268-L270

Useful? React with 👍 / 👎.

Comment thread src/core/all_tests.rs
use crate::core::jsonrpc::DomainSubscriberPlan;

const REGISTERS: &[DomainGroup] = &[
DomainGroup::Platform,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark Platform subscriber-less until it is consumed

In the inspected register_domain_subscribers, no branch reads plan.platform; the health, scheduler, TokenJuice, and service subscribers are instead installed unconditionally in the INFRA block. Classifying Platform as registering subscribers therefore encodes ownership that does not exist and cannot guard a Platform subscriber path; keep it in NO_SUBSCRIBERS until a genuinely Platform-gated registration consumes the plan field.

AGENTS.md reference: AGENTS.md:L265-L267

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit f65dc29 into tinyhumansai:main Aug 4, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant