Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f1985a1
Merge pull request #33 from tinyhumansai/oh-extract-leaves
senamakel Sep 30, 2026
f0c2f89
fix(test): use assert_ne macro for clarity
senamakel Sep 30, 2026
d016feb
fix(types): remove unused type definitions
senamakel Sep 30, 2026
45ac9b3
feat(collapse): add validation for collapsed tool actions
senamakel Sep 30, 2026
110508e
chore: files changed crates/tinytools-std/src/file_state/test/ops.rs,…
senamakel Sep 30, 2026
0c5475e
refactor(file_state): track written paths per agent in a separate set
senamakel Sep 30, 2026
2ebc524
test(collapse): add per-call permission and effect tests
senamakel Sep 30, 2026
6408a42
fix(collapse): conditionally import Tool only for doc builds
senamakel Sep 30, 2026
3e94418
feat(file_state): record read timestamp before I/O to detect sibling …
senamakel Sep 30, 2026
c01c507
docs(tinytools-std): document capturing the read stamp before I/O
senamakel Sep 30, 2026
a5e171e
test(url_guard): add tests for backslash and percent-encoded host smu…
senamakel Sep 30, 2026
24e0ee0
fix(url_guard): prevent panic on malformed URL input
senamakel Sep 30, 2026
b326027
fix(url_guard): prevent panic on malformed URLs
senamakel Sep 30, 2026
fda7258
fix(tinytools-std): return vetted socket addresses from the DNS check
senamakel Sep 30, 2026
d1050d5
fix(tinytools-std): skip PATHEXT append for names already carrying one
senamakel Sep 30, 2026
34deb28
fix: correct test imports to use crate paths
senamakel Sep 30, 2026
ab46994
fix(test): remove unused test files
senamakel Sep 30, 2026
29df531
feat: add module-level example and fix test return types
senamakel Sep 30, 2026
65b6085
chore(test): remove unnecessary return types from test functions
senamakel Sep 30, 2026
84a91bb
docs(tinytools-std): list backslash and percent rejections in validat…
senamakel Sep 30, 2026
d090e6b
refactor(file_state): move stale-check logic into coordinator methods
senamakel Sep 30, 2026
54a23b5
chore: files changed crates/tinytools-std/src/file_state/ops.rs,crate…
senamakel Sep 30, 2026
8a99135
chore: files changed crates/tinytools-std/src/file_state/mod.rs
senamakel Sep 30, 2026
9131490
fix(file_state): handle empty file path in state operations
senamakel Sep 30, 2026
1b018b2
chore: files changed crates/tinytools-std/src/file_state/test/ops.rs
senamakel Sep 30, 2026
638220e
feat(url-guard, collapse): add NAT64 prefix detection and schema-defi…
senamakel Sep 30, 2026
7720498
feat(collapse): namespace member definitions to avoid collisions
senamakel Sep 30, 2026
2fae43d
fix(detect_tools): use accessat with EACCESS for exec check on Unix
senamakel Sep 30, 2026
dd025a2
fix(detect_tools): use raw bytes for accessat path
senamakel Sep 30, 2026
a73471c
fix(url_guard): correct IPv6 NAT64 prefix mask check
senamakel Sep 30, 2026
a956a99
chore: add coverage.log to gitignore
senamakel Sep 30, 2026
6fd6c60
refactor(detect-tools): extract executable check into dedicated function
senamakel Sep 30, 2026
eafa2f7
test: add contract tests for DetectTools and deferred fake tools
senamakel Sep 30, 2026
9f23c42
test(url_guard): add tests for domain normalization, port validation,…
senamakel Sep 30, 2026
0c663b9
test(url_guard): clarify type annotation in loopback resolution test
senamakel Sep 30, 2026
07a3910
fix(collapse): correct let-else formatting in rewrite_local_refs
senamakel Sep 30, 2026
d8a6579
test: migrate tests to anyhow::Result return type
senamakel Sep 30, 2026
3d0b7fc
fix(test): use fully qualified default call in test
senamakel Sep 30, 2026
15bfeea
fix(collapse): avoid rewriting $ref inside instance data and prevent …
senamakel Sep 30, 2026
144f66a
test(collapse): use helper variables for namespace definitions in test
senamakel Sep 30, 2026
8be5697
fix(collapse): handle JSON Pointer tokens in $defs references
senamakel Sep 30, 2026
156906c
fix(collapse): correct reference suffix handling for empty suffix
senamakel Sep 30, 2026
3893209
Merge upstream main into PR branch
senamakel Sep 30, 2026
19a65ce
fix(test): correct assertion in external_effect test
senamakel Sep 30, 2026
fdd7328
feat(collapse): annotate conflicting property definitions with their …
senamakel Sep 30, 2026
fe97e28
chore: files changed Cargo.lock
senamakel Sep 30, 2026
22c1e9f
refactor(collapse): extract property-definition merging into its own …
senamakel Sep 30, 2026
1949844
feat(collapse): support draft-07 definitions and prefixItems in schem…
senamakel Sep 30, 2026
119528e
fix(collapse): rewrite refs in additionalItems and schema-valued depe…
senamakel Sep 30, 2026
d9c64e7
test(url_guard): add test case for non-private NAT64 address
senamakel Sep 30, 2026
80bec1e
fix(url_guard): treat well-known NAT64 prefix as non-global only when…
senamakel Sep 30, 2026
1ef33b3
docs(READMEs): add links to completed plan and spec
senamakel Sep 30, 2026
af56b5a
refactor(url_guard): simplify NAT64 IPv4 extraction in v6 check
senamakel Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ exclude = ["worktrees"]
# true`, so the version the release workflow bumps is written in exactly one
# place and every crate moves together.
[workspace.package]
version = "0.4.1"
version = "0.5.0"
edition = "2024"
rust-version = "1.88"
license = "GPL-3.0-only"
Expand Down
Empty file added coverage.log
Empty file.
2 changes: 1 addition & 1 deletion crates/tinytools-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tinytools = { path = "../tinytools", version = "0.4.1" }
tinytools = { path = "../tinytools", version = "0.5.0" }
tracing = { workspace = true, optional = true }

[features]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinytools-jev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ readme = "README.md"

[dependencies]
async-trait = { workspace = true }
tinytools = { path = "../tinytools", version = "0.4.1" }
tinytools = { path = "../tinytools", version = "0.5.0" }
tracing = { workspace = true, optional = true }

[dev-dependencies]
Expand Down
7 changes: 6 additions & 1 deletion crates/tinytools-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ publish = false
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
# Structured diagnostics for DNS validation and SSRF refusals.
log = "0.4"
# Async path locks must remain non-blocking while callers hold them across I/O.
parking_lot = "0.12"
# Safe effective-credential executable checks for Unix PATH candidates.
rustix = { version = "1", default-features = false, features = ["fs"] }
serde_json = { workspace = true }
tinytools = { path = "../tinytools", version = "0.4.1" }
tinytools = { path = "../tinytools", version = "0.5.0" }
# `spawn_blocking` keeps synchronous system DNS resolution off async executor threads.
tokio = { version = "1", default-features = false, features = ["rt", "sync"] }
Comment thread
senamakel marked this conversation as resolved.
tracing = { workspace = true }

Expand Down
4 changes: 2 additions & 2 deletions crates/tinytools-std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Host-independent building blocks for agent tools, extracted from OpenHuman.

| Module | What it is |
| --- | --- |
| `file_state` | Process-wide read/write stamps so parallel agents detect stale or partial reads before overwriting a file. The host decides whether the guard is on (`init_global(enabled)`). |
| `url_guard` | URL validation with SSRF checks; DNS validation returns addresses callers must pin for the connection. |
| `file_state` | Process-wide read/write stamps so parallel agents detect stale or partial reads before overwriting a file. The host decides whether the guard is on (`init_global(enabled)`); read tools pass `record_read` an `Instant` captured before their I/O. |
| `url_guard` | URL validation with SSRF checks for outbound network tools. `validate_url_with_dns_check` returns a `ValidatedUrl` whose vetted `addrs` the caller must pin its HTTP client to (e.g. `reqwest`'s `resolve_to_addrs`); re-resolving the hostname reopens DNS rebinding. |
| `detect_tools` | `find_on_path` and the read-only `detect_tools` tool. |

No enforcement of host policy lives here; the crate only supplies mechanisms.
90 changes: 58 additions & 32 deletions crates/tinytools-std/src/detect_tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,50 +34,76 @@ impl Default for DetectToolsTool {
}
}

/// Fallback executable extensions when Windows has no `PATHEXT` set.
const DEFAULT_PATHEXT: &str = ".EXE;.CMD;.BAT";

/// Locate `name` on `$PATH`, honoring `PATHEXT` on Windows. Returns the first
/// matching executable path, or `None` if not found.
#[must_use]
pub fn find_on_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
let exts: Vec<String> = {
#[cfg(windows)]
{
std::env::var("PATHEXT")
.unwrap_or_else(|_| ".EXE;.CMD;.BAT".to_string())
.split(';')
.map(std::string::ToString::to_string)
.collect()
}
#[cfg(not(windows))]
{
vec![String::new()]
}
};
let pathext = cfg!(windows)
.then(|| std::env::var("PATHEXT").unwrap_or_else(|_| DEFAULT_PATHEXT.to_string()));
let file_names = candidate_file_names(name, pathext.as_deref());
for dir in std::env::split_paths(&path) {
for ext in &exts {
let candidate = dir.join(format!("{name}{ext}"));
if candidate.is_file() {
// On Unix a plain `is_file()` can match a non-executable file and
// falsely report the tool as available; require the exec bit.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let is_exec = std::fs::metadata(&candidate)
.is_ok_and(|m| m.permissions().mode() & 0o111 != 0);
if is_exec {
return Some(candidate);
}
}
#[cfg(not(unix))]
{
return Some(candidate);
}
for file_name in &file_names {
let candidate = dir.join(file_name);
if is_executable_file(&candidate) {
return Some(candidate);
}
}
}
None
}

/// Whether `path` names a regular file the current process can execute.
fn is_executable_file(path: &std::path::Path) -> bool {
if !path.is_file() {
return false;
}
// On Unix, checking mode bits alone ignores the current process's
// effective credentials and supplementary groups.
#[cfg(unix)]
{
rustix::fs::accessat(
rustix::fs::CWD,
path.as_os_str().as_encoded_bytes(),
rustix::fs::Access::EXEC_OK,
rustix::fs::AtFlags::EACCESS,
)
.is_ok()
}
#[cfg(not(unix))]
{
true
}
}

/// The file names to probe in each `PATH` directory for `name`.
///
/// `pathext` is the Windows `PATHEXT` list (`;`-separated), or `None` on
/// platforms that run a bare file name. A name that already ends in one of
/// those extensions (compared case-insensitively, as Windows does) is probed
/// unchanged; any other name is probed once per extension.
fn candidate_file_names(name: &str, pathext: Option<&str>) -> Vec<String> {
let extensions: Vec<&str> = pathext
.into_iter()
.flat_map(|list| list.split(';'))
.filter(|ext| !ext.is_empty())
.collect();
let lower_name = name.to_ascii_lowercase();
let has_extension = extensions
.iter()
.any(|ext| lower_name.ends_with(&ext.to_ascii_lowercase()));
if extensions.is_empty() || has_extension {
return vec![name.to_string()];
}
extensions
.iter()
.map(|ext| format!("{name}{ext}"))
.collect()
}

#[async_trait]
impl Tool for DetectToolsTool {
fn name(&self) -> &'static str {
Expand Down
Loading
Loading