Skip to content

Move the pure tool-UI rules into tinymcp-bus - #105

Merged
M3gA-Mind merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/ui-rules-in-bus
Oct 15, 2026
Merged

M3gA-Mind merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/ui-rules-in-bus

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Security-relevant change: please review crates/tinymcp-bus/src/ui/web_url.rs closely. Link classification decides which URLs in a tool result a host shows as clickable. This PR replaces url::Url::parse in that decision with a hand-written, fail-closed splitter. The property it must hold, tested differentially against url: it never classifies a URL as Web or Image when url would refuse it or read a different host.

Behaviour change (fail-closed): http(s) links with userinfo, IDN/punycode or %-encoded hosts, IPv6, non-canonical IPv4, bad ports or dot segments are now Blocked (previously Web).

Summary

The pure tool-UI rules move from tinymcp::ui into the contract crate, as tinymcp_bus::ui:

  • template resolution
  • widget document checks
  • link classification and extraction
  • the widget tool-call and rendering policy

tinymcp::ui stays as a pub use tinymcp_bus::ui::* re-export, behind the same ui feature.

The rules are pure functions of a tool call's JSON. Until now, though, a host that renders tool UI had to link the whole module crate (reqwest, tokio, the storage drivers) to get them. OpenHuman's module-boundaries audit therefore flags openhuman-core and openhuman-app for their direct tinymcp dependency. With this change they can take tinymcp-bus alone.

The contract may depend only on serialization utilities (serde, serde_json, schemars, and base64, which OpenHuman's contract audit allows). The old rules used three crates outside that set, so:

  • regex → a hand-written scanner (links.rs, url_spans). It finds the same spans as the old pattern, (?i)\b[a-z][a-z0-9+.\-]{1,31}://[^\s"'<>{}|\^[]]+, and links_scan_tests.rs checks it against that regex on hand-picked and generated text. The scheme scan is bounded at 33 bytes, so text with a word boundary every other byte (a.a.a.…`) stays linear.

  • url → a fail-closed http(s) splitter (web_url.rs). It accepts:

    • lowercase-normalized ASCII domains (letters, digits, single inner hyphens, no --), or canonical dotted-decimal IPv4;
    • an optional port of 1 to 5 digits, at most 65535;
    • a path without dot segments.

    Everything else is Blocked.

  • tracing → removed. Five debug lines.

Contract 1.25. No member was added or removed.

Related issue

Refs tinyhumansai/openhuman#7868, which tracks moving these rules to the bus, then the host import switch and removing the temporary hosts:tinymcp boundary exception (tinyhumansai/openhuman#7869). This PR is the first step and does not finish that issue.

API or behavior changes

  • New public API: tinymcp_bus::ui now exports the rule functions and constants that tinymcp::ui exported (same names and signatures). tinymcp::ui re-exports them unchanged, so existing callers compile as before.
  • Contract: CONTRACT_VERSION 1.24 → 1.25, a minor bump with no member added or removed, following the precedent of 1.23's pure helpers.
  • Dependencies:
    • tinymcp-bus gains base64, plus url and regex as dev-dependencies (test oracles only).
    • tinymcp drops regex; its ui feature no longer enables a dependency.
  • Behavior change: classify_link, extract_links and is_image_url are stricter, and only in the fail-closed direction.
    • These http(s) URLs, previously Web or Image, are now Blocked:
      • URLs with userinfo or a backslash
      • URLs with whitespace or control characters anywhere
      • IDN (non-ASCII) and punycode (xn--) hosts, and any label containing --
      • percent-encoded hosts
      • bracketed IPv6 hosts
      • IPv4 in any form other than canonical dotted decimal (127.1, 0x7f.0.0.1, 010.0.0.1)
      • empty labels and a trailing dot, and labels with _
      • a port that is empty, longer than 5 digits, or above 65535
      • dot segments in the path (., .., %2e)
    • is_image_url now answers false for non-http(s) schemes. It previously ran url::Url::parse on any scheme. Its doc already said "an http(s) URL".
    • Nothing previously Blocked becomes Web, Image or Handoff. Handoff classification is unchanged.

Validation

Not run locally: clippy, build and the test suite. By our fleet's policy, CI is the gate for those, and this PR relies on module-ci to run them. Commands actually run:

  • cargo fmt --all. Ran it and committed the result; --check was not run separately.
  • cargo metadata --locked --offline. Exit 0, and Cargo.lock was unchanged afterwards, which confirms the hand-edited lockfile resolves (only the tinymcp and tinymcp-bus dependency lists changed).
  • cargo clippy --all-targets --all-features -- -D warnings (CI)
  • cargo build --all-targets --all-features (CI)
  • cargo test --all-features (CI)

Tests

  • Moved unchanged: links_tests.rs, policy_tests.rs and resolve_tests.rs (byte-for-byte; links_tests.rs was not modified).
  • New web_url_tests.rs: the differential property. For every input that classify_link calls Web or Image, url::Url::parse must succeed with an http(s) scheme, the same host_str(), and the same image verdict on its path(). Inputs:
    • Hostile corpus, every case asserted refused:
      • userinfo (https://a@b, https://b.com@evil.com/)
      • backslashes
      • tabs, newlines and spaces
      • IDN, punycode and Cyrillic homoglyphs
      • percent-encoded hosts
      • IPv4 in decimal, octal, hex and short forms
      • IPv6, mapped and zoned
      • out-of-range and malformed ports
      • dot segments
      • javascript:, data:, file: and ftp:
      • uppercase schemes
    • Plain corpus, every case asserted accepted and in agreement with url.
    • Generated set: 4 schemes × 20 hosts × 8 ports × 14 paths, every combination checked against the property.
  • New links_scan_tests.rs: the scanner against the old regex, on hand-picked haystacks covering terminators, word boundaries (including non-ASCII letters and digits), the 32-byte scheme limit and Unicode whitespace, plus 17³ generated three-piece strings.
  • Deliberately not matched to the regex: in Unicode word-boundary edge cases, the scanner's word test is char::is_alphanumeric() || '_', while the regex's \w also includes combining marks, other connector punctuation and join controls, and excludes non-decimal numerics. In those cases the two can disagree on whether a candidate is found right after such a character. Every candidate still goes through classify_link, so this cannot widen what is accepted.
  • Coverage: the new files are exercised by these tests; per-file coverage is checked in CI.

Documentation

  • README.md: the UI section now points at tinymcp_bus::ui and lists what classify_link blocks.
  • crates/tinymcp-bus/README.md: a contract 1.25 paragraph; the dependency closure now names base64.
  • crates/tinymcp-bus/src/ui/mod.rs and web_url.rs module docs; tinymcp::ui shim docs; the feature comment in crates/tinymcp/Cargo.toml; the version history in version/mod.rs.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints. The two new test files carry the same #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] header as every existing test file; library code has none.
  • No secrets, tokens, or .env contents in the diff or the description

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: be396acb-f510-4859-ac3a-c677805bd187
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@tinysweeper

tinysweeper Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 5 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Incomplete
Priority: none
Reviewed head: 81d9980c64ba
Updated: 2026-10-15T13:16:52Z

Review snapshot

Change surface Files Review signal Count
Production 7 Active findings 0
Tests 6 Noted findings 0
Documentation 2 Resolved findings 0
Configuration 2 Pending checks/questions 6

Completeness: Incomplete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

No active actionable findings.

Could not review: README.md, crates/tinymcp-bus/src/ui/links.rs, crates/tinymcp-bus/src/ui/links_scan_tests.rs, crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp-bus/src/version/mod_tests.rs, crates/tinymcp/Cargo.toml

Before merge

  • Complete the critique review for README.md, crates/tinymcp-bus/src/ui/links_scan_tests.rs, crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp-bus/src/version/mod_tests.rs, crates/tinymcp/Cargo.toml.
  • Complete the security review for crates/tinymcp-bus/src/ui/links.rs.

How this fits together

flowchart LR
  n0["classify_link<br/>changed"]:::changed
  n1["extract_links<br/>changed"]:::changed
  n2["image_path<br/>changed"]:::changed
  n3["link_kind<br/>changed"]:::changed
  n4["trim_trailing<br/>changed"]:::changed
  n5["resolve_presentation<br/>changed"]:::changed
  n6["json"]:::impacted
  n7["view"]:::impacted
  n8["...ocument_the_result_embedded_is_prefetched"]:::impacted
  n9["...uctured_content_is_dropped_but_links_kept"]:::impacted
  n10["resource_from_contents"]:::impacted
  n11["drops_image_assets"]:::impacted
  n0 -->|calls| n2
  n1 -->|calls| n0
  n1 -->|calls| n3
  n1 -->|calls| n4
  n5 -->|calls| n1
  n5 -->|calls| n10
  n7 -->|calls| n6
  n8 -->|calls| n5
  n8 -->|tests| n5
  n8 -->|calls| n6
  n8 -->|tests| n6
  n8 -->|calls| n7
  n8 -->|tests| n7
  n9 -->|calls| n5
  n9 -->|tests| n5
  n9 -->|calls| n6
  n9 -->|tests| n6
  n9 -->|calls| n7
  n9 -->|tests| n7
  n11 -->|calls| n1
  n11 -->|tests| n1
  n11 -->|calls| n6
  n11 -->|tests| n6
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: incomplete; unanswered: README.md, crates/tinymcp-bus/src/ui/links_scan_tests.rs, crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp-bus/src/version/mod_tests.rs, crates/tinymcp/Cargo.toml
  • Lane summary: Reviewed 8 files; 0 findings. 5 files could not be reviewed: README.md, crates/tinymcp-bus/src/ui/links_scan_tests.rs, crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp-bus/src/version/mod_tests.rs, crates/tinymcp/Cargo.toml. _The code index is behind this pull request (indexed at `c3afdeeba8c7`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: incomplete; unanswered: crates/tinymcp-bus/src/ui/links.rs
  • Lane summary: Reviewed 10 files; 0 findings. 1 file could not be reviewed: crates/tinymcp-bus/src/ui/links.rs. 2 files were not security-reviewed: README.md (prose or tabular data), crates/tinymcp-bus/README.md (prose or tabular data). _The code index is behind this pull request (indexed at `c3afdeeba8c7`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: I need to check the existing link tests and surrounding UI module code before deciding whether the behavioural change (fail-closed link classification) is properly pinned and whether the new scanner has any reachable defect the differential tests miss. Provisional: the change looks well covered — new scanner, new splitter, each with differential tests against the previous oracle (`url` crate / the old regex), and the existing links/resolve tests already exercise the moved functions — but I want to verify what `links_tests.rs` and `web_url_tests.rs`'s neighbours already assert, and how `policy.rs`/`resolve.rs` callers use `classify_link`, so I don't report a coverage gap that isn't one or miss a regression the new fail-closed rules introduce on shapes the old regex+`url` path accepted as `Web`. In particular: the old code accepted any host the `url` crate parsed as non-empty (including userinfo-less IDN, IPv6-in-brackets was already parsed-and-failed-open? need to check), and the new splitter refuses several of those — I want to confirm the REFUSED list matches what the old code actually did with those inputs, since a regression claim ('previously `Web`') should be pinned, not asserted in prose. Also want to check `UiSource`/`select_source` context around the `resolve.rs` simplification (`inline_document = ...ok()` drops the `Err` branch — previously `inline_document` stayed `None` either way, so behaviour is identical, but the `tracing` removal is a logging-only change, not behavioural). Looking up: the old behaviour of `classify_link` on the newly-refused shapes (was IPv6 `Web` before? the old code parsed with `url` and required non-empty host — `[::1]` parses fine in `url`, so yes it was `Web` and is now `Blocked`, which the README/comment correctly describes as a deliberate fail-closed change, not a silent regression); whether `links_tests.rs` and `resolve_tests.rs` pin any of the newly-refused shapes as `Web` (which would now fail — those tests live outside the diff, so I need to see them to know whether this PR breaks them, which is reportable); and the `ui` feature's new empty definition — `tinymcp/src/ui/mod.rs` does `pub use tinymcp_bus::ui::*` unconditionally, so I should check whether `tinymcp_bus` is always a dependency of `tinymcp` (it appears to be, from `lib.rs`), meaning the `ui` feature gate now does nothing to the build — need to see `crates/tinymcp/src/lib.rs`'s `#[cfg(feature = "ui")] pub mod ui;` line to confirm the feature still gates the module. Also want to check `policy.rs`'s `rendering`/`widget_call_policy` which the index says reach into the changed symbols, for any caller that assumed `Web` for a now-`Blocked` shape in a way that changes a decision, and whether `links_tests.rs` pins any now-refused URL as `Web`/`Image` (that would make this PR break its own existing tests, which is reportable). Four reads should settle it: links_tests.rs, resolve_tests.rs and policy.rs in the bus ui module, plus the cfg-gating in tinymcp's lib.rs around the ui module declaration. For the coverage rule: `classify_link`, `link_kind`, `image_path`, `trim_trailing`, `select_source` are flagged by the index as unexercised by tests — but `links_scan_tests.rs` (in the diff) exercises `url_spans`/`trim_trailing` indirectly and `web_url_tests.rs` exercises `classify_link` heavily; the index predates this diff. I will not report a coverage finding on those without seeing the test files. One more thing to verify: `WebUrl::split` on `"https://a\.com:08/x\.JPG"&#96; — `valid_port` says `port.parse::<u16>().is_ok()`; `"08"` parses to 8, so it's accepted, and the test in ACCEPTED expects it accepted — but a WHATWG parser also accepts `:08` as port 8, so this is consistent, not a defect. Similarly `":0"` parses fine. `valid_ipv4("0.0.0.0")` → all labels single digit, no leading zero issue, parses, accepted — consistent with test. `valid_domain("1e.com")` — last label `1e.com`? No: `rsplit('.').next()` on `"1e.com"` is `"com"`, not all digits, doesn't start with `0x`, labels all lowercase-alnum/hyphen, so accepted — matches test expectation. `valid_domain("a.1")` — last is `"1"`, all digits → refused. But wait: the ACCEPTED test has no `a.1`, and REFUSED has `"https://a\.123/"&#96; (last label all digits → refused, and `url` parses `a.123` — WHATWG treats an all-numeric last label as IPv4-ish and would... actually the `url` crate parses `https://a\.123/&#96; fine with host "a.123" since it's not a valid IPv4 but is a domain? Need the test to confirm; the test only asserts the splitter refuses it and `classify_link` doesn't call it Web, which is self-consistent regardless of what `url` does). The real regression surface is in the ACCEPTED/combination test which pins agreement with `url`, so I'm satisfied the splitter is well-tested; what I cannot see from the diff is the pre-existing `links_tests.rs`, which may assert `Web` for shapes this change blocks — that is the one thing a lookup settles definitively, and if so it is a real, reportable break of a test this change touches. Similarly `resolve_tests.rs` may embed widget documents whose URIs pass through the new code paths — those don't involve links, so lower risk. And policy.rs may reference `MAX_LINKS` or `classify_link` on fixed test URLs. Requesting the reads now; verdict deferred until they return, findings empty for this turn as instructed. Before that, one thing I can settle without a lookup: `crates/tinymcp/src/ui/mod.rs`'s `pub use tinymcp_bus::ui::*` inside a `#[cfg(feature = "ui")]`-gated module means the feature now gates a pure re-export — no behaviour change, and the Cargo.toml comment in the diff says exactly that, so nothing to report. And the `base64` dependency added to `tinymcp-bus`'s `[dependencies]` (not optional, not behind the `ui` feature — the Cargo.toml hunk puts it under `[dependencies]` unconditionally with a comment about "the pure tool-UI rules in `ui`") makes it a hard dependency of the contract crate for all hosts, which contradicts the workspace rule "Default and all-feature dependency closures contain only serialization and schema utilities" from the bus README the same diff edits (it does update the README to include `base64`, so the author is aware and has re-declared the allowed set — I won't report this since the diff updates the README to permit it, and `base64` is serialization-adjacent, not a transport/runtime). Nothing else is reportable without the reads. Waiting on lookups for the final answer on test-breakage and policy.rs's use of the changed functions. In the meantime, the `contract 1.25` bump and its pinned test (`the_shipped_contract_version_is_pinned` updated in the diff) are consistent. The `mod.rs` re-export list adds `is_read_only`, `visible_to_app`, `client_capabilities` to the bus — need to confirm `policy.rs` exports those (the old `tinymcp/src/ui/mod.rs` re-exported them from its own `policy` module, and the bus's `mod.rs` in the diff now re-exports the same names from its own `policy` — if the bus's `policy.rs` doesn't define them, this is a compile error, which is reportable without needing more reads if I can confirm from the index; the index lists `policy.rs#rendering`, `policy.rs#widget_call_policy`, `policy_tests.rs#presentation`, `policy_tests.rs#rendering_falls_back_to_links_without_widget_support` — no mention of `is_read_only`/`visible_to_app`/`client_capabilities` in the bus's policy.rs. But the index is partial ("3 more not shown"), so I can't conclude absence from it; a lookup at policy.rs settles it. That's my fourth lookup: read policy.rs to confirm the five re-exported names exist. Lookups: 1) read `crates/tinymcp-bus/src/ui/links_tests.rs` (full, up to 200 lines) — does any existing test assert `Web`/`Image` for a shape the new splitter refuses, or assert `extract_links` output that changes; 2) read `crates/tinymcp-bus/src/ui/resolve_tests.rs` (up to 200 lines) — does the moved `resolve_presentation` break any pinned expectation; 3) read `crates/tinymcp-bus/src/ui/policy.rs` (full) — confirm the five names exist for the new re-exports and see how `rendering`/`widget_call_policy` use the changed link functions; 4) search `crates/tinymcp/src/lib.rs` for `feature = "ui"` to confirm the gate still exists around `pub mod ui`. That covers everything I flagged. I'll defer the verdict. If policy.rs turns out to lack the five names, that's a reportable compile-level defect (the diff itself shows the re-export list naming items that must exist). If links_tests/resolve_tests pin refused shapes as Web, that's a reportable test break. Otherwise the change is sound and the differential tests earn their keep: they would fail if the scanner or splitter disagreed with their oracle, and `web_url_tests.rs#refuses_every_hostile_shape` + `every_combination_agrees_with_url` pin the fail-closed claim the README and CONTRACT_VERSION comment assert. The claim "http(s) links with userinfo, IDN/punycode or %-encoded hosts, IPv6, non-canonical IPv4, bad ports or dot segments are now `Blocked`" is evidenced by `web_url_tests.rs`'s REFUSED list and the combination test, which name each of those shapes and assert `classify_link` does not return Web/Image for them. That is the test that would fail if the claim were false. Nothing else is claimed without evidence. Findings empty this turn; summary above is provisional and will be finalized after the lookups return. Looking forward to the results. (Note: if the lookups are not granted or return nothing useful, I'll fall back to the provisional verdict based on the diff alone, which currently looks sound — the differential tests are real, they assert against a concrete oracle and would fail on disagreement, and the fail-closed claim is pinned by REFUSED + the combination test. The only unknowns are the pre-existing test files, which are outside the diff and which I was given a way to read.) Actually, re-examining `valid_domain`: it refuses a last label starting with `0x` — what about `0X` (uppercase)? `last.starts_with("0x")` is case-sensitive, so `"0X7f"` would not be refused by that check; but WHATWG also only lowercases the host... `url` lowercases too, and WHATWG ends up parsing `0X7f` as an IPv4 number as well, so a host ending in `0X7f` should be refused. But `host` is already lowercased by `to_ascii_lowercase()` before `valid_domain` runs — `to_ascii_lowercase` maps `X`→`x`, so `"0X7f"` becomes `"0x7f"` and IS caught. Good, no gap. What about a last label that is all digits but the host has trailing dot (`"1.2.3.4."`)? `valid_domain` gets `"1.2.3.4."` → `rsplit('.').next()` is `""` (empty after trailing dot) → `!"".all(digit)` is true (empty all() is true → `!true` = false) — wait: `"".bytes().all(|b| b.is_ascii_digit())` is `true` for the empty iterator, so `!true` = `false` → the `&&` chain short-circuits → `valid_domain("1.2.3.4.")` returns false. And `valid_ipv4("1.2.3.4.")` splits into 5 labels → false. So the splitter refuses `https://a\.com\./&#96; (trailing dot) — and indeed REFUSED contains `"https://a\.com\./"&#96; with a comment. But WHATWG parses `https://a\.com\./&#96; fine (trailing dot is legal in DNS). So this is the fail-closed trade-off the author explicitly documents, not a defect. The combination test covers `"a.com."` in hosts too. No gap to report. One last thought on `url_len`: `.take(MAX_SCHEME_LEN + 1)` counts up to 33 scheme-chars; if the run is longer than 32, `scheme` = 33, fails the `(2..=32).contains(&scheme)` check, returns None — correct. If text starts with a non-alnum `+`-like char at the boundary, `text.as_bytes()[0].is_ascii_alphabetic()` fails — but wait, if the first byte is not ASCII-alphabetic but IS in `+.-` (e.g. `-http://`), the take_while consumed it, scheme counts it, then the alphabetic check fails → None. Correct. And `text[scheme..].starts_with("://")` — if `scheme == text.len()` (all of a short text consumed, e.g. text = "ab"), `text[2..]` is `""`, doesn't start with `://` → None. No panic (index at len is valid for slicing). If text is 1 byte non-ASCII... take_while consumes UTF-8 continuation bytes? `b.is_ascii_alphanumeric()` on a continuation byte (0x80+) is false, so take_while stops immediately, scheme = 0, fails `2..=32` → None, and `text.as_bytes()[0]` is only read after the scheme-len check passes... actually the `||` short-circuits: `!(2..=32).contains(&scheme)` is true when scheme=0, so the whole condition is true → return None before indexing. No panic. Good. What about a multi-byte char where the first byte IS ASCII-alphabetic? Impossible — a UTF-8 lead byte for a non-ASCII char is ≥ 0xC2. Fine. `url_spans` boundary check: `haystack[..at].chars().next_back().is_none_or(|c| !is_word(c))` — at=0 → `next_back()` is None → `is_none_or` → true → boundary at position 0. Correct (regex `\b` also matches at start). `is_word` uses `char::is_alphanumeric()` which is Unicode-aware, matching regex's `\b` (also Unicode-aware by default). Consistent. The scanner looks correct. I have no findings without the reads; proceed with lookups.)}{ _The code index is behind this pull request (indexed at `c3afdeeba8c7`), so retrieved context may be out of date._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: _The code index is behind this pull request (indexed at `c3afdeeba8c7`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, glm-5.3-flash,
  • Spend: $0.005563
  • Tokens: 522784 input · 24960 output · 0 cached · 871 embedding
Head State Pass summary
c3afdeeba8c7 incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-10-15T12:45:17Z)
81d9980c64ba incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-10-15T13:16:52Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking, but could not review everything, so this is not an approval: crates/tinymcp-bus/Cargo.toml, crates/tinymcp-bus/src/ui/links.rs, crates/tinymcp-bus/src/ui/web_url.rs, crates/tinymcp-bus/src/ui/web_url_tests.rs, crates/tinymcp/Cargo.toml.

             $0.0100 · 614,764 in / 37,392 out · 13,440 cached (2%) · ladder/vectors-oai3, glm-5.3-flash · 871 embedded
critique:    $0.0065 · 272,018 in / 23,670 out · 4,480 cached (2%)  · glm-5.3-flash
security:    $0.0027 · 283,136 in / 8,564 out  · 8,960 cached (3%)  · glm-5.3-flash
tests:       $0.0002 · 24,531 in  / 129 out    · 0 cached (0%)      · glm-5.3-flash
description: $0.0002 · 18,399 in  / 1,669 out  · 0 cached (0%)      · glm-5.3-flash

Template resolution, widget document checks, link classification and
extraction, and the widget tool-call and rendering policy are pure
functions of a tool call's JSON. Living in tinymcp::ui made every host
that renders tool UI link the whole module crate (reqwest, tokio, the
storage drivers) for them. Move them into tinymcp_bus::ui and keep
tinymcp::ui as a re-export.

The contract may depend only on serialization utilities, so:
- the URL scanner is hand-written instead of a regex, with a
  differential test against the old pattern;
- http(s) links are split by a fail-closed host and path splitter
  instead of the url crate, with a differential test against url that
  every Web or Image verdict parses there with the same host;
- the debug logs are gone.

Contract 1.25; no member was added or removed.

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking, but could not review everything, so this is not an approval: README.md, crates/tinymcp-bus/src/ui/links.rs, crates/tinymcp-bus/src/ui/links_scan_tests.rs, crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp-bus/src/version/mod_tests.rs, crates/tinymcp/Cargo.toml.

             $0.0056 · 522,784 in / 24,960 out · 0 cached (0%) · ladder/vectors-oai3, glm-5.3-flash,  · 871 embedded
critique:    $0.0024 · 226,066 in / 12,446 out · 0 cached (0%) · glm-5.3-flash,
security:    $0.0023 · 234,782 in / 4,687 out  · 0 cached (0%) · glm-5.3-flash,
tests:       $0.0003 · 24,986 in  / 3,383 out  · 0 cached (0%) · glm-5.3-flash
description: $0.0002 · 18,399 in  / 11 out     · 0 cached (0%) · glm-5.3-flash

@M3gA-Mind
M3gA-Mind merged commit 1aee325 into tinyhumansai:main Oct 15, 2026
32 of 33 checks passed
@M3gA-Mind M3gA-Mind mentioned this pull request Oct 15, 2026
4 of 8 tasks
M3gA-Mind added a commit to M3gA-Mind/tinymcp that referenced this pull request Oct 15, 2026
Ships tinyhumansai#105 (the pure tool-UI rules move into tinymcp-bus, contract 1.25)
and tinyhumansai#107. Workspace version and the two lockfile entries only, as in the
0.18.5 release.

Part of tinyhumansai/openhuman#7868
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant