571572f5 Andy McCall 2 days ago ↶
1
2
3
4
037c45d8 Andy McCall 2 days ago ↶
5
mod common ;
6
7
use common:: { add_agent, run, setup, text, Serving} ;
571572f5 Andy McCall 2 days ago ↶
8
use serde_json:: json;
9
use spectra_core:: keys:: KeyPair;
10
use spectra_mcp:: client:: { rpc_result, Client} ;
11
12
fn call ( id : u64 , name : & str , args : serde_json:: Value) -> serde_json:: Value {
13
json! ( { " jsonrpc" : " 2.0" , " id" : id, " method" : " tools/call" , " params" : { " name" : name, " arguments" : args,
14
" _meta" : { " io.modelcontextprotocol/protocolVersion" : " 2026-07-28" } } } )
15
}
16
17
# [ tokio ::test ]
18
async fn an_agent_discovers_lists_and_calls_tools_over_the_socket ( ) {
19
let dir = setup ( ) ;
20
let d = dir.path ( ) ;
21
let agent = add_agent ( d, " claude-oxygen" , & [ ] ) ;
22
let srv = Serving:: start( d) ;
23
let c = Client:: new( & srv.socket, Some ( agent.clone ( ) ) ) ;
24
25
let r = c.mcp ( " 2026-07-28" , & json! ( { " jsonrpc" : " 2.0" , " id" : 1 , " method" : " server/discover" , " params" : { } } ) ) .await.unwrap ( ) ;
26
assert_eq! ( r.status, 200 , " {}" , String :: from_utf8_lossy( & r.body) ) ;
27
let v = r.json ( ) .unwrap ( ) ;
28
assert_eq! ( v[ " result" ] [ " serverInfo" ] [ " name" ] , " spectra" ) ;
29
assert_eq! ( v[ " result" ] [ " resultType" ] , " complete" ) ;
30
assert! ( v[ " result" ] [ " supportedVersions" ] .as_array ( ) .unwrap ( ) .contains ( & json! ( " 2025-11-25" ) ) ) ;
31
32
let v = c.mcp ( " 2026-07-28" , & json! ( { " jsonrpc" : " 2.0" , " id" : 2 , " method" : " tools/list" , " params" : { } } ) ) .await.unwrap ( ) .json ( ) .unwrap ( ) ;
33
assert! ( v[ " result" ] [ " tools" ] .as_array ( ) .unwrap ( ) .iter ( ) .any ( | t | t[ " name" ] = = " agents" ) ) ;
34
assert_eq! ( v[ " result" ] [ " ttlMs" ] , 3_600_000 ) ;
35
assert! ( v[ " result" ] [ " _meta" ] [ " spectra/session" ] .is_string ( ) , " {v}" ) ;
36
37
let r = c.mcp ( " 2026-07-28" , & call ( 3 , " agents" , json! ( { } ) ) ) .await.unwrap ( ) ;
38
let v = rpc_result ( & r) .unwrap ( ) ;
39
assert_eq! ( v[ " result" ] [ " isError" ] , false , " {v}" ) ;
40
assert_eq! ( v[ " result" ] [ " structuredContent" ] [ " agents" ] [ 0 ] [ " owner" ] , " Andy McCall" ) ;
41
assert! ( v[ " result" ] [ " content" ] [ 0 ] [ " text" ] .as_str ( ) .unwrap ( ) .contains ( " claude-oxygen · Andy McCall's agent" ) ) ;
42
43
44
let v = c.mcp ( " 2025-11-25" , & json! ( { " jsonrpc" : " 2.0" , " id" : 4 , " method" : " initialize" , " params" : { " protocolVersion" : " 2025-11-25" , " capabilities" : { } , " clientInfo" : { " name" : " t" , " version" : " 1" } } } ) ) .await.unwrap ( ) .json ( ) .unwrap ( ) ;
45
assert_eq! ( v[ " result" ] [ " protocolVersion" ] , " 2025-11-25" ) ;
46
assert! ( v[ " result" ] [ " instructions" ] .as_str ( ) .unwrap ( ) .contains ( " Call `brief` first" ) ) ;
47
assert_eq! ( c.mcp ( " 2025-11-25" , & json! ( { " jsonrpc" : " 2.0" , " method" : " notifications/initialized" } ) ) .await.unwrap ( ) .status, 202 ) ;
48
srv.stop ( ) ;
49
}
50
51
# [ tokio ::test ]
52
async fn every_refusal_the_spec_promises ( ) {
53
let dir = setup ( ) ;
54
let d = dir.path ( ) ;
55
let agent = add_agent ( d, " claude-oxygen" , & [ ] ) ;
56
let uid = unsafe { libc:: getuid( ) } ;
57
let other_uid = ( uid + 1 ) .to_string ( ) ;
58
let bound = add_agent ( d, " bound" , & [ " --uid" , & other_uid] ) ;
59
let srv = Serving:: start( d) ;
60
let list = json! ( { " jsonrpc" : " 2.0" , " id" : 1 , " method" : " tools/list" , " params" : { " _meta" : { " io.modelcontextprotocol/protocolVersion" : " 2026-07-28" } } } ) ;
61
62
let unsigned = Client:: new( & srv.socket, None ) .mcp ( " 2026-07-28" , & list) .await.unwrap ( ) ;
63
assert_eq! ( unsigned.status, 401 ) ;
64
let stranger = Client:: new( & srv.socket, Some ( KeyPair:: generate( " stranger" , " " ) ) ) .mcp ( " 2026-07-28" , & list) .await.unwrap ( ) ;
65
assert_eq! ( stranger.status, 401 ) ;
66
assert! ( String :: from_utf8_lossy( & stranger.body) .contains ( " unknown key" ) ) ;
67
68
let c = Client:: new( & srv.socket, Some ( agent) ) ;
69
let r = c
70
.send (
71
" POST" ,
72
" /mcp" ,
73
& [ ( " mcp-protocol-version" , " 2026-07-28" .into ( ) ) , ( " mcp-method" , " tools/call" .into ( ) ) ] ,
74
serde_json:: to_vec( & list) .unwrap ( ) ,
75
)
76
.await
77
.unwrap ( ) ;
78
assert_eq! ( ( r.status, r.json ( ) .unwrap ( ) [ " error" ] [ " code" ] .as_i64 ( ) ) , ( 400 , Some ( - 32020 ) ) , " the Mcp-Method header must match the body" ) ;
79
let r = c.send ( " POST" , " /mcp" , & [ ( " mcp-protocol-version" , " 1999-01-01" .into ( ) ) ] , serde_json:: to_vec( & list) .unwrap ( ) ) .await.unwrap ( ) ;
80
assert_eq! ( ( r.status, r.json ( ) .unwrap ( ) [ " error" ] [ " code" ] .as_i64 ( ) ) , ( 400 , Some ( - 32022 ) ) ) ;
81
let r = c
82
.send (
83
" POST" ,
84
" /mcp" ,
85
& [ ( " origin" , " http://evil.example" .into ( ) ) , ( " mcp-protocol-version" , " 2026-07-28" .into ( ) ) , ( " mcp-method" , " tools/list" .into ( ) ) ] ,
86
serde_json:: to_vec( & list) .unwrap ( ) ,
87
)
88
.await
89
.unwrap ( ) ;
90
assert_eq! ( r.status, 403 , " a page from another origin" ) ;
91
let r = c.send ( " GET" , " /mcp" , & [ ] , vec! [ ] ) .await.unwrap ( ) ;
92
assert_eq! ( r.status, 405 ) ;
93
94
let wrong = Client:: new( & srv.socket, Some ( bound) ) .mcp ( " 2026-07-28" , & list) .await.unwrap ( ) ;
95
assert_eq! ( wrong.status, 401 ) ;
96
assert! ( String :: from_utf8_lossy( & wrong.body) .contains ( & format! ( " bound to uid {other_uid} " ) ) ) ;
97
srv.stop ( ) ;
98
99
let v: serde_json:: Value = serde_json:: from_slice( & run ( d, & [ " admin" , " verify" , " --json" ] ) .stdout) .unwrap ( ) ;
100
assert! ( v[ " findings" ] .as_array ( ) .unwrap ( ) .is_empty ( ) , " {v}" ) ;
101
assert_eq! ( v[ " uncovered" ] , 0 , " the stop wrote a checkpoint" ) ;
102
let pool = sqlx:: SqlitePool:: connect( & format! ( " sqlite://{} " , d.join ( " data/spectra.db" ) .display ( ) ) ) .await.unwrap ( ) ;
103
let logged: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM events WHERE kind = 'guard.impersonation'" ) .fetch_one ( & pool) .await.unwrap ( ) ;
104
assert_eq! ( logged, 1 , " the wrong-uid attempt is in the ledger" ) ;
105
}
106
107
# [ tokio ::test ]
108
async fn owner_commands_go_through_the_running_daemon_and_learn_uid_binds ( ) {
109
let dir = setup ( ) ;
110
let d = dir.path ( ) ;
111
let srv = Serving:: start( d) ;
112
113
let learner = add_agent ( d, " learner" , & [ " --learn-uid" ] ) ;
114
std:: fs:: create_dir_all( d.join ( " hub" ) ) .unwrap ( ) ;
115
let o = run ( d, & [ " project" , " add" , d.join ( " hub" ) .to_str ( ) .unwrap ( ) ] ) ;
116
assert! ( o.status.success ( ) , " {}" , text ( & o) ) ;
117
let o = run ( d, & [ " agent" , " list" , " --json" ] ) ;
118
assert! ( text ( & o) .contains ( " learner" ) , " {}" , text ( & o) ) ;
119
120
let list = json! ( { " jsonrpc" : " 2.0" , " id" : 1 , " method" : " tools/list" , " params" : { } } ) ;
121
assert_eq! ( Client:: new( & srv.socket, Some ( learner) ) .mcp ( " 2025-11-25" , & list) .await.unwrap ( ) .status, 200 ) ;
122
srv.stop ( ) ;
123
let pool = sqlx:: SqlitePool:: connect( & format! ( " sqlite://{} " , d.join ( " data/spectra.db" ) .display ( ) ) ) .await.unwrap ( ) ;
124
let uid: Option< i64 > = sqlx:: query_scalar( " SELECT uid FROM agents WHERE name = 'learner'" ) .fetch_one ( & pool) .await.unwrap ( ) ;
125
assert_eq! ( uid, Some ( unsafe { libc:: getuid( ) } as i64 ) , " bound to the uid of its first signed connection" ) ;
126
let v: serde_json:: Value = serde_json:: from_slice( & run ( d, & [ " admin" , " verify" , " --json" ] ) .stdout) .unwrap ( ) ;
127
assert! ( v[ " findings" ] .as_array ( ) .unwrap ( ) .is_empty ( ) , " {v}" ) ;
128
}