Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
18e095d
Make memory corrections durable and refresh ANN across writers
RodrigoCordoba Sep 5, 2026
84b0bd4
Isolate delayed correction evidence and guard index publication
RodrigoCordoba Sep 5, 2026
e8e316e
fix(brain): bound live retrieval and post-rerank usefulness
RodrigoCordoba Sep 5, 2026
bf34021
Preserve distinct claims and make lifecycle decisions conservative
RodrigoCordoba Sep 5, 2026
3316112
fix(brain): fail closed on unbound exposures and preserve graph policy
RodrigoCordoba Sep 5, 2026
d45b7f7
Persist temporary applicability through ingestion and proposal review
RodrigoCordoba Sep 5, 2026
c688c68
Expose opt-in shared ONNX inference thread control
RodrigoCordoba Sep 5, 2026
01cc74f
fix(brain): bound final context delivery and disable Free host harves…
RodrigoCordoba Sep 5, 2026
e0611db
fix(remote): preserve compact context delivery budget
RodrigoCordoba Sep 5, 2026
da92a95
Preserve replay history and make memory rebuilds atomic under concurr…
RodrigoCordoba Sep 5, 2026
1eb5981
Stage historical trace events before binding revisions in causal replay
RodrigoCordoba Sep 5, 2026
a01d9ad
Bind memory feedback to delivered claims and preserve recoverable wor…
RodrigoCordoba Sep 5, 2026
87b5dca
Prevent rejected-claim restore and select explicit episode lanes cons…
RodrigoCordoba Sep 5, 2026
b514085
Advertise episode identity on the context tool schemas
RodrigoCordoba Sep 5, 2026
2ef07e4
fix(brain): unify measured delivery and evidence-aware tuning
RodrigoCordoba Sep 5, 2026
9965795
fix(eval): preserve fixture expiry and supersession
RodrigoCordoba Sep 5, 2026
9c130de
fix(sync): replay merged lifecycle imports atomically
RodrigoCordoba Sep 5, 2026
73d2dd2
validate warm-start claims and isolate task delivery
RodrigoCordoba Sep 5, 2026
ece62f2
bind warm digest delivery to current assembled inputs
RodrigoCordoba Sep 5, 2026
f99e1b6
reject failed requested embedders at measured serving boundaries
RodrigoCordoba Sep 5, 2026
b965899
fix: index remote repository files under owning brain
RodrigoCordoba Sep 5, 2026
7ac83ae
docs: record hardening results and paired memory benchmarks
RodrigoCordoba Sep 5, 2026
41ff583
Add configurable rerank cutoff and pinned multilingual model option
RodrigoCordoba Sep 7, 2026
a5e1b6a
Document corrected retrieval benchmarks and multilingual tradeoffs
RodrigoCordoba Sep 7, 2026
06b2946
Add opt-in explicit-fact admission guard for agent memory
RodrigoCordoba Sep 7, 2026
9d893f9
Record answerability quality and performance comparisons
RodrigoCordoba Sep 7, 2026
ab44800
Preserve verification logs referenced by answerability manifest
RodrigoCordoba Sep 7, 2026
3ae8329
Add scoped fact evidence and partial answer delivery
RodrigoCordoba Sep 7, 2026
281c6df
Document structured fact evidence validation and remaining gaps
RodrigoCordoba Sep 7, 2026
a961539
Document unreleased answerability results and format fact modules
RodrigoCordoba Sep 7, 2026
a7efc93
Resolve Clippy warnings in memory hardening code
RodrigoCordoba Sep 7, 2026
b8990cf
Remove stale rerank floor comment from daemon state
RodrigoCordoba Sep 7, 2026
f62b7b7
Prepare v2.8.0 and harden remote paths and Bedrock transport
RodrigoCordoba Sep 8, 2026
785c3df
Link benchmark security follow-up after harness merge
RodrigoCordoba Sep 8, 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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Preserve the exact bytes of reproducible audit artifacts.
docs/audits/2026-09-07-retrieval/** -text whitespace=cr-at-eol
docs/audits/2026-09-07-answerability/** -text whitespace=cr-at-eol,-blank-at-eof
6 changes: 5 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,15 @@ jobs:
BEGIN { include = 0 }
/^## / {
if (include == 1) exit
if ($2 == v) include = 1
if ($2 == v || $2 == v ":") include = 1
else next
}
include == 1 { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "::error::No changelog content extracted for v${VERSION}"
exit 1
fi
else
echo "Release v${VERSION}" > release-notes.md
echo "" >> release-notes.md
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,52 @@ breaking changes require a major bump.

## Unreleased

## v2.8.0: Durable memory and scoped answerability

Structured answerability remains opt-in. Existing brains
upgrade to schema 15 when opened by this version. Memory projection maintenance
adds local storage/write work even with the guard disabled; no extra model calls
are required for fact extraction or evidence accounting.

### Added

- Opt-in `broker.explicit_fact_guard` for scoped configuration evidence, partial
answers and conflicting values. Schema 15 stores a rebuildable fact projection
bound to source events, revisions and visible evidence. No additional model
calls; delivered evidence still consumes context tokens.
- Reproducible paired evaluation and per-query delivered evidence. The new
45-query synthetic fixture retained 24/27 positive hits and reduced unwanted
injections from 15/18 to 3/18. Exact metadata matched 36/45 in both repeats;
p95 was 376.6 → 386.6 ms. Compound retrieval and unsupported subjects remain
gaps. See [answerability](docs/answerability.md) and the [report](docs/audits/2026-09-07-structured-facts.md).
- Configurable reranker cutoff, an optional pinned multilingual reranker and
opt-in shared ONNX inference thread control. Existing model defaults remain.

### Fixed

- Durable corrections and cross-writer ANN refresh; delayed feedback is bound to
delivered claim revisions. Lifecycle decisions preserve distinct claims and
archive/restore state; replay and merged sync imports are atomic.
- Final serialized delivery budgets, post-rerank policy, current warm-start
evidence and explicit episode lanes. Free-tier hooks avoid host harvesting.
- Conflict warnings survive intermediate budgets, capsule caps and daemon
transport. Tagged agent-recorded lessons can produce structured evidence.
- Remote context honors an absent server reranker instead of loading the local
stdio configuration and consulting its warm-start cache.
- Release notes correctly extract changelog headings with a version followed by
a colon; empty extracted notes now fail the release job.

### Security

- Updated transitive `h2` from 0.4.14 to 0.4.16, fixing
`RUSTSEC-2026-0258` (unbounded empty HTTP/2 DATA frames).
- Reject repository identifiers that alias Windows paths and reject redirected
repository roots. Validate existing brain state paths before first-use shortcuts.
- Restrict Bedrock region values to a hostname label, encode model IDs as path
segments, require HTTPS, and disable redirects for signed requests.
- Replace the yanked `der` 0.8.0 dependency with 0.8.2. See the
[release security review](docs/audits/2026-09-08-release-security.md) for the
dependency audit and the disposition of existing code-scanning alerts.

## v2.7.0: Retrieval that knows when to stay silent

Expand Down
19 changes: 10 additions & 9 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 @@ -15,7 +15,7 @@ resolver = "3"
# everything below except the per-crate `description`, `keywords`,
# and `categories` which live in each `[package]` block since
# crates.io enforces them per crate.
version = "2.7.0"
version = "2.8.0"
edition = "2024"
# Rust ecosystem dual-license (matches tokio, serde, fastembed-rs,
# etc.). UNLICENSED would block crates.io entirely.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ ingest, TLS, Prometheus metrics, and a server-side reranker. Full setup in

## Docs

- **[v2.8.0 answerability](docs/answerability.md)**: opt-in scoped facts,
partial/conflicting evidence, configuration and limits. On the new 45-query
synthetic fixture, unwanted injections fell **15/18 → 3/18**, with **24/27 hits
retained**; this is not a new overall benchmark score. [Measured report](docs/audits/2026-09-07-structured-facts.md).

- **[Install & host wiring](https://kimetsu.dev/docs/install)**: every install path, host
wiring, auto-harvest and distiller setup, maintenance commands.
- **[How Kimetsu Works](https://kimetsu.dev/docs/how-kimetsu-works)**: the brain, the broker,
Expand Down
4 changes: 2 additions & 2 deletions crates/kimetsu-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ aws-sigv4.workspace = true
aws-smithy-runtime-api.workspace = true
blake3.workspace = true
http.workspace = true
kimetsu-brain = { path = "../kimetsu-brain", version = "2.7.0" }
kimetsu-core = { path = "../kimetsu-core", version = "2.7.0" }
kimetsu-brain = { path = "../kimetsu-brain", version = "2.8.0" }
kimetsu-core = { path = "../kimetsu-core", version = "2.8.0" }
regex.workspace = true
reqwest.workspace = true
rusqlite.workspace = true
Expand Down
108 changes: 86 additions & 22 deletions crates/kimetsu-agent/src/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ impl BedrockProvider {
return Ok(None);
};

let client = Client::builder()
.timeout(Duration::from_secs(config.model.request_timeout_secs))
.build()?;
bedrock_endpoint(&region, &config.model.model)?;
let client = bedrock_client(config.model.request_timeout_secs)?;

Ok(Some(Self {
client,
Expand All @@ -123,16 +122,17 @@ impl BedrockProvider {
temperature: f32,
timeout_secs: u64,
) -> KimetsuResult<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()?;
let region = region.into();
let model_id = model_id.into();
bedrock_endpoint(&region, &model_id)?;
let client = bedrock_client(timeout_secs)?;
Ok(Self {
client,
access_key: SecretString::new(access_key.into()),
secret_key: SecretString::new(secret_key.into()),
session_token: session_token.map(SecretString::new),
region: region.into(),
model_id: model_id.into(),
region,
model_id,
max_output_tokens,
temperature,
timeout_secs,
Expand Down Expand Up @@ -238,23 +238,19 @@ impl ModelProvider for BedrockProvider {
&request,
);
let payload = serde_json::to_vec(&body)?;
let url = format!(
"https://bedrock-runtime.{}.amazonaws.com/model/{}/invoke",
self.region,
url_encode_model_id(&self.model_id),
);
let url = bedrock_endpoint(&self.region, &self.model_id)?;

let headers = sign_bedrock_headers(
self.access_key.expose_secret(),
self.secret_key.expose_secret(),
self.session_token.as_ref().map(|s| s.expose_secret()),
&self.region,
&url,
url.as_str(),
&payload,
SystemTime::now(),
)?;

let mut req = self.client.post(&url);
let mut req = self.client.post(url);
for (name, value) in &headers {
req = req.header(name.as_str(), value.as_str());
}
Expand All @@ -274,13 +270,35 @@ impl ModelProvider for BedrockProvider {
}
}

/// Percent-encode characters in model IDs that could be misinterpreted in URL
/// paths. Bedrock model IDs typically contain only alphanumerics, hyphens,
/// dots, and colons — but the colon must be percent-encoded in URL paths to
/// avoid ambiguity with `scheme:`.
fn url_encode_model_id(model_id: &str) -> String {
// Only colons need encoding in practice; percent-encode the rest if needed.
model_id.replace(':', "%3A")
fn bedrock_client(timeout_secs: u64) -> KimetsuResult<Client> {
Ok(Client::builder()
.https_only(true)
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(timeout_secs))
.build()?)
}

/// Only an AWS region label can influence the fixed HTTPS authority. Model IDs
/// are encoded as one path segment; credentials are never sent across redirects.
fn bedrock_endpoint(region: &str, model_id: &str) -> KimetsuResult<reqwest::Url> {
if region.is_empty()
|| region.len() > 63
|| !region
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
|| region.starts_with('-')
|| region.ends_with('-')
{
return Err("invalid AWS region: expected a lowercase region label".into());
}
if model_id.is_empty() {
return Err("Bedrock model id is empty".into());
}
let mut url = reqwest::Url::parse(&format!("https://bedrock-runtime.{region}.amazonaws.com"))?;
url.path_segments_mut()
.map_err(|_| "invalid Bedrock endpoint")?
.extend(["model", model_id, "invoke"]);
Ok(url)
}

#[cfg(test)]
Expand All @@ -289,6 +307,52 @@ mod tests {
use crate::model::{MessageContent, MessageRole, ModelMessage, ToolChoice};
use serde_json::json;

#[test]
fn endpoint_rejects_region_authority_injection_and_encodes_model_id() {
for region in [
"",
"us-east-1@attacker.test",
"us-east-1/evil",
"us-east-1?x",
"us-east-1#x",
"us-east-1:443",
"US-EAST-1",
"us-east-1.evil",
] {
assert!(
bedrock_endpoint(region, "test-model").is_err(),
"region {region}"
);
}
let endpoint = bedrock_endpoint(
"us-east-1",
"arn:aws:bedrock:us-east-1:123:model/example?x#y",
)
.unwrap();
assert_eq!(endpoint.scheme(), "https");
assert_eq!(
endpoint.host_str(),
Some("bedrock-runtime.us-east-1.amazonaws.com")
);
assert_eq!(endpoint.query(), None);
assert_eq!(endpoint.fragment(), None);
assert!(endpoint.path().contains("%2F"));
assert!(endpoint.path().ends_with("/invoke"));
}

#[test]
fn bedrock_client_rejects_plaintext_before_connecting() {
let error = bedrock_client(1)
.unwrap()
.post("http://127.0.0.1:9/")
.send()
.unwrap_err();
assert!(
error.is_builder(),
"HTTPS-only validation must reject before I/O: {error}"
);
}

fn simple_request() -> ModelRequest {
ModelRequest {
messages: vec![ModelMessage::user_text("Hello")],
Expand Down
Loading
Loading