Changelog
[Unreleased]
Section titled “[Unreleased]”SsrfPolicy.allowlist(HostMatcher) is now exposed to every language binding via a binding-safe tagged representation (exact/suffix/cidr). Allowlist entries permit access regardless of the default denylist. Closes #37.CrawlConfig.ssrf_deny_private_explicitlets a caller pinssrf.deny_privateto an explicit value so it is no longer consulted fromCRAWLBERG_ALLOW_PRIVATE_NETWORK, removing the ambiguity between a caller who meansdeny_private: trueand a binding whose struct default happens to land there.CrawlConfig.max_links_per_pagebounds how many links are enqueued from a single page. Links past the cap are dropped and a warning is logged.CrawlConfig.document_output_dirwrites downloaded document bytes to disk (<dir>/<content_hash>.<ext>) and drops them from the result, populatingDownloadedDocument.content_pathinstead ofcontent. No effect on wasm32 (no filesystem).CrawlConfig.document_content_encoding(newDocumentContentEncodingenum) opts a downloaded document’s bytes intoDownloadedDocument.content_base64for bindings that need an in-memory, serializable copy. Off by default: base64-encoding a document by default would duplicate an already up-to-document_max_sizebuffer (50 MB default) in memory per document.CrawlConfig.capture_screenshot(scrape-only, chromiumoxide-only) captures a base64-encoded PNG screenshot of the page.CrawlConfig.browser_profile(chromiumoxide-only) selects a named browser profile for persistent sessions (cookies, localStorage).
Changed
Section titled “Changed”- JS evaluation paths (
ExecuteJsinteractions andeval_script) now run under a timeout, so a hung script can no longer permanently burn a worker slot or hang the isolate. - Credentials are redacted before reaching tracing spans, SSRF-violation error messages, and
Debugoutput —ProxyConfigandAuthConfigno longer leakuser:pass@in errors or logs. - Idle per-domain rate-limiter and EWMA domain state now expire on a TTL instead of accumulating unboundedly for long-running processes that crawl many distinct domains.
- Document persistence now writes via
tokio::fsinstead of blockingstd::fson the async document-download path. - Bindings regenerated on alef 0.60.0.
- E2E fixtures use Alef’s canonical
brewlanguage identifier, allowing strict fixture-driven generation to proceed.
[1.1.3] - 2026-08-04
Section titled “[1.1.3] - 2026-08-04”Changed
Section titled “Changed”- Regenerated all language bindings on alef 0.51.2 and updated dependencies.
- Ruby: the gem no longer publishes its generated types into the global
Objectnamespace (theParsercollision with theparsergem); generated types stay namespaced underCrawlberg(tree-sitter-language-pack #173, via alef 0.51.1).
[1.1.2] - 2026-08-01
Section titled “[1.1.2] - 2026-08-01”cargo binstall crawlberg-clisupport — prebuilt CLI binaries can now be installed directly from GitHub Releases without compiling from source. Adds[package.metadata.binstall]to the CLI crate plus a release-timeverify-binstallCI job that installs viacargo binstalland smoke-tests the binary across the target matrix.
Changed
Section titled “Changed”- Updated dependencies.
[1.1.0] - 2026-07-31
Section titled “[1.1.0] - 2026-07-31”- Advertise a typed
outputSchema(SEP-2106) on every MCP tool, derived from the result types viaschemars(gated behind themcpfeature). This completes the structured-output story: clients now get both the machine-readablestructuredContentand a schema to validate it against.download,get_version, and the batch tools serialize dedicated DTOs so their schema and output share one source of truth. Drift tests assert every serialized field is a declared schema property and every required property is emitted, so the schema andstructuredContentcan never diverge.
Changed
Section titled “Changed”- Raw
println!/eprintln!/print!/eprint!/dbg!are denied in production code across the whole workspace (clippyprint_stdout/print_stderr/dbg_macro);tracingis the sole diagnostic surface, and CLI result output to stdout opts back in per call site (#[expect(clippy::print_stdout)]). Language bindings were regenerated with alef 0.48.11. - Breaking: the
telemetry-initCargo feature is renamed tootelto match the org-wide observability feature name; update--features telemetry-initinvocations to--features otel. - Breaking: the
crawlberglibrary is now emit-only — it installs no global subscriber or OTLP exporter. The subscriber/OTLP install (init_otlp,TelemetryConfig,TelemetryGuard,TelemetryInitError) and the console-logging module (LogConfig,LogFormat,try_init,layer) moved tocrawlberg-cli; the libraryloggingfeature is removed. The libraryotelfeature no longer pulls the exporter/subscriber stack — it only forwardsliter-llm/otelso theaiintegration’s GenAI metrics compile in. crawlberg’s own spans, semantic-convention attributes, and metric instruments remain always-on and flow into whatever exporter the consumer installs. The W3C helpers (with_traceparent,current_traceparent) are unchanged. Consumers that installed telemetry via the library should usecrawlberg-cli --features otel(export is activated at runtime byOTEL_EXPORTER_OTLP_ENDPOINT) or install their own subscriber. crawlberg-cligains anotelfeature that installs the OTLP export pipeline for every command (includingserve), gated at runtime byOTEL_EXPORTER_OTLP_ENDPOINT; the console subscriber is installed by default when OTLP is not configured. The server Docker image builds withcrawlberg-cli/otel.- Upgrade
html-to-markdown-rs3.9 → 3.10 andliter-llm1.11 → 1.12.liter-llm1.12 makestracingan always-on dependency (itstracingCargo feature is gone) and ships a real OTLP export path in its CLI; crawlberg’sotelforwarding toliter-llm/otel(behindai) is unaffected.
- The publish workflow no longer leaves the Homebrew formula pointing at a stale bottle when a release republishes the CLI.
[1.0.12] - 2026-07-30
Section titled “[1.0.12] - 2026-07-30”- Leverage the rmcp 3.0 Tasks extension (SEP-2663): the MCP server advertises the
io.modelcontextprotocol/taskscapability and, when a client both declares it and augments atools/call, runs the tool as a pollable async task (tasks/get/tasks/update/tasks/cancel) instead of blocking. Task support is exercised end-to-end over the stdio transport; on the stateless HTTP transport, which cannot propagate per-request client capabilities, a task-augmented call degrades gracefully to inline execution. crawlberg mcp --http [--host <h>] [--port <p>]serves the MCP Streamable HTTP transport directly (stdio remains the default, so existing client manifests are unaffected). Requires themcp-httpfeature.
Changed
Section titled “Changed”- MCP tool results now carry machine-readable
structuredContent(SEP-2106) alongside the human-readable text block, so schema-aware clients get typed output regardless of theformatparameter. - The Streamable HTTP MCP transport is now stateless by default (SEP-2567):
legacy_session_modeis disabled andjson_responseenabled, with a shared,Arc-backed task store so tasks remain observable across requests. - Upgrade
base64from 0.22 to 0.23, aligning with rmcp 3.0’s requirement.
[1.0.11] - 2026-07-29
Section titled “[1.0.11] - 2026-07-29”Changed
Section titled “Changed”- Upgrade
rmcp(andrmcp-macros) from 2.0 to 3.0. The MCP server, param, and error code is source-compatible with the new major, so no adjustments were needed; contract and HTTP transport tests pass unchanged. - Update the remaining Rust dependencies within range (
schemars,tokio-stream,sse-stream,ref-cast). - Regenerate all language bindings on alef 0.48.8, which fixes the Swift e2e
suite (optional
Vec<Named>metadata fields such asheadingsare JSON-bridged to aRustStringgetter and are no longer emitted as uncompilable.countassertions) and adds a per-RID native runtime project for the C# meta+runtime split.
- Refresh the PHP e2e
composer.locksoguzzlehttp/guzzleresolves to^8.0; the lock still pinned 7.x against the^8.0constraint, abortingcomposer installbefore the PHP e2e suite could run.
[1.0.10] - 2026-07-27
Section titled “[1.0.10] - 2026-07-27”Changed
Section titled “Changed”- Regenerate all language bindings on alef 0.48.4, which fixes Java (Maven)
publishing by lowering the maven-enforcer version floor and fixes C# (NuGet)
publishing by generating a
runtime.jsontemplate rendered at pack time. - Verify Rust dependencies against their latest incompatible versions; all were already current, so no dependency versions changed.
[1.0.9] - 2026-07-26
Section titled “[1.0.9] - 2026-07-26”Changed
Section titled “Changed”- Regenerate all language bindings on alef 0.48.2.
- Update dependencies to their latest compatible versions.
Removed
Section titled “Removed”- Remove unused Java PMD ruleset and stale linter configuration.
[1.0.8] - 2026-07-20
Section titled “[1.0.8] - 2026-07-20”- wasm32 builds no longer fail compiling
mio.reqwestwas declared with its default feature set (default-tls,http2,system-proxy), which enablestokio/net→mioat the Cargo-manifest level.miohas no wasm32 support, so any downstream wasm build that pulls crawlberg (e.g.xberg-wasm) failed to compile — even though reqwest’s own code cfg-gates its native transport off wasm.reqwestis nowdefault-features = falseat the workspace level, with the native TLS/HTTP2/proxy features re-added only under[target.'cfg(not(target_arch = "wasm32"))'.dependencies]in the crates that need them (crawlberg,crawlberg-browser,crawlberg-bypass, and the internalbenchmark-harnesstool). Native behavior is unchanged; wasm builds get a fetch-backed reqwest with no tokio/mio.
[1.0.7] - 2026-07-19
Section titled “[1.0.7] - 2026-07-19”- Elixir NIF now builds and publishes. 1.0.6 could not publish the Elixir
package — the generated streaming-start NIF cloned the
Arc<RwLock<Handle>>and called a core stream method that does not exist on it (E0599), failing all NIF builds. Regenerated with alef 0.38.0, the streaming NIF read-locks and clones the inner handle first, matching the non-streaming path. - Elixir
create_engine/1no longer double-encodes its config. The generated binding unconditionally re-encoded its argument, so the documentedJason.encode!(%CrawlConfig{})string form was JSON-encoded twice (serde rejected the string) andcreate_engine(nil)became"null". alef 0.38.0 forwardsniland pre-encoded strings as-is, encoding only native maps. - Dart
freezeddev-dependency pinned back to^3.2.5. The 1.0.6 release carried a4.0.0-dev.3prerelease that requires a newer Dart SDK than CI provides; reverted sodart pub getresolves the stable release. (packages/dart/pubspec.yaml) - Swift e2e length assertions on JSON-bridged metadata collections compile
again.
metadata.headings/hreflangs/faviconsareOption<Vec<T>>fields that swift-bridge exposes as a scalarRustString(no.count), so the generated.lengthassertions emitted uncompilable.count. alef 0.38.0 skips these, matching the other C-ABI backends.
- Bindings, stubs, READMEs, docs, and e2e suites regenerated with alef 0.38.0 (up from 0.34.4).
[1.0.6] - 2026-07-19
Section titled “[1.0.6] - 2026-07-19”map()/map_urls()no longer materialize the entire sitemap tree before applyingmap_limit. The limit previously bounded only the returned slice, not peak memory: a large sitemap-index host could drive the process into multiple GB and be OOM-killed even with a smallmap_limitset.map_limitand theexclude_paths/map_searchfilters are now compiled once and threaded through the sitemap fetch loop — entries are filtered as they are parsed, and both child-sitemap fetching and per-child parsing stop once the limit is reached. Peak memory is bounded to roughly the limit plus a single child sitemap. (crates/crawlberg/src/map.rs,crates/crawlberg/src/sitemap.rs) Closes #33.
- Refreshed in-major dependencies (
deno_core0.408,uuid1.24) and lock files. - Internal maintenance: pruned stale TODO markers, closed remaining todo gaps, and added the ai-rulez Poly commit hooks.
[1.0.5] - 2026-07-09
Section titled “[1.0.5] - 2026-07-09”Security
Section titled “Security”- Per-hop SSRF re-validation on the headless-browser tier. Closes the known
limitation noted in 1.0.4: real headless Chrome follows 3xx redirects and
client-side navigations internally, so only the seed URL was checked. Browser
fetches now enable CDP Fetch interception for the duration of each navigation
and validate every request URL (initial navigation, redirects, and
subresources) against the SSRF policy before Chrome connects. Blocked requests
are failed with
BlockedByClient; a blocked main-frame request surfaces as a preciseCrawlError::SsrfPolicyViolationrather than a generic navigation error. This brings the chromiumoxide backend to parity with the native backend, which already re-validates each redirect hop. (crates/crawlberg/src/browser.rs)
- Bindings, stubs, READMEs, docs, and e2e suites regenerated with alef 0.34.4
(up from 0.31.1). The 0.34.4 scaffold formats generated files in place instead
of excluding them from poly, and refreshes the
.gitattributes/.pubignorescaffolding.
[1.0.4] - 2026-07-09
Section titled “[1.0.4] - 2026-07-09”Security
Section titled “Security”-
SSRF validation on the headless-browser tier. The browser fallback (reached directly via
BrowserMode::Always/Stealth, or via dispatch escalation toTier::Browser) navigatedpage.goto(url)without the SSRF check the HTTP tier already enforced, so a seed or escalated URL could reach loopback, RFC1918, link-local, or cloud-metadata addresses through a real browser. The target is now validated againstCrawlConfig::ssrf— the samedeny_privatepolicy and DNS resolution as the HTTP tier — before any navigation. (crates/crawlberg/src/browser.rs)Known limitation: in-browser redirects and client-side navigations are not yet re-validated per hop (that requires CDP request interception); the pre-navigation check plus
deny_privatecover the direct and DNS-rebinding-on-the-seed vectors.
[1.0.3] - 2026-07-04
Section titled “[1.0.3] - 2026-07-04”Maintenance release. Migrated pre-commit hooks to poly + mago (dropping prek,
phpstan, and php-cs-fixer), made the update/upgrade tasks resilient to
per-language failures, and regenerated bindings. Version-only bump synced
across all manifests.
[1.0.2] - 2026-07-02
Section titled “[1.0.2] - 2026-07-02”Maintenance release. Migrated the toolchain to poly via the shared reusable validate workflow, upgraded binding dependencies, and regenerated bindings. Version-only bump synced across all manifests.
[1.0.1] - 2026-06-29
Section titled “[1.0.1] - 2026-06-29”Maintenance release. Version-only bump synced across all manifests; .gitignore
ai-rulez block reorganized.
[1.0.0] - 2026-06-27
Section titled “[1.0.0] - 2026-06-27”First stable release. Promotes 1.0.0-rc.2; version-only bump synced across all manifests.
[1.0.0-rc.2] - 2026-06-27
Section titled “[1.0.0-rc.2] - 2026-06-27”Release candidate 2. Maintenance release with version bump.
[1.0.0-rc.1] - 2026-06-26
Section titled “[1.0.0-rc.1] - 2026-06-26”Changed
Section titled “Changed”- Renamed the project from
kreuzcrawltocrawlberg. The crate (crawlberg), every per-language package, the C FFI symbol prefix (kcrawl_*→cberg_*), the Go module (github.com/xberg-io/crawlberg), and the docs domain (docs.crawlberg.xberg.io) follow. - Rebranded the
kreuzbergnamespace toxberg. npm scope@kreuzberg→@xberg-io, JVM/Maven groupIddev.kreuzberg→io.xberg, ecosystem links and badges move togithub.com/xberg-io/xbergand theXberg.devbrand, andKREUZBERG_*env vars becomeCRAWLBERG_*. The legal entity name (Kreuzberg, Inc.) is unchanged.
- Swift publish now creates the
release/swift/<version>branch carrying the substituted XCFramework checksum. The alef-generated Swift e2e/test-app pins.package(url: …, branch: "release/swift/<version>"), but the publish workflow only force-moved thev<version>tag and never created that branch, so SwiftPM could not resolve the package. The checksummed commit is now also pushed torefs/heads/release/swift/<version>. (.github/workflows/publish.yaml)
[0.3.0] - 2026-06-23
Section titled “[0.3.0] - 2026-06-23”First stable release. crawlberg ships a Rust core with active bindings for Python, TypeScript/Node, Ruby, PHP, Go, Java/JNI, C#, Elixir, WebAssembly, Dart, Kotlin/Android, Swift, Zig, and C FFI, plus a CLI, an HTTP API, and an MCP server.
- Tiered dispatch engine. The crawl engine chains HTTP → Bypass → Browser
tiers driven by per-attempt signals rather than a single bypass
short-circuit. Public
crawlberg::types::dispatchsurface:Tier,EscalationStrategy,EscalationReason,AttemptOutcome,RetryDirective,RetryPolicy,WafSignal,WafClassifier,DomainStatePort,DomainRecommendation,EscalationBudget, andDispatchProfile(dispatch enums are#[non_exhaustive]).CrawlConfig::builder()andDispatchProfile::builder()provide fluent construction. - WAF detection. A TOML fingerprint corpus (
rules/waf_fingerprints.toml, 34 fingerprints) with an Aho-Corasick matcher,TomlClassifier::watch()hot-reload (debounced, atomicArcSwap, Kubernetes ConfigMap-safe), andEwmaDomainStatefor per-domain block-rate tracking that promotes/demotes the starting tier. - SSRF defense. New
crawlberg::net::ssrfmodule —SsrfPolicy,HostMatcher(Exact/Suffix/Cidr),SsrfError, and asyncvalidate_url.CrawlConfig::ssrfplus builder methodsallow_private_networks(bool)andssrf_allowlist_host(HostMatcher);CrawlError::SsrfPolicyViolation. Exposed as a settable DTO (deny_private,max_redirects) across every binding. - Browser pool injection.
BrowserPool/BrowserPoolConfigandNativeBrowserExecutor/NativeBrowserExecutorConfigare public;CrawlEngineBuilder::with_browser_pool/with_native_executorandCrawlEngineHandle::from_enginelet consumers construct andwarm()a pool once and reuse it across all crawl jobs. - Public substrate parsers.
crawlberg::robotsandcrawlberg::sitemapare public (parse_robots_txt,is_path_allowed,RobotsRules,parse_sitemap_xml,parse_sitemap_index,is_sitemap_index) — usable without spinning up the engine. - Pluggable proxy rotation.
ProxyProvidertrait +StaticProxyProviderbaseline, wired into the reqwest fetch path viaCrawlEngineBuilder::with_proxy_provider; called per request and taking precedence over the staticCrawlConfig::proxyvalue. - CLI.
batch-scrape,batch-crawl,download,citations, andversionsubcommands, bringing the CLI to 1:1 with the core and MCP surfaces. - MCP server. Tools are 1:1 with the CLI (
batch_crawl,generate_citations, …), each declaringread_only/destructive/open_worldsafety annotations, and are served over both stdio and rmcp Streamable HTTP at/mcpwhen the binary is built with theapi+mcpfeatures. - Observability. OpenTelemetry counters
crawlberg_waf_fingerprint_matches_totalandcrawlberg_escalations_total, plus property tests, cargo-fuzz targets, and Criterion benchmarks covering the WAF subsystem.
Changed
Section titled “Changed”- Memory-bounded streaming crawl.
crawl_stream/batch_crawl_streammove each page into itsCrawlEvent::Pageand drop it instead of accumulating every page, bounding peak memory on large crawls (≈2.5 GB → ≈20 MB working set).crawl()’s batch result is unchanged. - Dispatch model.
CrawlError::WafBlockedis now a struct variant ({ vendor, message });DomainStatePortmoved to an observation model (recommend/observe);SimpleRetryPolicy’s off-by-one is fixed (max_retries=3yields 3 retries);#[non_exhaustive]added toCrawlError,NetworkErrorKind, and the dispatch enums so future variants are non-breaking. - Asset downloads route through
http_fetch, so every file fetch is subject to the SSRF policy.
- Crawl loop materializes downloaded documents. The
download_documentsflag was previously honored only by single-pagescrape(); the crawl loop now buildsCrawlPageResult.downloaded_documentfor linked PDFs/DOCX via a shared helper instead of fetching, flagging, and discarding the bytes. - SSRF rollout hardening. Follow-up fixes to the SSRF refactor: redirect
final_urlis tracked again (per-hop re-validation moved intofollow_redirects), within-batch URL dedup no longer races, crawl child-depth is incremented (restoringmax_depthandinclude_pathssemantics), andCrawlConfigJSON deserialization honorsCRAWLBERG_ALLOW_PRIVATE_NETWORKthrough aSsrfPolicy::from_envserde default. Each is covered by a regression test. - MCP server exposed zero tools. The handler was missing rmcp’s
#[tool_handler], sotools/list/tools/callreturned an empty list over both stdio and HTTP; it now delegates to the generated tool router.
Security
Section titled “Security”- SSRF defense, enabled by default.
scrape(),crawl(),batch_crawl(), sitemap fetch, robots.txt fetch, and asset download refuse URLs resolving to loopback (127.0.0.0/8), RFC1918 private networks, link-local (169.254.0.0/16), cloud metadata (0.0.0.0/8), multicast (224.0.0.0/4), IPv6 ULA (fc00::/7), IPv6 link-local (fe80::/10), IPv6 multicast (ff00::/8), or any non-http(s) scheme. Includes DNS-rebinding mitigation (every resolved IP must pass the policy), redirect-chain re-validation (bounded byssrf.max_redirects, default 5), and link-enqueue validation with bounded concurrency. Opt out viaCRAWLBERG_ALLOW_PRIVATE_NETWORK=1orCrawlConfig::allow_private_networks(true).
- Bindings, facades, READMEs, docs, stubs, and e2e suites are generated by alef (pinned at 0.26.6) across all 14 language targets.
- Publish-pipeline hardening: a native per-arch Docker matrix that drops QEMU emulation, Flutter-free Dart native builds for pub.dev, Swift artifactbundle checksum injection and Apple system-framework linking, and lockfile-preserving source publishes for the Elixir NIF, PHP extension, and Ruby gem.