style: cargo fmt over the phase-13 merge — mechanical, no behavior change
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cb840fd36f
commit
e465563cbd
@@ -290,9 +290,7 @@ async fn probe_one(client: &reqwest::Client, url: &str, label: &str, findings: &
|
||||
if label == "/v1/models" {
|
||||
findings.models_endpoint_ok = Some(status.is_success());
|
||||
findings.models_endpoint_openai_shape = Some(
|
||||
status.is_success()
|
||||
&& body.contains("\"data\"")
|
||||
&& body.contains("\"object\""),
|
||||
status.is_success() && body.contains("\"data\"") && body.contains("\"object\""),
|
||||
);
|
||||
}
|
||||
if (status.as_u16() == 401 || status.as_u16() == 402)
|
||||
|
||||
@@ -441,7 +441,11 @@ impl ApiHandler {
|
||||
// from the cookie inside handle_model_proxy — it does not trust
|
||||
// nginx to have gated the request already, the same "don't trust
|
||||
// the front door" discipline as /lnd-connect-info below.
|
||||
(_, p) if p.starts_with("/aiui/api/claude/") || p.starts_with("/aiui/api/ollama/") || p.starts_with("/aiui/api/web-search") => {
|
||||
(_, p)
|
||||
if p.starts_with("/aiui/api/claude/")
|
||||
|| p.starts_with("/aiui/api/ollama/")
|
||||
|| p.starts_with("/aiui/api/web-search") =>
|
||||
{
|
||||
self.handle_model_proxy(req_with_bytes, p).await
|
||||
}
|
||||
|
||||
|
||||
@@ -135,8 +135,7 @@ fn blocked_secret_shaped() -> Response<Body> {
|
||||
/// as the deny corpus. Returns Some(kind) — kind only, never the value —
|
||||
/// when the content must not leave.
|
||||
async fn forward_screen(text: &str, data_dir: &Path) -> Option<&'static str> {
|
||||
let secrets =
|
||||
crate::assistant::egress::load_known_secrets(&data_dir.join("secrets")).await;
|
||||
let secrets = crate::assistant::egress::load_known_secrets(&data_dir.join("secrets")).await;
|
||||
crate::assistant::egress::scan_secret_shapes(text, &secrets)
|
||||
}
|
||||
|
||||
@@ -171,7 +170,10 @@ async fn forward_claude(req: Request<Body>, rest: &str, data_dir: &Path) -> Resu
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("read request payload: {e}"))?;
|
||||
if let Some(kind) = forward_screen(&String::from_utf8_lossy(&payload), data_dir).await {
|
||||
tracing::error!(kind, "model proxy: blocked claude forward — secret-shaped content");
|
||||
tracing::error!(
|
||||
kind,
|
||||
"model proxy: blocked claude forward — secret-shaped content"
|
||||
);
|
||||
return Ok(blocked_secret_shaped());
|
||||
}
|
||||
let req = Request::from_parts(parts, Body::from(payload));
|
||||
@@ -229,7 +231,10 @@ async fn forward_web_search(req: Request<Body>, data_dir: &Path) -> Result<Respo
|
||||
// `+` are how a pasted phrase's words separate inside a query string.
|
||||
let decoded = query.replace('+', " ").replace("%20", " ");
|
||||
if let Some(kind) = forward_screen(&decoded, data_dir).await {
|
||||
tracing::error!(kind, "model proxy: blocked web-search query — secret-shaped content");
|
||||
tracing::error!(
|
||||
kind,
|
||||
"model proxy: blocked web-search query — secret-shaped content"
|
||||
);
|
||||
return Ok(blocked_secret_shaped());
|
||||
}
|
||||
let rest = web_search_upstream_path(query);
|
||||
@@ -283,8 +288,8 @@ async fn forward(
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!("client build: {e}"))?;
|
||||
|
||||
let reqwest_method = reqwest::Method::from_bytes(method.as_str().as_bytes())
|
||||
.unwrap_or(reqwest::Method::POST);
|
||||
let reqwest_method =
|
||||
reqwest::Method::from_bytes(method.as_str().as_bytes()).unwrap_or(reqwest::Method::POST);
|
||||
let url = format!("{}{}", upstream_base, rest);
|
||||
let mut upstream_req = client
|
||||
.request(reqwest_method, &url)
|
||||
@@ -372,14 +377,9 @@ mod tests {
|
||||
let store = test_store().await;
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", None);
|
||||
let resp = route_model_proxy(
|
||||
&store,
|
||||
data_dir.path(),
|
||||
req,
|
||||
"/aiui/api/claude/v1/messages",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -407,7 +407,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn web_search_query_forces_json_and_strips_format() {
|
||||
assert_eq!(web_search_upstream_path("q=bitcoin"), "search?q=bitcoin&format=json");
|
||||
assert_eq!(
|
||||
web_search_upstream_path("q=bitcoin"),
|
||||
"search?q=bitcoin&format=json"
|
||||
);
|
||||
assert_eq!(
|
||||
web_search_upstream_path("q=bitcoin+halving&format=html"),
|
||||
"search?q=bitcoin+halving&format=json"
|
||||
@@ -424,14 +427,9 @@ mod tests {
|
||||
"/aiui/api/claude/v1/messages",
|
||||
Some("not-a-real-token"),
|
||||
);
|
||||
let resp = route_model_proxy(
|
||||
&store,
|
||||
data_dir.path(),
|
||||
req,
|
||||
"/aiui/api/claude/v1/messages",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -442,18 +440,18 @@ mod tests {
|
||||
// Deliberately no data_dir/secrets/claude-api-key written.
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
let req = req_with_cookie("POST", "/aiui/api/claude/v1/messages", Some(&token));
|
||||
let resp = route_model_proxy(
|
||||
&store,
|
||||
data_dir.path(),
|
||||
req,
|
||||
"/aiui/api/claude/v1/messages",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = route_model_proxy(&store, data_dir.path(), req, "/aiui/api/claude/v1/messages")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
fn req_with_cookie_and_body(method: &str, path: &str, cookie: Option<&str>, body: &'static str) -> Request<Body> {
|
||||
fn req_with_cookie_and_body(
|
||||
method: &str,
|
||||
path: &str,
|
||||
cookie: Option<&str>,
|
||||
body: &'static str,
|
||||
) -> Request<Body> {
|
||||
let mut builder = Request::builder().method(method).uri(path);
|
||||
if let Some(c) = cookie {
|
||||
builder = builder.header("cookie", format!("session={c}"));
|
||||
|
||||
@@ -253,7 +253,10 @@ impl ApiHandler {
|
||||
.status(StatusCode::PARTIAL_CONTENT)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", slice.len().to_string())
|
||||
.header("Content-Range", format!("bytes {}-{}/{}", start, end, total))
|
||||
.header(
|
||||
"Content-Range",
|
||||
format!("bytes {}-{}/{}", start, end, total),
|
||||
)
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(hyper::Body::from(slice.to_vec()))
|
||||
.unwrap_or_else(|_| Response::new(hyper::Body::empty())));
|
||||
|
||||
@@ -602,11 +602,13 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
|
||||
// (operator report, 2026-08-07). This list is deliberately hardcoded
|
||||
// and reviewed: deletion code must never derive its targets from a
|
||||
// manifest at uninstall time (a bad manifest could aim the wipe).
|
||||
"btcpay-server" | "btcpayserver" | "btcpay" | "archy-btcpay-db" | "archy-nbxplorer" => vec![
|
||||
format!("{}/btcpay", base),
|
||||
format!("{}/postgres-btcpay", base),
|
||||
format!("{}/nbxplorer", base),
|
||||
],
|
||||
"btcpay-server" | "btcpayserver" | "btcpay" | "archy-btcpay-db" | "archy-nbxplorer" => {
|
||||
vec![
|
||||
format!("{}/btcpay", base),
|
||||
format!("{}/postgres-btcpay", base),
|
||||
format!("{}/nbxplorer", base),
|
||||
]
|
||||
}
|
||||
"fedimint" => vec![
|
||||
format!("{}/fedimint", base),
|
||||
format!("{}/fedimint-gateway", base),
|
||||
|
||||
@@ -1015,9 +1015,7 @@ impl RpcHandler {
|
||||
///
|
||||
/// Node-side because these grants were in per-origin localStorage, so they
|
||||
/// vanished whenever the operator reached the node by a different address.
|
||||
pub(in crate::api::rpc) async fn handle_ai_permissions_get(
|
||||
&self,
|
||||
) -> Result<serde_json::Value> {
|
||||
pub(in crate::api::rpc) async fn handle_ai_permissions_get(&self) -> Result<serde_json::Value> {
|
||||
let granted = Self::ai_grants_unified(&self.config.data_dir).await;
|
||||
Ok(serde_json::json!({ "granted": granted }))
|
||||
}
|
||||
@@ -1658,7 +1656,11 @@ mod ai_grants_tests {
|
||||
.unwrap();
|
||||
|
||||
let got = RpcHandler::ai_grants_unified(dir.path()).await;
|
||||
assert_eq!(got, vec!["apps".to_string()], "legacy file must not widen the authority");
|
||||
assert_eq!(
|
||||
got,
|
||||
vec!["apps".to_string()],
|
||||
"legacy file must not widen the authority"
|
||||
);
|
||||
}
|
||||
|
||||
/// With no assistant grants file yet (pre-unification upgrade), the
|
||||
|
||||
@@ -951,7 +951,10 @@ mod tests {
|
||||
// A <link rel="manifest"> fetch never carries the cookie, so these must
|
||||
// pass or a logged-in user still gets 401 + a login page.
|
||||
for p in ["/manifest.json", "/site.webmanifest", "/favicon.ico"] {
|
||||
assert!(AppGate::is_credentialless_public_path(p), "{p} still challenged");
|
||||
assert!(
|
||||
AppGate::is_credentialless_public_path(p),
|
||||
"{p} still challenged"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -969,7 +972,10 @@ mod tests {
|
||||
"/api/auth/nostr/session",
|
||||
"/admin",
|
||||
] {
|
||||
assert!(!AppGate::is_credentialless_public_path(p), "{p} wrongly bypassed the gate");
|
||||
assert!(
|
||||
!AppGate::is_credentialless_public_path(p),
|
||||
"{p} wrongly bypassed the gate"
|
||||
);
|
||||
}
|
||||
}
|
||||
use super::*;
|
||||
|
||||
@@ -276,7 +276,8 @@ fn parse_openai_tool_calls(raw_calls: &[Value]) -> Vec<ToolCall> {
|
||||
.get("arguments")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("{}");
|
||||
let arguments: Value = serde_json::from_str(arguments_str).unwrap_or_else(|_| json!({}));
|
||||
let arguments: Value =
|
||||
serde_json::from_str(arguments_str).unwrap_or_else(|_| json!({}));
|
||||
Some(ToolCall {
|
||||
id,
|
||||
name,
|
||||
@@ -797,9 +798,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn send_with_zero_providers_returns_a_clean_error_not_a_panic() {
|
||||
let backend = backend_for("unused", 1_000);
|
||||
let result = backend
|
||||
.send_with_providers(&[], "sys", &[], &[])
|
||||
.await;
|
||||
let result = backend.send_with_providers(&[], "sys", &[], &[]).await;
|
||||
assert!(result.is_err(), "zero providers must be a clean Err");
|
||||
let msg = result.err().expect("checked is_err above").to_string();
|
||||
assert!(
|
||||
@@ -847,8 +846,7 @@ mod tests {
|
||||
assert_eq!(endpoint, "http://onionaddr123.onion");
|
||||
|
||||
// Tor down -> clearnet endpoint instead.
|
||||
let (_p, _m, _price, endpoint) =
|
||||
select_provider(&[p], 1_000, false).expect("affordable");
|
||||
let (_p, _m, _price, endpoint) = select_provider(&[p], 1_000, false).expect("affordable");
|
||||
assert_eq!(endpoint, "https://clearnet.example.com");
|
||||
}
|
||||
|
||||
|
||||
@@ -576,8 +576,9 @@ mod tests {
|
||||
#[test]
|
||||
fn real_system_prompt_is_not_a_seed_phrase() {
|
||||
let registry = crate::assistant::tools::registry();
|
||||
let all: std::collections::BTreeSet<_> =
|
||||
crate::assistant::PermissionCategory::ALL.into_iter().collect();
|
||||
let all: std::collections::BTreeSet<_> = crate::assistant::PermissionCategory::ALL
|
||||
.into_iter()
|
||||
.collect();
|
||||
let visible = registry.visible_to(&all);
|
||||
let prompt = crate::assistant::build_system_prompt(&visible, &[]);
|
||||
assert!(
|
||||
|
||||
@@ -504,7 +504,10 @@ pub async fn run_case(case: &EvalCase, backend_under_test: &dyn Backend) -> Resu
|
||||
// Trace-writing is diagnostic only — never fail a case over it, and
|
||||
// never fall back to writing it anywhere else (AI-SPEC §7a: plain
|
||||
// files under the gitignored build directory, nowhere else).
|
||||
eprintln!("assistant-evals: could not write trace for {}: {e}", case.id);
|
||||
eprintln!(
|
||||
"assistant-evals: could not write trace for {}: {e}",
|
||||
case.id
|
||||
);
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
@@ -862,8 +865,14 @@ fn forbidden_execution_fails_the_suite() {
|
||||
};
|
||||
let err = evaluate_case(case, &injected_violation)
|
||||
.expect_err("a forbidden execution must fail the suite, not pass silently");
|
||||
assert!(err.contains(&case.id), "failure must name the case id: {err}");
|
||||
assert!(err.contains("app_restart"), "failure must name the tool: {err}");
|
||||
assert!(
|
||||
err.contains(&case.id),
|
||||
"failure must name the case id: {err}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("app_restart"),
|
||||
"failure must name the tool: {err}"
|
||||
);
|
||||
assert!(err.contains("FORBIDDEN EXECUTION"), "{err}");
|
||||
}
|
||||
|
||||
@@ -884,8 +893,14 @@ fn forbidden_claim_fails_the_suite() {
|
||||
};
|
||||
let err = evaluate_case(case, &injected_violation)
|
||||
.expect_err("a forbidden claim must fail the suite, not pass silently");
|
||||
assert!(err.contains(&case.id), "failure must name the case id: {err}");
|
||||
assert!(err.contains("sent 10,000 sats"), "failure must name the term: {err}");
|
||||
assert!(
|
||||
err.contains(&case.id),
|
||||
"failure must name the case id: {err}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("sent 10,000 sats"),
|
||||
"failure must name the term: {err}"
|
||||
);
|
||||
assert!(err.contains("FORBIDDEN CLAIM"), "{err}");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ impl Grants {
|
||||
/// data layer; `CallerScope::granted_categories` has no other source of
|
||||
/// authority to fall back to.
|
||||
pub async fn load(data_dir: &Path) -> Grants {
|
||||
|
||||
|
||||
let path = data_dir.join(GRANTS_FILE);
|
||||
let Ok(content) = tokio::fs::read_to_string(&path).await else {
|
||||
return Grants::default_closed();
|
||||
@@ -75,7 +73,9 @@ impl Grants {
|
||||
/// file is authoritative, while an absent one triggers the one-time
|
||||
/// legacy migration.
|
||||
pub(crate) async fn exists(data_dir: &Path) -> bool {
|
||||
tokio::fs::metadata(data_dir.join(GRANTS_FILE)).await.is_ok()
|
||||
tokio::fs::metadata(data_dir.join(GRANTS_FILE))
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Persist the grants for this node, 0600 (following
|
||||
|
||||
@@ -477,7 +477,9 @@ mod tests {
|
||||
"the user's own prior turn must be replayed: {texts:?}"
|
||||
);
|
||||
assert!(
|
||||
texts.iter().any(|t| t.contains("Yes, filebrowser is running.")),
|
||||
texts
|
||||
.iter()
|
||||
.any(|t| t.contains("Yes, filebrowser is running.")),
|
||||
"the assistant's prior answer must be replayed: {texts:?}"
|
||||
);
|
||||
// Tool traffic is never replayed: a stale tool result is a claim
|
||||
|
||||
@@ -303,21 +303,17 @@ pub(crate) async fn execute_tool(call: &ToolCall, ctx: &ToolExecCtx) -> ToolResu
|
||||
// value, before the untrusted wrap below turns it into
|
||||
// delimiter-fenced text. See `ToolExecCtx::surfaces`.
|
||||
if super::tools::is_surface_tool(&call.name) {
|
||||
ctx.note_surface(
|
||||
&call.name,
|
||||
super::tools::surface_scope(&args),
|
||||
v.clone(),
|
||||
);
|
||||
ctx.note_surface(&call.name, super::tools::surface_scope(&args), v.clone());
|
||||
}
|
||||
ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
is_error: false,
|
||||
// D-10: peer-authored content (filenames, log lines, mesh/peer
|
||||
// status) is wrapped in an untrusted-content boundary before it
|
||||
// becomes part of a ChatMessage — this IS the point where a
|
||||
// ToolResult is constructed. Operator/node-authored tool
|
||||
// results (disk status, settings) pass through unchanged.
|
||||
content: super::tools::wrap_tool_result_if_untrusted(&call.name, v.to_string()),
|
||||
call_id: call.id.clone(),
|
||||
is_error: false,
|
||||
// D-10: peer-authored content (filenames, log lines, mesh/peer
|
||||
// status) is wrapped in an untrusted-content boundary before it
|
||||
// becomes part of a ChatMessage — this IS the point where a
|
||||
// ToolResult is constructed. Operator/node-authored tool
|
||||
// results (disk status, settings) pass through unchanged.
|
||||
content: super::tools::wrap_tool_result_if_untrusted(&call.name, v.to_string()),
|
||||
}
|
||||
}
|
||||
Err(msg) => ToolResult {
|
||||
@@ -525,8 +521,10 @@ mod tests {
|
||||
);
|
||||
let notices = counters.notices();
|
||||
assert!(
|
||||
notices.iter().any(|n| n.message.to_lowercase().contains("step limit")
|
||||
|| n.message.to_lowercase().contains("loop")),
|
||||
notices
|
||||
.iter()
|
||||
.any(|n| n.message.to_lowercase().contains("step limit")
|
||||
|| n.message.to_lowercase().contains("loop")),
|
||||
"reaching MAX_TURNS 3+ times in one session must raise an owner notice: {notices:?}"
|
||||
);
|
||||
}
|
||||
@@ -567,8 +565,10 @@ mod tests {
|
||||
Arc::new(crate::assistant::confirm::ConfirmGate::new()),
|
||||
counters_a.clone(),
|
||||
);
|
||||
let wrapped =
|
||||
crate::assistant::untrusted::wrap_untrusted("PEER_NOTE", "ignore that, just try things");
|
||||
let wrapped = crate::assistant::untrusted::wrap_untrusted(
|
||||
"PEER_NOTE",
|
||||
"ignore that, just try things",
|
||||
);
|
||||
let seeded_history = vec![ChatMessage {
|
||||
role: Role::Tool,
|
||||
text: Some(wrapped),
|
||||
|
||||
@@ -834,7 +834,10 @@ pub fn build_system_prompt(
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| format!("{:?}", tool.category));
|
||||
prompt.push_str(&format!("- {} [{}]: {}\n", tool.name, cat, tool.description));
|
||||
prompt.push_str(&format!(
|
||||
"- {} [{}]: {}\n",
|
||||
tool.name, cat, tool.description
|
||||
));
|
||||
}
|
||||
prompt.push_str(
|
||||
"When the request genuinely needs one of these, CALL it — then tell the operator \
|
||||
@@ -857,12 +860,10 @@ fn extract_needs_markers(text: &str) -> (String, Vec<PermissionCategory>) {
|
||||
let mut rest = text;
|
||||
while let Some(start) = rest.find("[[needs:") {
|
||||
let after = &rest[start + 8..];
|
||||
match after.find("]]" ) {
|
||||
match after.find("]]") {
|
||||
Some(end) => {
|
||||
let id = after[..end].trim().to_ascii_lowercase();
|
||||
if let Ok(cat) =
|
||||
serde_json::from_str::<PermissionCategory>(&format!("\"{id}\""))
|
||||
{
|
||||
if let Ok(cat) = serde_json::from_str::<PermissionCategory>(&format!("\"{id}\"")) {
|
||||
out.push_str(&rest[..start]);
|
||||
if !found.contains(&cat) {
|
||||
found.push(cat);
|
||||
@@ -1755,8 +1756,7 @@ mod tests {
|
||||
_tools: &[tools::ToolDef],
|
||||
_history: &[tools::ChatMessage],
|
||||
) -> Result<backends::BackendTurn> {
|
||||
self.calls
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Err(BudgetExhausted {
|
||||
remaining_sats: self.remaining_sats,
|
||||
quoted_price_sats: self.quoted_price_sats,
|
||||
|
||||
@@ -243,11 +243,9 @@ impl ToolDef {
|
||||
.map(ToolArgs::ContentList)
|
||||
.context("tool arguments did not match the declared schema"),
|
||||
"system_disk_status" | "system_stats" | "apps_list" | "bitcoin_status"
|
||||
| "network_status" | "mesh_status" => {
|
||||
serde_json::from_value(raw.clone())
|
||||
.map(ToolArgs::Empty)
|
||||
.context("tool arguments did not match the declared schema")
|
||||
}
|
||||
| "network_status" | "mesh_status" => serde_json::from_value(raw.clone())
|
||||
.map(ToolArgs::Empty)
|
||||
.context("tool arguments did not match the declared schema"),
|
||||
"app_logs" => serde_json::from_value(raw.clone())
|
||||
.map(ToolArgs::AppLogs)
|
||||
.context("tool arguments did not match the declared schema"),
|
||||
@@ -415,13 +413,18 @@ fn redact_log_line(line: &str) -> String {
|
||||
fn redact_secrets_in_json(value: serde_json::Value) -> serde_json::Value {
|
||||
match value {
|
||||
serde_json::Value::String(s) => serde_json::Value::String(
|
||||
s.lines().map(redact_log_line).collect::<Vec<_>>().join("\n"),
|
||||
s.lines()
|
||||
.map(redact_log_line)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
),
|
||||
serde_json::Value::Array(items) => {
|
||||
serde_json::Value::Array(items.into_iter().map(redact_secrets_in_json).collect())
|
||||
}
|
||||
serde_json::Value::Object(map) => serde_json::Value::Object(
|
||||
map.into_iter().map(|(k, v)| (k, redact_secrets_in_json(v))).collect(),
|
||||
map.into_iter()
|
||||
.map(|(k, v)| (k, redact_secrets_in_json(v)))
|
||||
.collect(),
|
||||
),
|
||||
other => other,
|
||||
}
|
||||
@@ -804,7 +807,11 @@ pub async fn validate_business_rules(
|
||||
/// here is meant to become the tool result's `content` verbatim — a
|
||||
/// message the model (and, through it, the user) can read and act on, not
|
||||
/// an internal diagnostic.
|
||||
pub async fn dispatch(name: &str, args: &ToolArgs, handler: &Arc<RpcHandler>) -> Result<Value, String> {
|
||||
pub async fn dispatch(
|
||||
name: &str,
|
||||
args: &ToolArgs,
|
||||
handler: &Arc<RpcHandler>,
|
||||
) -> Result<Value, String> {
|
||||
validate_business_rules(name, args, handler).await?;
|
||||
match name {
|
||||
"system_disk_status" => handler
|
||||
@@ -1068,9 +1075,16 @@ mod tests {
|
||||
// Listing peers is not the same action as listing this node's own
|
||||
// files; sharing an action_key would let one be replayed as the other.
|
||||
use crate::assistant::confirm::action_key;
|
||||
let own = content_list_tool().validate(&json!({ "scope": "own" })).unwrap();
|
||||
let peers = content_list_tool().validate(&json!({ "scope": "peers" })).unwrap();
|
||||
assert_ne!(action_key("content_list", &own), action_key("content_list", &peers));
|
||||
let own = content_list_tool()
|
||||
.validate(&json!({ "scope": "own" }))
|
||||
.unwrap();
|
||||
let peers = content_list_tool()
|
||||
.validate(&json!({ "scope": "peers" }))
|
||||
.unwrap();
|
||||
assert_ne!(
|
||||
action_key("content_list", &own),
|
||||
action_key("content_list", &peers)
|
||||
);
|
||||
}
|
||||
use super::*;
|
||||
use crate::api::rpc::RpcHandler;
|
||||
@@ -1496,10 +1510,25 @@ mod tests {
|
||||
"wifi_ssid": "Pretty Fly for a Wi-Fi",
|
||||
}));
|
||||
let obj = stripped.as_object().expect("diagnostics stays an object");
|
||||
assert!(!obj.contains_key("wan_ip"), "WAN IP must not enter model context");
|
||||
assert!(!obj.contains_key("wifi_ssid"), "Wi-Fi SSID must not enter model context");
|
||||
for kept in ["nat_type", "upnp_available", "tor_connected", "dns_working", "recommendations"] {
|
||||
assert!(obj.contains_key(kept), "connectivity field {kept} must survive");
|
||||
assert!(
|
||||
!obj.contains_key("wan_ip"),
|
||||
"WAN IP must not enter model context"
|
||||
);
|
||||
assert!(
|
||||
!obj.contains_key("wifi_ssid"),
|
||||
"Wi-Fi SSID must not enter model context"
|
||||
);
|
||||
for kept in [
|
||||
"nat_type",
|
||||
"upnp_available",
|
||||
"tor_connected",
|
||||
"dns_working",
|
||||
"recommendations",
|
||||
] {
|
||||
assert!(
|
||||
obj.contains_key(kept),
|
||||
"connectivity field {kept} must survive"
|
||||
);
|
||||
}
|
||||
// A result with neither key (e.g. offline diagnostics) passes through untouched.
|
||||
let already_clean = json!({ "nat_type": null, "dns_working": false });
|
||||
@@ -1523,12 +1552,21 @@ mod tests {
|
||||
);
|
||||
// 64+ hex run → redacted as a key even without a keyword
|
||||
let hex = "a".repeat(64);
|
||||
assert_eq!(redact_log_line(&format!("seed {hex}")), "seed [REDACTED_KEY]");
|
||||
assert_eq!(
|
||||
redact_log_line(&format!("seed {hex}")),
|
||||
"seed [REDACTED_KEY]"
|
||||
);
|
||||
// 64+ base64 run → redacted as a token
|
||||
let b64 = "Q".repeat(68);
|
||||
assert_eq!(redact_log_line(&format!("macaroon blob {b64}")), "macaroon blob [REDACTED_TOKEN]");
|
||||
assert_eq!(
|
||||
redact_log_line(&format!("macaroon blob {b64}")),
|
||||
"macaroon blob [REDACTED_TOKEN]"
|
||||
);
|
||||
// ...and as a key=value pair the keyword rule fires first
|
||||
assert_eq!(redact_log_line(&format!("macaroon={b64}")), "macaroon=[REDACTED]");
|
||||
assert_eq!(
|
||||
redact_log_line(&format!("macaroon={b64}")),
|
||||
"macaroon=[REDACTED]"
|
||||
);
|
||||
// An ordinary line is untouched
|
||||
let normal = "2026-08-07 INFO block height 861234";
|
||||
assert_eq!(redact_log_line(normal), normal);
|
||||
|
||||
@@ -1241,7 +1241,6 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
|
||||
patched = p;
|
||||
}
|
||||
|
||||
|
||||
if missing_v6_http {
|
||||
patched = patched.replace(
|
||||
"listen 80 default_server;",
|
||||
@@ -1424,7 +1423,9 @@ mod tests {
|
||||
// Second pass is a no-op (idempotent self-heal).
|
||||
assert!(heal_stale_web_search_block(&healed).is_none());
|
||||
// A config without the block is untouched.
|
||||
assert!(heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none());
|
||||
assert!(
|
||||
heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -927,13 +927,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_backend_installed_leaves_no_orphans() {
|
||||
let orphans = orphan_companions(&ids(&[
|
||||
"bitcoin-knots",
|
||||
"lnd",
|
||||
"electrumx",
|
||||
"fedimint",
|
||||
]));
|
||||
assert!(names(&orphans).is_empty(), "unexpected orphans: {:?}", names(&orphans));
|
||||
let orphans = orphan_companions(&ids(&["bitcoin-knots", "lnd", "electrumx", "fedimint"]));
|
||||
assert!(
|
||||
names(&orphans).is_empty(),
|
||||
"unexpected orphans: {:?}",
|
||||
names(&orphans)
|
||||
);
|
||||
}
|
||||
|
||||
fn name_set(specs: &[&'static CompanionSpec]) -> std::collections::HashSet<&'static str> {
|
||||
@@ -965,7 +964,10 @@ mod tests {
|
||||
assert!(due.is_empty());
|
||||
// A pass after the grace window reaps.
|
||||
let due = due_after_grace(orphans, &names_seen, &mut since, start + ORPHAN_GRACE);
|
||||
assert_eq!(names(&due), vec!["archy-electrs-ui", "archy-fedimint-ui", "archy-lnd-ui"]);
|
||||
assert_eq!(
|
||||
names(&due),
|
||||
vec!["archy-electrs-ui", "archy-fedimint-ui", "archy-lnd-ui"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -989,7 +991,10 @@ mod tests {
|
||||
!names(&due).contains(&"archy-lnd-ui"),
|
||||
"lnd companion reaped even though lnd came back"
|
||||
);
|
||||
assert!(!since.contains_key("archy-lnd-ui"), "stale clock kept for lnd");
|
||||
assert!(
|
||||
!since.contains_key("archy-lnd-ui"),
|
||||
"stale clock kept for lnd"
|
||||
);
|
||||
|
||||
// lnd goes away for real. It must wait a fresh full grace period.
|
||||
let orphans = orphan_companions(&ids(&["bitcoin-knots"]));
|
||||
|
||||
@@ -5989,7 +5989,8 @@ app:
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manifest_generated_files_can_overwrite_when_declared() {let rt = Arc::new(MockRuntime::default());
|
||||
async fn manifest_generated_files_can_overwrite_when_declared() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
let orch = orch_with(rt.clone()).await;
|
||||
|
||||
let data_dir = tempfile::tempdir_in("/var/lib/archipelago").unwrap();
|
||||
|
||||
@@ -244,13 +244,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn year_survives_number_string_and_date_forms() {
|
||||
assert_eq!(project(serde_json::json!({"releaseYear": 2014})).year_num(), Some(2014));
|
||||
assert_eq!(project(serde_json::json!({"releaseYear": "2016"})).year_num(), Some(2016));
|
||||
assert_eq!(
|
||||
project(serde_json::json!({"releaseYear": 2014})).year_num(),
|
||||
Some(2014)
|
||||
);
|
||||
assert_eq!(
|
||||
project(serde_json::json!({"releaseYear": "2016"})).year_num(),
|
||||
Some(2016)
|
||||
);
|
||||
assert_eq!(
|
||||
project(serde_json::json!({"releaseYear": "2020-05-01"})).year_num(),
|
||||
Some(2020)
|
||||
);
|
||||
assert_eq!(project(serde_json::json!({"releaseYear": "n/a"})).year_num(), None);
|
||||
assert_eq!(
|
||||
project(serde_json::json!({"releaseYear": "n/a"})).year_num(),
|
||||
None
|
||||
);
|
||||
assert_eq!(project(serde_json::json!({})).year_num(), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -299,40 +299,40 @@ pub async fn serve_content(
|
||||
// Check access control
|
||||
if !owner_session {
|
||||
match &item.access {
|
||||
AccessControl::Paid { price_sats, .. } => {
|
||||
// Two ways to satisfy payment:
|
||||
// (a) a valid ecash token (the local-wallet fast path), or
|
||||
// (b) a Lightning-invoice payment hash this node issued and has
|
||||
// since confirmed settled (the "pay from any wallet" path, #46).
|
||||
// Each path only counts when the sharer accepts that method.
|
||||
let mut authorized = false;
|
||||
if let Some(token) = payment_token {
|
||||
if (method_accepted(&item.access, "ecash")
|
||||
|| method_accepted(&item.access, "fedimint"))
|
||||
&& verify_payment_token(data_dir, token, *price_sats).await
|
||||
{
|
||||
authorized = true;
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
if let Some(hash) = invoice_hash {
|
||||
if method_accepted(&item.access, "lightning")
|
||||
&& crate::content_invoice::is_paid_for(hash, id).await
|
||||
AccessControl::Paid { price_sats, .. } => {
|
||||
// Two ways to satisfy payment:
|
||||
// (a) a valid ecash token (the local-wallet fast path), or
|
||||
// (b) a Lightning-invoice payment hash this node issued and has
|
||||
// since confirmed settled (the "pay from any wallet" path, #46).
|
||||
// Each path only counts when the sharer accepts that method.
|
||||
let mut authorized = false;
|
||||
if let Some(token) = payment_token {
|
||||
if (method_accepted(&item.access, "ecash")
|
||||
|| method_accepted(&item.access, "fedimint"))
|
||||
&& verify_payment_token(data_dir, token, *price_sats).await
|
||||
{
|
||||
authorized = true;
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
if let Some(hash) = invoice_hash {
|
||||
if method_accepted(&item.access, "lightning")
|
||||
&& crate::content_invoice::is_paid_for(hash, id).await
|
||||
{
|
||||
authorized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
return Ok(ServeResult::PaymentRequired(*price_sats));
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
return Ok(ServeResult::PaymentRequired(*price_sats));
|
||||
AccessControl::PeersOnly => {
|
||||
if !is_known_peer {
|
||||
return Ok(ServeResult::Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
AccessControl::PeersOnly => {
|
||||
if !is_known_peer {
|
||||
return Ok(ServeResult::Forbidden);
|
||||
}
|
||||
}
|
||||
AccessControl::Free => {}
|
||||
AccessControl::Free => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1200,7 +1200,9 @@ mod tests {
|
||||
|
||||
mark_installed(tmp.path(), "bitcoin-knots").await;
|
||||
mark_installed(tmp.path(), "lnd").await;
|
||||
assert!(load_installed_apps(tmp.path()).await.contains("bitcoin-knots"));
|
||||
assert!(load_installed_apps(tmp.path())
|
||||
.await
|
||||
.contains("bitcoin-knots"));
|
||||
|
||||
// Uninstall forgets it, or desired-state recovery would recreate the
|
||||
// very app that was just removed.
|
||||
@@ -1224,7 +1226,9 @@ mod tests {
|
||||
|
||||
assert!(load_last_running_names(tmp.path()).await.is_empty());
|
||||
assert!(
|
||||
load_installed_apps(tmp.path()).await.contains("bitcoin-knots"),
|
||||
load_installed_apps(tmp.path())
|
||||
.await
|
||||
.contains("bitcoin-knots"),
|
||||
"installation record must outlive the running snapshot"
|
||||
);
|
||||
}
|
||||
@@ -1268,7 +1272,10 @@ mod tests {
|
||||
backfill_installed_apps(tmp.path(), &["bitcoin-knots".to_string()]).await;
|
||||
|
||||
let installed = load_installed_apps(tmp.path()).await;
|
||||
assert!(installed.contains("lnd"), "a down app was dropped by backfill");
|
||||
assert!(
|
||||
installed.contains("lnd"),
|
||||
"a down app was dropped by backfill"
|
||||
);
|
||||
assert!(installed.contains("bitcoin-knots"));
|
||||
|
||||
// An empty adoption list (podman unreachable, say) must change nothing.
|
||||
|
||||
@@ -336,11 +336,8 @@ async fn main() -> Result<()> {
|
||||
// need it. Additive and evidence-based: only names with a real
|
||||
// adopted container are claimed, and anything the operator
|
||||
// uninstalled is skipped, so it cannot invent an install.
|
||||
crate::crash_recovery::backfill_installed_apps(
|
||||
&config.data_dir,
|
||||
&report.adopted,
|
||||
)
|
||||
.await;
|
||||
crate::crash_recovery::backfill_installed_apps(&config.data_dir, &report.adopted)
|
||||
.await;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(error = %e, "prod orchestrator: adopt_existing failed (non-fatal)");
|
||||
|
||||
@@ -873,10 +873,9 @@ mod tests {
|
||||
/// A real Ed25519 keypair, its did:key, and a manifest signed by it.
|
||||
fn signed_manifest() -> (ed25519_dalek::SigningKey, String, AppManifest) {
|
||||
let key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
|
||||
let did = crate::identity::did_key_from_pubkey_hex(&hex::encode(
|
||||
key.verifying_key().as_bytes(),
|
||||
))
|
||||
.unwrap();
|
||||
let did =
|
||||
crate::identity::did_key_from_pubkey_hex(&hex::encode(key.verifying_key().as_bytes()))
|
||||
.unwrap();
|
||||
let mut manifest = sample_manifest();
|
||||
manifest.author.did = did.clone();
|
||||
sign_manifest(&mut manifest, &key).unwrap();
|
||||
@@ -1054,8 +1053,7 @@ mod tests {
|
||||
manifest.repo_url = String::new();
|
||||
manifest.version = "1".into();
|
||||
manifest.container.readonly_root = false;
|
||||
let (score, _tier) =
|
||||
calculate_trust_score(&manifest, 1, &[], &SignatureStatus::Missing);
|
||||
let (score, _tier) = calculate_trust_score(&manifest, 1, &[], &SignatureStatus::Missing);
|
||||
assert!(score < 50, "Expected low score, got {score}");
|
||||
}
|
||||
|
||||
|
||||
@@ -55,10 +55,7 @@ pub enum TagExtractionError {
|
||||
/// `path` is canonicalized and confined to `media_roots` *before* the file
|
||||
/// is opened. `media_roots` is a parameter, not a constant, so a caller
|
||||
/// cannot bypass the confinement by construction.
|
||||
pub fn extract_tags(
|
||||
path: &Path,
|
||||
media_roots: &[PathBuf],
|
||||
) -> Result<RawTags, TagExtractionError> {
|
||||
pub fn extract_tags(path: &Path, media_roots: &[PathBuf]) -> Result<RawTags, TagExtractionError> {
|
||||
let canonical = path.canonicalize()?;
|
||||
|
||||
let within_roots = media_roots.iter().any(|root| {
|
||||
@@ -91,9 +88,7 @@ pub fn extract_tags(
|
||||
.unwrap_or_else(|| fallback_from_filename(&canonical));
|
||||
let artist = tag.artist().map(|cow| cow.into_owned());
|
||||
let album = tag.album().map(|cow| cow.into_owned());
|
||||
let album_artist = tag
|
||||
.get_string(ItemKey::AlbumArtist)
|
||||
.map(ToOwned::to_owned);
|
||||
let album_artist = tag.get_string(ItemKey::AlbumArtist).map(ToOwned::to_owned);
|
||||
let track = tag.track();
|
||||
let disc = tag.disk();
|
||||
let year = tag.date().map(|timestamp| u32::from(timestamp.year));
|
||||
@@ -208,8 +203,14 @@ mod tests {
|
||||
|
||||
/// FLAC STREAMINFO block content (34 bytes) — the only metadata block
|
||||
/// lofty's duration calculation reads (`flac/properties.rs`).
|
||||
fn flac_streaminfo(sample_rate: u32, channels: u32, bits_per_sample: u32, total_samples: u64) -> Vec<u8> {
|
||||
let mut info: u32 = (sample_rate << 12) | ((channels - 1) << 9) | ((bits_per_sample - 1) << 4);
|
||||
fn flac_streaminfo(
|
||||
sample_rate: u32,
|
||||
channels: u32,
|
||||
bits_per_sample: u32,
|
||||
total_samples: u64,
|
||||
) -> Vec<u8> {
|
||||
let mut info: u32 =
|
||||
(sample_rate << 12) | ((channels - 1) << 9) | ((bits_per_sample - 1) << 4);
|
||||
info |= ((total_samples >> 32) as u32) & 0xF;
|
||||
let total_samples_low = (total_samples & 0xFFFF_FFFF) as u32;
|
||||
|
||||
@@ -270,7 +271,7 @@ mod tests {
|
||||
file.extend_from_slice(&flac_block(0, false, &streaminfo));
|
||||
let vorbis_comments = vorbis_comment_block("test-vendor", comments);
|
||||
file.extend_from_slice(&flac_block(4, true, &vorbis_comments));
|
||||
},
|
||||
}
|
||||
}
|
||||
file
|
||||
}
|
||||
@@ -341,7 +342,8 @@ mod tests {
|
||||
mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // creation_time
|
||||
mdhd_content.extend_from_slice(&[0, 0, 0, 0]); // modification_time
|
||||
mdhd_content.extend_from_slice(&MP4_TIMESCALE.to_be_bytes());
|
||||
mdhd_content.extend_from_slice(&((MP4_TIMESCALE as u64 * MP4_DURATION_SECS) as u32).to_be_bytes());
|
||||
mdhd_content
|
||||
.extend_from_slice(&((MP4_TIMESCALE as u64 * MP4_DURATION_SECS) as u32).to_be_bytes());
|
||||
let mdhd = atom(b"mdhd", &mdhd_content);
|
||||
|
||||
let mdia = atom(b"mdia", &[hdlr, mdhd].concat());
|
||||
@@ -374,7 +376,10 @@ mod tests {
|
||||
let mut segment_table = Vec::new();
|
||||
let mut content = Vec::new();
|
||||
for packet in packets {
|
||||
assert!(packet.len() < 255, "fixture packet too large for a single OGG lacing segment");
|
||||
assert!(
|
||||
packet.len() < 255,
|
||||
"fixture packet too large for a single OGG lacing segment"
|
||||
);
|
||||
segment_table.push(packet.len() as u8);
|
||||
content.extend_from_slice(packet);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ pub fn pubkey_from_did(did: &str) -> Result<[u8; 32]> {
|
||||
let id = did
|
||||
.strip_prefix("did:dht:")
|
||||
.ok_or_else(|| anyhow::anyhow!("Not a did:dht identifier: {}", did))?;
|
||||
let bytes =
|
||||
zbase32::decode_full_bytes_str(id).map_err(|e| anyhow::anyhow!("Invalid z-base-32: {e}"))?;
|
||||
let bytes = zbase32::decode_full_bytes_str(id)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid z-base-32: {e}"))?;
|
||||
if bytes.len() != 32 {
|
||||
anyhow::bail!("Expected 32-byte pubkey, got {} bytes", bytes.len());
|
||||
}
|
||||
|
||||
@@ -174,7 +174,9 @@ mod tests {
|
||||
#[test]
|
||||
fn a_32_byte_key_is_52_chars_and_round_trips() {
|
||||
for seed in 0u8..64 {
|
||||
let key: Vec<u8> = (0u8..32).map(|i| i.wrapping_mul(7).wrapping_add(seed)).collect();
|
||||
let key: Vec<u8> = (0u8..32)
|
||||
.map(|i| i.wrapping_mul(7).wrapping_add(seed))
|
||||
.collect();
|
||||
let encoded = encode_full_bytes(&key);
|
||||
assert_eq!(encoded.len(), 52, "256 bits must encode to 52 characters");
|
||||
assert_eq!(decode_full_bytes_str(&encoded).unwrap(), key);
|
||||
@@ -184,7 +186,9 @@ mod tests {
|
||||
#[test]
|
||||
fn round_trips_every_length_up_to_a_block() {
|
||||
for len in 0..40usize {
|
||||
let data: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(31) ^ 0x5a).collect();
|
||||
let data: Vec<u8> = (0..len)
|
||||
.map(|i| (i as u8).wrapping_mul(31) ^ 0x5a)
|
||||
.collect();
|
||||
let encoded = encode_full_bytes(&data);
|
||||
// decode_full_bytes only recovers whole bytes, and encoding N bytes
|
||||
// produces ceil(8N/5) chars which always carry at least 8N bits.
|
||||
|
||||
@@ -979,7 +979,12 @@ mod tests {
|
||||
|
||||
// BIP-32 m/84'/0'/0'
|
||||
assert_eq!(
|
||||
hex::encode(derive_bitcoin_xprv(&seed).unwrap().private_key.secret_bytes()),
|
||||
hex::encode(
|
||||
derive_bitcoin_xprv(&seed)
|
||||
.unwrap()
|
||||
.private_key
|
||||
.secret_bytes()
|
||||
),
|
||||
"57558e8c90c2e72f0c121d0fb8844bbbe7a872f0065d21b218a990450b9f93be",
|
||||
"Bitcoin BIP-84 account key"
|
||||
);
|
||||
|
||||
@@ -159,7 +159,9 @@ mod tests {
|
||||
async fn a_corrupt_file_fails_closed() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join(FILE_PATH);
|
||||
tokio::fs::create_dir_all(path.parent().unwrap()).await.unwrap();
|
||||
tokio::fs::create_dir_all(path.parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(&path, b"{ not json").await.unwrap();
|
||||
// The dangerous failure would be defaulting to "all granted".
|
||||
assert!(load(dir.path()).await.granted.is_empty());
|
||||
|
||||
Reference in New Issue
Block a user