tambo 0.6.0

Install and prove the whole AI knowledge-tool kit
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
//! Register Quipu's MCP endpoint with Claude Code without putting the secret in
//! any configuration file.
//!
//! A direct `{type: http, url}` entry sends no bearer, so every MCP write is
//! refused with 401 while reads keep working (aegis-nvw6ye). Claude Code's
//! `headersHelper` runs a command at connect time and uses the JSON it prints
//! as request headers. `apply` installs that command and registers the entry;
//! `verify` proves an authenticated MCP write reaches Quipu's parser, with an
//! unauthenticated positive control.
use std::{
    env,
    ffi::OsString,
    fs,
    io::Write,
    os::unix::fs::PermissionsExt,
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

use anyhow::{bail, Context, Result};
use serde_json::{json, Value};

pub const SERVER_NAME: &str = "quipu";
const HELPER_NAME: &str = "quipu-mcp-headers";

/// Mirrors `quipu_auth::resolve`: QUIPU_AUTH_TOKEN, then QUIPU_AUTH_TOKEN_FILE,
/// then the default file. The token is read at connect time and is never
/// written into this script or into the Claude configuration. `@DEFAULT@` is
/// replaced by `helper_script` with the plan's token file (a path, not the
/// secret) or ~/.config/quipu/token.
const HELPER_TEMPLATE: &str = r#"#!/bin/sh
# Installed by caboodle. Prints Quipu's MCP Authorization header at connect time.
# Token precedence matches caboodle: QUIPU_AUTH_TOKEN, then QUIPU_AUTH_TOKEN_FILE,
# then the default file below. The secret is never stored in this file.
set -u
token=${QUIPU_AUTH_TOKEN:-}
if [ -z "$token" ]; then
    file=${QUIPU_AUTH_TOKEN_FILE:-}
    [ -n "$file" ] || file=@DEFAULT@
    if [ ! -e "$file" ]; then
        echo "quipu-mcp-headers: no Quipu token at $file: set QUIPU_AUTH_TOKEN_FILE or install the token there" >&2
        exit 1
    fi
    if ! token=$(cat "$file"); then
        echo "quipu-mcp-headers: cannot read the Quipu token file" >&2
        exit 1
    fi
    token=$(printf '%s' "$token" | tr -d ' \t\r\n')
fi
if [ -z "$token" ]; then
    echo "quipu-mcp-headers: the Quipu token is empty" >&2
    exit 1
fi
case $token in
    *[!A-Za-z0-9._~+/=-]*)
        echo "quipu-mcp-headers: the Quipu token has characters that cannot be sent as a header" >&2
        exit 1
        ;;
esac
printf '{"Authorization":"Bearer %s"}\n' "$token"
"#;

/// The helper for a plan. `token_file` replaces ~/.config/quipu/token as the
/// default so a host whose convention differs (for example a token kept under
/// another directory) works in Claude Code's own environment, which need not
/// carry QUIPU_AUTH_TOKEN_FILE.
pub fn helper_script(token_file: Option<&str>) -> Result<String> {
    let default = match token_file {
        None => "\"${HOME:-}/.config/quipu/token\"".to_owned(),
        Some(path) => {
            if !path.starts_with('/') || path.contains(['\'', '\n', '\r']) {
                bail!(
                    "the Quipu token file must be an absolute path without quotes or line breaks"
                );
            }
            format!("'{path}'")
        }
    };
    Ok(HELPER_TEMPLATE.replace("@DEFAULT@", &default))
}

pub fn helper_path() -> Result<PathBuf> {
    let home = env::var_os("HOME").context("HOME is not set; cannot place the Quipu MCP helper")?;
    Ok(Path::new(&home).join(".local/bin").join(HELPER_NAME))
}

/// The `/mcp` endpoint for a plan's Quipu URL, which may already name it.
pub fn endpoint(url: &str) -> String {
    let base = url.trim_end_matches('/');
    if base.ends_with("/mcp") {
        base.to_owned()
    } else {
        format!("{base}/mcp")
    }
}

pub fn desired_entry(url: &str, helper: &Path) -> Value {
    json!({"type": "http", "url": endpoint(url), "headersHelper": helper.to_string_lossy()})
}

fn claude_config_path() -> Result<PathBuf> {
    if let Some(dir) = env::var_os("CLAUDE_CONFIG_DIR").filter(|d| !d.is_empty()) {
        return Ok(Path::new(&dir).join(".claude.json"));
    }
    let home = env::var_os("HOME").context("HOME is not set; cannot read Claude configuration")?;
    Ok(Path::new(&home).join(".claude.json"))
}

fn read_claude_config() -> Result<Value> {
    let path = claude_config_path()?;
    match fs::read_to_string(&path) {
        Ok(body) => serde_json::from_str(&body)
            .with_context(|| format!("parse Claude configuration {}", path.display())),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(json!({})),
        Err(error) => Err(error).with_context(|| format!("read {}", path.display())),
    }
}

/// Every `quipu` entry Claude Code could use: user scope plus each project's
/// local scope. A local entry shadows the user entry in its directory.
pub fn quipu_entries(config: &Value) -> Vec<(String, Value)> {
    let mut found = Vec::new();
    if let Some(entry) = config["mcpServers"].get(SERVER_NAME) {
        found.push(("user scope".to_owned(), entry.clone()));
    }
    if let Some(projects) = config["projects"].as_object() {
        for (dir, project) in projects {
            if let Some(entry) = project["mcpServers"].get(SERVER_NAME) {
                found.push((format!("local scope for {dir}"), entry.clone()));
            }
        }
    }
    found
}

fn write_helper(path: &Path, script: &str) -> Result<bool> {
    if fs::read_to_string(path).is_ok_and(|body| body == script)
        && fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o777 == 0o755)
    {
        return Ok(false);
    }
    let dir = path
        .parent()
        .context("helper path has no parent directory")?;
    fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
    let mut temp = tempfile::NamedTempFile::new_in(dir).context("create helper temp file")?;
    temp.write_all(script.as_bytes())
        .context("write Quipu MCP helper")?;
    fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))
        .context("make Quipu MCP helper executable")?;
    temp.persist(path)
        .with_context(|| format!("install {}", path.display()))?;
    Ok(true)
}

/// An MCP entry safe to print: header and env VALUES are replaced and any
/// userinfo is dropped from the URL. A hand-made entry can carry a static
/// bearer, and error text reaches terminals and logs.
pub fn redact(entry: &Value) -> Value {
    let mut entry = entry.clone();
    for key in ["headers", "env"] {
        if let Some(map) = entry.get_mut(key).and_then(Value::as_object_mut) {
            for value in map.values_mut() {
                *value = Value::String("<redacted>".to_owned());
            }
        }
    }
    if let Some(url) = entry.get_mut("url") {
        if let Some(text) = url.as_str() {
            *url = Value::String(redact_url(text));
        }
    }
    entry
}

/// Drop URL userinfo and any query string or fragment, where credentials
/// (`?token=`) can also live. Scheme, host and path stay readable.
fn redact_url(text: &str) -> String {
    let (base, hidden_tail) = match text.find(['?', '#']) {
        Some(i) => (&text[..i], true),
        None => (text, false),
    };
    let base = match base.split_once("://") {
        Some((scheme, rest)) => {
            let (authority, path) = rest
                .split_once('/')
                .map_or((rest, None), |(a, p)| (a, Some(p)));
            let authority = authority
                .rsplit_once('@')
                .map_or(authority.to_owned(), |(_, host)| {
                    format!("<redacted>@{host}")
                });
            match path {
                Some(path) => format!("{scheme}://{authority}/{path}"),
                None => format!("{scheme}://{authority}"),
            }
        }
        None => base.to_owned(),
    };
    if hidden_tail {
        format!("{base}?<redacted>")
    } else {
        base
    }
}

/// Redact any argument that is a JSON object (the `add-json` entry).
fn printable(arg: &OsString) -> String {
    let text = arg.to_string_lossy();
    match serde_json::from_str::<Value>(&text) {
        Ok(value) if value.is_object() => redact(&value).to_string(),
        _ => text.into_owned(),
    }
}

fn claude(args: &[OsString]) -> Result<()> {
    let out = Command::new("claude")
        .args(args)
        .stdin(Stdio::null())
        .output()
        .context("run the `claude` CLI")?;
    if !out.status.success() {
        bail!(
            "`claude {}` failed ({}): {}",
            args.iter().map(printable).collect::<Vec<_>>().join(" "),
            out.status,
            String::from_utf8_lossy(&out.stderr).trim()
        );
    }
    Ok(())
}

/// Install the helper and register the user-scope entry. Idempotent; replaces
/// a user-scope entry that differs (for example one without a headersHelper).
pub fn provision(url: &str, token_file: Option<&str>) -> Result<String> {
    let helper = helper_path()?;
    let wrote = write_helper(&helper, &helper_script(token_file)?)?;
    let desired = desired_entry(url, &helper);
    let existing = read_claude_config()?["mcpServers"]
        .get(SERVER_NAME)
        .cloned();
    let mut notes = Vec::new();
    if wrote {
        notes.push(format!("helper installed at {}", helper.display()));
    }
    match existing {
        Some(entry) if entry == desired => {}
        Some(entry) => {
            notes.push(if entry.get("headersHelper").is_none() {
                "replaced a user-scope quipu entry that had no headersHelper (MCP writes were unauthenticated)".to_owned()
            } else {
                "replaced a user-scope quipu entry that differed from the plan".to_owned()
            });
            // `remove` then `add-json` is not atomic. If the add fails, put the
            // previous entry back rather than leave the user with none.
            claude(&["mcp", "remove", SERVER_NAME, "-s", "user"].map(OsString::from))?;
            if let Err(error) = add(&desired) {
                return Err(match add(&entry) {
                    Ok(()) => error.context("registering the planned quipu entry failed; the previous entry was restored"),
                    Err(restore) => error.context(format!(
                        "registering the planned quipu entry failed AND restoring the previous entry failed ({restore:#}); re-add it by hand; it was (secret values redacted) {}", redact(&entry)
                    )),
                });
            }
        }
        None => {
            notes.push("registered the user-scope quipu entry".to_owned());
            add(&desired)?;
        }
    }
    let registered = read_claude_config()?["mcpServers"]
        .get(SERVER_NAME)
        .cloned();
    if registered.as_ref() != Some(&desired) {
        bail!("`claude mcp add-json` returned success but the user-scope quipu entry does not read back as planned");
    }
    for (scope, entry) in quipu_entries(&read_claude_config()?) {
        if scope != "user scope" && entry.get("headersHelper").is_none() {
            notes.push(format!(
                "WARNING: a quipu entry in {scope} has no headersHelper and shadows the user entry there; remove it with `claude mcp remove quipu -s local` in that directory"
            ));
        }
    }
    Ok(if notes.is_empty() {
        "already provisioned".to_owned()
    } else {
        notes.join("; ")
    })
}

fn add(entry: &Value) -> Result<()> {
    claude(&[
        OsString::from("mcp"),
        OsString::from("add-json"),
        OsString::from(SERVER_NAME),
        OsString::from(entry.to_string()),
        OsString::from("-s"),
        OsString::from("user"),
    ])
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProbeAnswer {
    /// The call reached Quipu's Turtle parser: the bearer was accepted.
    Parsed,
    /// Quipu refused the call for a missing or invalid bearer.
    Unauthorized,
    /// Anything else, including no answer at all.
    Other,
}

/// Classify a `tools/call` answer. Quipu wraps REST errors as a tool result,
/// and the transport is SSE, so the HTTP status says nothing: read the body.
///
/// This is coupled to two strings in Quipu's REST errors: the auth refusal's
/// `reason: missing_or_invalid_bearer_token`, and `/knot`'s `RDF parse error`
/// prefix. If Quipu rewords either, answers fall to `Other`, and verify then
/// reports UNKNOWN rather than a false PASS. The test fixtures are the shapes
/// measured against a served Quipu 0.9.1.
pub fn classify(body: &str) -> ProbeAnswer {
    let payloads: Vec<&str> = body
        .lines()
        .filter_map(|line| line.strip_prefix("data:").map(str::trim))
        .collect();
    let candidates = if payloads.is_empty() {
        vec![body.trim()]
    } else {
        payloads
    };
    for payload in candidates {
        let Ok(message) = serde_json::from_str::<Value>(payload) else {
            continue;
        };
        let Some(text) = message["result"]["content"][0]["text"].as_str() else {
            continue;
        };
        let inner: Value = serde_json::from_str(text).unwrap_or(Value::Null);
        if inner["reason"] == "missing_or_invalid_bearer_token" {
            return ProbeAnswer::Unauthorized;
        }
        if inner["error"]
            .as_str()
            .is_some_and(|e| e.starts_with("RDF parse error"))
        {
            return ProbeAnswer::Parsed;
        }
    }
    ProbeAnswer::Other
}

/// Invalid Turtle: Quipu checks the bearer first, then fails to parse, so an
/// accepted credential writes nothing.
const PROBE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"quipu_knot","arguments":{"turtle":"caboodle auth probe: not turtle <<<"}}}"#;

fn post(endpoint: &str, token: Option<&str>) -> Result<String> {
    let body_file = tempfile::NamedTempFile::new().context("create MCP probe output file")?;
    let auth = match token {
        Some(token) => {
            let file = tempfile::NamedTempFile::new().context("create MCP probe auth config")?;
            fs::write(file.path(), crate::adapter::curl_auth_header_line(token)?)
                .context("write MCP probe auth config")?;
            Some(file)
        }
        None => None,
    };
    let mut args: Vec<OsString> = [
        "--silent",
        "--max-time",
        "20",
        "--request",
        "POST",
        "--header",
        "Content-Type: application/json",
        "--header",
        "Accept: application/json, text/event-stream",
        "--header",
        "X-Quipu-Client: caboodle-verify",
        "--data",
        PROBE,
        "--output",
    ]
    .iter()
    .map(OsString::from)
    .collect();
    args.push(body_file.path().as_os_str().to_owned());
    if let Some(file) = &auth {
        args.push(OsString::from("--config"));
        args.push(file.path().as_os_str().to_owned());
    }
    args.push(OsString::from(endpoint));
    let status = Command::new("curl")
        .args(&args)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .context("run curl for the Quipu MCP probe")?;
    if !status.success() {
        bail!("curl could not reach {endpoint} ({status})");
    }
    Ok(fs::read_to_string(body_file.path()).unwrap_or_default())
}

/// Run the registered helper exactly as Claude Code would and return the
/// bearer it supplies.
fn helper_token(helper: &str, clean_env: bool) -> Result<String> {
    let mut command = Command::new(helper);
    if clean_env {
        command
            .env_remove("QUIPU_AUTH_TOKEN")
            .env_remove("QUIPU_AUTH_TOKEN_FILE");
    }
    let out = command
        .stdin(Stdio::null())
        .output()
        .with_context(|| format!("run headersHelper {helper}"))?;
    if !out.status.success() {
        bail!(
            "headersHelper {helper} failed: {}",
            String::from_utf8_lossy(&out.stderr).trim()
        );
    }
    let headers: Value = serde_json::from_slice(&out.stdout)
        .with_context(|| format!("headersHelper {helper} did not print a JSON object"))?;
    let value = headers["Authorization"]
        .as_str()
        .context("headersHelper printed no Authorization header")?;
    let token = value
        .strip_prefix("Bearer ")
        .context("headersHelper Authorization is not a Bearer credential")?;
    Ok(token.to_owned())
}

/// Prove the registered entry authenticates MCP writes. Every outcome other
/// than PASS is an error that names FAIL or UNKNOWN; a PASS may carry notes.
pub fn verify(url: &str) -> Result<Vec<String>> {
    let helper = helper_path()?;
    let desired = desired_entry(url, &helper);
    let config = read_claude_config()?;
    let entries = quipu_entries(&config);
    match entries.iter().find(|(scope, _)| scope == "user scope") {
        None => bail!("FAIL: no user-scope quipu MCP entry is registered; run `tambo apply`"),
        Some((_, entry)) if entry.get("headersHelper").is_none() => bail!(
            "FAIL: the user-scope quipu MCP entry has no headersHelper, so MCP writes carry no bearer; run `tambo apply`"
        ),
        Some((_, entry)) if entry != &desired => bail!(
            "FAIL: the user-scope quipu MCP entry differs from the plan (expected {desired}, found {}); run `tambo apply`",
            redact(entry)
        ),
        Some(_) => {}
    }
    for (scope, entry) in &entries {
        if entry.get("headersHelper").is_none() {
            bail!("FAIL: a quipu MCP entry in {scope} has no headersHelper and shadows the user entry there");
        }
    }
    let target = endpoint(url);
    // Positive control first: without a credential this server must refuse,
    // or an accepted authenticated call proves nothing about the helper.
    match classify(&post(&target, None)?) {
        ProbeAnswer::Unauthorized => {}
        ProbeAnswer::Parsed => bail!(
            "UNKNOWN: {target} accepted an MCP write with NO credential, so it cannot show whether the helper's bearer works"
        ),
        ProbeAnswer::Other => bail!(
            "UNKNOWN: {target} gave no recognisable answer to the unauthenticated control; the MCP write path is untested"
        ),
    }
    let helper_str = helper.to_string_lossy();
    let token = helper_token(&helper_str, false).map_err(|e| anyhow::anyhow!("FAIL: {e:#}"))?;
    // verify runs in this shell; Claude Code runs the helper in ITS environment
    // at connect time, which may not carry the token variables. Say so when the
    // helper only works because of them.
    let mut notes = Vec::new();
    let from_env = ["QUIPU_AUTH_TOKEN", "QUIPU_AUTH_TOKEN_FILE"]
        .into_iter()
        .filter(|key| env::var_os(key).is_some_and(|v| !v.is_empty()))
        .collect::<Vec<_>>();
    if !from_env.is_empty() && helper_token(&helper_str, true).is_err() {
        notes.push(format!(
            "WARNING: the helper found a token only through {} in this shell. Claude Code must see the same variable when it connects, or re-plan with --quipu-mcp-token-file",
            from_env.join(" and ")
        ));
    }
    match classify(&post(&target, Some(&token))?) {
        ProbeAnswer::Parsed => Ok(notes),
        ProbeAnswer::Unauthorized => bail!(
            "FAIL: {target} refused the headersHelper's bearer (missing_or_invalid_bearer_token); check the Quipu token"
        ),
        ProbeAnswer::Other => bail!(
            "UNKNOWN: the authenticated MCP probe to {target} gave no recognisable answer"
        ),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn run_helper(env: &[(&str, &str)], home: &Path) -> (bool, String) {
        run_helper_with(env, home, None)
    }

    fn run_helper_with(env: &[(&str, &str)], home: &Path, default: Option<&str>) -> (bool, String) {
        let dir = tempfile::tempdir().unwrap();
        let helper = dir.path().join(HELPER_NAME);
        write_helper(&helper, &helper_script(default).unwrap()).unwrap();
        let mut command = Command::new(&helper);
        command
            .env_clear()
            .env("PATH", "/usr/bin:/bin")
            .env("HOME", home);
        for (key, value) in env {
            command.env(key, value);
        }
        // Parallel tests write and exec scripts; a sibling's fork can briefly
        // hold a just-written file open, and exec then fails with ETXTBSY.
        let out = (0..50)
            .find_map(|_| match command.output() {
                Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
                    std::thread::sleep(std::time::Duration::from_millis(20));
                    None
                }
                other => Some(other.unwrap()),
            })
            .expect("helper stayed busy");
        (
            out.status.success(),
            String::from_utf8_lossy(&out.stdout).into_owned(),
        )
    }

    #[test]
    fn helper_precedence_matches_quipu_auth() {
        let home = tempfile::tempdir().unwrap();
        let default = home.path().join(".config/quipu/token");
        fs::create_dir_all(default.parent().unwrap()).unwrap();
        fs::write(&default, "default\n").unwrap();
        let explicit = home.path().join("explicit");
        let bearer = |t: &str| format!("{{\"Authorization\":\"Bearer {t}\"}}\n");
        // 1. the default file when nothing is set
        assert_eq!(run_helper(&[], home.path()), (true, bearer("default")));
        // 2. an explicit file wins over the default, and a missing explicit file
        //    does NOT fall back to the default (quipu_auth returns None there)
        let file = explicit.to_str().unwrap();
        assert!(!run_helper(&[("QUIPU_AUTH_TOKEN_FILE", file)], home.path()).0);
        fs::write(&explicit, "first\n").unwrap();
        assert_eq!(
            run_helper(&[("QUIPU_AUTH_TOKEN_FILE", file)], home.path()),
            (true, bearer("first"))
        );
        // an empty QUIPU_AUTH_TOKEN falls through to the file
        assert_eq!(
            run_helper(
                &[("QUIPU_AUTH_TOKEN", ""), ("QUIPU_AUTH_TOKEN_FILE", file)],
                home.path()
            ),
            (true, bearer("first"))
        );
        // 3. the environment value wins over both files
        assert_eq!(
            run_helper(
                &[
                    ("QUIPU_AUTH_TOKEN", "override"),
                    ("QUIPU_AUTH_TOKEN_FILE", file)
                ],
                home.path()
            ),
            (true, bearer("override"))
        );
        // empty and unsendable tokens are refused, never printed
        fs::write(&explicit, "\n").unwrap();
        assert!(!run_helper(&[("QUIPU_AUTH_TOKEN_FILE", file)], home.path()).0);
        fs::write(&explicit, "a\"b").unwrap();
        assert!(!run_helper(&[("QUIPU_AUTH_TOKEN_FILE", file)], home.path()).0);
    }

    #[test]
    fn helper_holds_no_secret() {
        let script = helper_script(None).unwrap();
        assert!(!script.contains("Bearer ey"));
        assert!(script.contains("QUIPU_AUTH_TOKEN_FILE"));
    }

    #[test]
    fn planned_token_file_replaces_the_default_but_not_the_environment() {
        let home = tempfile::tempdir().unwrap();
        let planned = home.path().join("host-convention/quipu_token");
        fs::create_dir_all(planned.parent().unwrap()).unwrap();
        fs::write(&planned, "planned\n").unwrap();
        let path = planned.to_str();
        assert_eq!(
            run_helper_with(&[], home.path(), path),
            (true, "{\"Authorization\":\"Bearer planned\"}\n".to_owned())
        );
        assert_eq!(
            run_helper_with(&[("QUIPU_AUTH_TOKEN", "override")], home.path(), path).1,
            "{\"Authorization\":\"Bearer override\"}\n"
        );
        for bad in ["relative/token", "/tmp/it's", "/tmp/a\nb"] {
            assert!(helper_script(Some(bad)).is_err(), "{bad}");
        }
    }

    #[test]
    fn classifies_the_measured_quipu_answers() {
        let refused = r#"data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"endpoint\":\"/knot\",\"error\":\"unauthorized: /knot is a WRITE endpoint\",\"reason\":\"missing_or_invalid_bearer_token\"}"}],"isError":true}}"#;
        let parsed = r#"data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"error\":\"RDF parse error: Parser error at line 1\"}"}],"isError":true}}"#;
        assert_eq!(classify(refused), ProbeAnswer::Unauthorized);
        assert_eq!(classify(parsed), ProbeAnswer::Parsed);
        // plain JSON transport, and noise
        assert_eq!(
            classify(parsed.strip_prefix("data: ").unwrap()),
            ProbeAnswer::Parsed
        );
        assert_eq!(classify(""), ProbeAnswer::Other);
        assert_eq!(classify("<html>502</html>"), ProbeAnswer::Other);
    }

    #[test]
    fn endpoint_and_entry_shape() {
        assert_eq!(endpoint("http://quipu.example"), "http://quipu.example/mcp");
        assert_eq!(
            endpoint("http://quipu.example/mcp/"),
            "http://quipu.example/mcp"
        );
        let entry = desired_entry(
            "http://quipu.example",
            Path::new("/h/.local/bin/quipu-mcp-headers"),
        );
        assert_eq!(entry["type"], "http");
        assert_eq!(entry["headersHelper"], "/h/.local/bin/quipu-mcp-headers");
    }

    #[test]
    fn redact_hides_static_bearers_and_url_credentials() {
        let entry = json!({
            "type": "http",
            "url": "https://user:hunter2@quipu.example/mcp",
            "headers": {"Authorization": "Bearer s3cr3t-token"},
            "env": {"QUIPU_AUTH_TOKEN": "s3cr3t-token"}
        });
        let shown = redact(&entry).to_string();
        let query = redact(&json!({"url": "https://quipu.example/mcp?token=q-s3cr3t#frag"}));
        assert_eq!(query["url"], "https://quipu.example/mcp?<redacted>");
        assert_eq!(redact_url("http://quipu.example"), "http://quipu.example");
        assert_eq!(
            redact_url("http://u:p@quipu.example"),
            "http://<redacted>@quipu.example"
        );
        for secret in ["s3cr3t-token", "hunter2", "user:"] {
            assert!(!shown.contains(secret), "{shown}");
        }
        assert!(shown.contains("quipu.example/mcp") && shown.contains("Authorization"));
        // the add-json argument is redacted in claude()'s own error text
        let arg = OsString::from(entry.to_string());
        assert!(!printable(&arg).contains("s3cr3t-token"));
        assert_eq!(printable(&OsString::from("add-json")), "add-json");
        // an entry with nothing secret is unchanged
        let plain = desired_entry("http://quipu.example", Path::new("/h/helper"));
        assert_eq!(redact(&plain), plain);
    }

    #[test]
    fn finds_shadowing_local_entries() {
        let config = json!({
            "mcpServers": {"quipu": {"type": "http", "url": "u", "headersHelper": "h"}},
            "projects": {"/w": {"mcpServers": {"quipu": {"type": "http", "url": "u"}}}}
        });
        let entries = quipu_entries(&config);
        assert_eq!(entries.len(), 2);
        assert!(entries[1].0.contains("/w") && entries[1].1.get("headersHelper").is_none());
    }
}