OXYGENHUB
◆ main spectra / tests / allow.rs
3.0 KiB · Rust · last changed The API reports each agent's extra channels 1 day ago source blame history raw
1
//! `spectra agent allow` (spec §8.4): the owner lets one agent post in
2
//! another project's channel, and can take it back; an agent cannot grant
3
//! itself anything.
4
5
mod common;
6
7
use common::{run, setup, text, Serving};
8
use serde_json::{json, Value};
9
use spectra_core::keys::KeyPair;
10
use spectra_mcp::client::{rpc_result, Client};
11
12
async fn say(c: &Client, channel: &str) -> Value {
13
    let req = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "say", "arguments": { "body": format!("hello {channel}"), "channel": channel } } });
14
    rpc_result(&c.mcp("2025-11-25", &req).await.unwrap()).unwrap()["result"].clone()
15
}
16
17
#[tokio::test]
18
async fn the_owner_opens_one_channel_to_one_agent() {
19
    let dir = setup();
20
    let d = dir.path();
21
    assert!(run(d, &["project", "add", "bubblebobble"]).status.success());
22
    let home = d.join("agents/claude-mg");
23
    let o =
24
        run(d, &["agent", "add", "claude-mg", "--kind", "claude-code", "--project", "oxygen", "--home", home.to_str().unwrap(), "--yes"]);
25
    assert!(o.status.success(), "{}", text(&o));
26
    let srv = Serving::start(d);
27
    let c = Client::new(&srv.socket, Some(KeyPair::load_home(&home).unwrap()));
28
29
    let r = say(&c, "bubblebobble").await;
30
    assert_eq!(r["isError"], true);
31
    assert!(r["content"][0]["text"].as_str().unwrap().contains("spectra agent allow NAME --channel bubblebobble"), "{r}");
32
33
    let o = run(d, &["agent", "allow", "claude-mg", "--channel", "#bubblebobble"]);
34
    assert!(o.status.success(), "{}", text(&o));
35
    let listed: Value = serde_json::from_slice(&run(d, &["agent", "list", "--json"]).stdout).unwrap();
36
    let mg = listed.as_array().unwrap().iter().find(|a| a["name"] == "claude-mg").unwrap();
37
    assert_eq!(mg["channels"], json!(["bubblebobble"]), "the API reports the grant: {mg}");
38
    assert_eq!(say(&c, "bubblebobble").await["isError"], false, "allowed");
39
    assert_eq!(say(&c, "oxygen").await["isError"], false, "its own channel still");
40
41
    assert!(run(d, &["agent", "allow", "claude-mg", "--channel", "bubblebobble", "--remove"]).status.success());
42
    assert_eq!(say(&c, "bubblebobble").await["isError"], true, "taken back");
43
    assert!(!run(d, &["agent", "allow", "claude-mg", "--channel", "Not A Channel"]).status.success());
44
45
    // An agent cannot grant itself: its key signing agent.scoped fails verify.
46
    let me = KeyPair::load_home(&home).unwrap();
47
    let forged = spectra_core::principals::signed_event(
48
        &me,
49
        "agent:claude-mg",
50
        "agent.scoped",
51
        json!({ "name": "claude-mg", "scope": "projects:all" }),
52
    );
53
    let w = spectra_core::daemon::WireEvent::from_new(&forged);
54
    let r = c.send("POST", "/api/v1/ledger/events", &[], serde_json::to_vec(&vec![w]).unwrap()).await.unwrap();
55
    assert_eq!(r.status, 403, "{}", String::from_utf8_lossy(&r.body));
56
    assert_eq!(say(&c, "bubblebobble").await["isError"], true);
57
    srv.stop();
58
59
    for args in [["admin", "verify"], ["admin", "check"], ["admin", "rebuild"]] {
60
        let o = run(d, &args);
61
        assert!(o.status.success(), "{args:?}: {}", text(&o));
62
    }
63
}