09752034 Andy McCall 2 days ago ↶
1
2
3
4
5
6
571572f5 Andy McCall 2 days ago ↶
7
use super :: identity:: { owner_home, refresh_event} ;
5d695d86 Andy McCall 2 days ago ↶
8
use super :: { marks, ui, Ctx} ;
09752034 Andy McCall 2 days ago ↶
9
use anyhow:: { bail, Context, Result } ;
10
use spectra_core:: ids:: now_ms;
11
use spectra_core:: keys:: { home_identity, KeyPair} ;
12
use spectra_core:: ledger:: Ledger;
13
use spectra_core:: principals:: { identity_payload, signed_event, Principal, Principals, DEFAULT_SCOPES , DELEGATION_SCOPE } ;
14
use std:: path:: PathBuf;
15
use std:: time:: Duration;
16
17
pub const KINDS : & [ & str ] = & [ " claude-code" , " antigravity" , " kimi-code" , " codex" , " other" ] ;
18
19
pub struct Add {
20
pub name : String,
21
pub kind : String,
22
pub project : String,
23
pub home : PathBuf,
24
pub uid : Option< u32 > ,
25
pub learn_uid : bool ,
26
pub wait_secs : u64 ,
27
pub owner_home : Option< PathBuf> ,
28
}
29
30
pub async fn add ( ctx : & Ctx, a : Add) -> Result< i32 > {
31
if ! valid_name ( & a.name) {
32
bail! ( " an agent's name is lower-case letters, digits and dashes, starting with a letter or digit, at most 64" ) ;
33
}
34
if ! KINDS .contains ( & a.kind.as_str ( ) ) {
35
bail! ( " --kind is one of {}" , KINDS .join ( " , " ) ) ;
36
}
37
{
38
let ledger = Ledger:: open_read_only( & ctx.db ( ) ? ) .await? ;
39
let p = Principals:: load( & ledger) .await? ;
40
if p.agent_key ( & a.name) .is_some ( ) {
41
bail! ( " an agent named {} already exists (spectra agent list)" , a.name) ;
42
}
2fc88a7b Andy McCall 2 days ago ↶
43
let known: Option< String> =
44
sqlx:: query_scalar( " SELECT name FROM projects WHERE name = ?" ) .bind ( & a.project) .fetch_optional ( ledger.pool ( ) ) .await? ;
45
if known.is_none ( ) {
46
bail! ( " no project named {} (spectra project add PATH --name {})" , a.project, a.project) ;
47
}
09752034 Andy McCall 2 days ago ↶
48
}
49
let home = owner_home ( ctx, a.owner_home.clone ( ) ) ;
50
51
let agent = if a.home.join ( " keys" ) .exists ( ) {
52
KeyPair:: load_home( & a.home) .with_context ( | | format! ( " reading the agent's key in {} " , a.home.display ( ) ) ) ?
53
} else {
54
let pair = KeyPair:: create_home( & a.home, & a.name, " " ) ? ;
5d695d86 Andy McCall 2 days ago ↶
55
println! ( " {} {} {} " , ui:: done( " Created" ) , ui:: subject( format! ( " {} 's key" , a.name) ) , ui:: path( a.home.display ( ) ) ) ;
09752034 Andy McCall 2 days ago ↶
56
pair
57
} ;
58
let delegated = | id : & oxy_core:: identity:: Identity| id.delegates_to ( & agent.key, DELEGATION_SCOPE , now_ms ( ) ) ;
59
if ! delegated ( & home_identity ( & home) ? ) {
5d695d86 Andy McCall 2 days ago ↶
60
println! ( " {} {} " , ui:: caution( ) , ui:: warn( format! ( " {} is not yours yet: delegate to its key, as yourself:" , a.name) ) ) ;
09752034 Andy McCall 2 days ago ↶
61
println! (
5d695d86 Andy McCall 2 days ago ↶
62
" {} " ,
09752034 Andy McCall 2 days ago ↶
63
ui:: id( format! (
64
" oxy identity delegate add {} --scope changes --expires 8h" ,
65
a.home.join ( " keys" ) .join ( format! ( " {} .identity" , agent.key.to_hex ( ) ) ) .display ( )
66
) )
67
) ;
5d695d86 Andy McCall 2 days ago ↶
68
let spin = ui:: spinner( & format! (
69
" waiting for your delegation to {} (up to {} s; Ctrl-C stops, and running this again resumes)" ,
70
a.name, a.wait_secs
71
) ) ;
09752034 Andy McCall 2 days ago ↶
72
let deadline = std:: time:: Instant:: now( ) + Duration:: from_secs( a.wait_secs) ;
73
loop {
74
if home_identity ( & home) .is_ok_and ( | id | delegated ( & id) ) {
5d695d86 Andy McCall 2 days ago ↶
75
spin.clear ( ) ;
09752034 Andy McCall 2 days ago ↶
76
break ;
77
}
78
if std:: time:: Instant:: now( ) > = deadline {
5d695d86 Andy McCall 2 days ago ↶
79
spin.clear ( ) ;
09752034 Andy McCall 2 days ago ↶
80
bail! ( " {}'s key is not delegated yet: run the command above, then this again" , a.name) ;
81
}
82
tokio:: time:: sleep( Duration:: from_secs( 2 ) ) .await;
83
}
84
}
571572f5 Andy McCall 2 days ago ↶
85
let w = ctx.owner_writer ( & home) .await? ;
86
let mut events: Vec< _ > = refresh_event ( w.ledger ( ) , & home) .await? .into_iter ( ) .collect ( ) ;
09752034 Andy McCall 2 days ago ↶
87
let owner = KeyPair:: load_home( & home) ? ;
88
let mut p = identity_payload ( & agent.identity) ;
89
p[ " name" ] = a.name.clone ( ) .into ( ) ;
90
p[ " kind" ] = a.kind.into ( ) ;
91
p[ " project" ] = a.project.into ( ) ;
92
p[ " home" ] = a.home.display ( ) .to_string ( ) .into ( ) ;
93
p[ " owner" ] = owner.key.to_hex ( ) .into ( ) ;
94
p[ " owner_name" ] = owner.identity.name.clone ( ) .into ( ) ;
95
p[ " scopes" ] = DEFAULT_SCOPES .into ( ) ;
96
if let Some ( u) = a.uid {
97
p[ " uid" ] = u.into ( ) ;
98
}
99
p[ " learn_uid" ] = a.learn_uid.into ( ) ;
100
let d = owner.identity.delegate ( & agent.key) .context ( " the delegation vanished" ) ? ;
101
p[ " delegation" ] = serde_json:: json! ( { " scope" : d.scope.names ( ) , " expires" : d.expires } ) ;
571572f5 Andy McCall 2 days ago ↶
102
events.push ( signed_event ( & owner, " human" , " agent.added" , p) ) ;
103
w.append ( events) .await? ;
5d695d86 Andy McCall 2 days ago ↶
104
println! ( " {} {} {} {} " , ui:: done( " Added" ) , ui:: subject( & a.name) , ui:: sep( ) , ui:: author( format! ( " {} 's agent" , owner.identity.name) ) ) ;
105
println! ( " {} " , ui:: kv( " key" , ui:: hash( agent.key.to_hex ( ) ) ) ) ;
106
let until = d.expires.map ( | e | format! ( " until {} " , super :: format:: when( e) ) ) .unwrap_or_default ( ) ;
107
println! ( " {} " , ui:: kv( " delegated" , format! ( " {} {until} " , d.scope.names ( ) .join ( " ," ) ) ) ) ;
108
println! ( " {} {} {} " , ui:: note( ) , ui:: dim( " connect its client with" ) , ui:: id( format! ( " spectra mcp --agent {} " , a.name) ) ) ;
09752034 Andy McCall 2 days ago ↶
109
Ok ( 0 )
2fc88a7b Andy McCall 2 days ago ↶
110
}
111
112
113
# [ derive ( sqlx::FromRow ) ]
114
struct AgentRow {
115
name : String,
116
key : String,
117
kind : String,
118
project : String,
119
disabled_ms : Option< i64 > ,
120
presence : Option< String> ,
121
status : Option< String> ,
09752034 Andy McCall 2 days ago ↶
122
}
123
124
pub async fn list ( ctx : & Ctx, json : bool ) -> Result< i32 > {
125
let ledger = Ledger:: open_read_only( & ctx.db ( ) ? ) .await? ;
126
let p = Principals:: load( & ledger) .await? ;
2fc88a7b Andy McCall 2 days ago ↶
127
let rows: Vec< AgentRow> = sqlx:: query_as(
128
" SELECT a.name, a.key, a.kind, a.project, a.disabled_ms, p.state AS presence, p.status_text AS status FROM agents a LEFT JOIN presence p ON p.agent = a.name ORDER BY a.name" ,
129
)
130
.fetch_all ( ledger.pool ( ) )
131
.await? ;
09752034 Andy McCall 2 days ago ↶
132
let owner = p.owner ( ) ;
133
let now = now_ms ( ) ;
134
let mut out = Vec :: new( ) ;
2fc88a7b Andy McCall 2 days ago ↶
135
for AgentRow { name, key, kind, project, disabled_ms: disabled, presence, status } in rows {
09752034 Andy McCall 2 days ago ↶
136
let k = oxy_core:: hash:: ObjectRef:: from_hex( & key) .context ( " a stored key" ) ? ;
137
let state = match p.principal ( & k, now) {
138
Ok ( _ ) => " ok" .to_string ( ) ,
139
Err ( why) => why,
140
} ;
141
let expires = owner.and_then ( | o | o.delegate ( & k) ) .and_then ( | d | d.expires ) ;
142
let who = match p.principal ( & k, now) {
143
Ok ( pr @ Principal:: Agent { .. } ) => pr.display ( ) ,
144
_ => format! ( " {name} · {} 's agent" , owner.map ( | o | o.name.as_str ( ) ) .unwrap_or ( " ?" ) ) ,
145
} ;
2fc88a7b Andy McCall 2 days ago ↶
146
let presence = if disabled.is_some ( ) { " offline" .to_string ( ) } else { presence.unwrap_or_else ( | | " offline" .into ( ) ) } ;
147
out.push ( serde_json:: json! ( { " name" : name, " display" : who, " key" : key, " kind" : kind, " project" : project, " state" : state, " presence" : presence, " status" : status, " delegation_expires" : expires, " disabled" : disabled.is_some ( ) } ) ) ;
09752034 Andy McCall 2 days ago ↶
148
}
149
if json {
150
println! ( " {} " , serde_json:: Value:: Array( out) ) ;
151
return Ok ( 0 ) ;
152
}
153
if out.is_empty ( ) {
5d695d86 Andy McCall 2 days ago ↶
154
println! ( " {} {} " , ui:: note( ) , ui:: dim( " no agents yet: spectra agent add NAME --kind KIND --project PROJECT --home DIR" ) ) ;
09752034 Andy McCall 2 days ago ↶
155
}
156
for a in out {
157
let state = a[ " state" ] .as_str ( ) .unwrap_or ( " " ) ;
5d695d86 Andy McCall 2 days ago ↶
158
let until = a[ " delegation_expires" ] .as_u64 ( ) .map ( | e | format! ( " {} until {} " , ui:: sep( ) , super :: format:: when( e) ) ) .unwrap_or_default ( ) ;
159
let display = a[ " display" ] .as_str ( ) .unwrap_or ( " " ) ;
160
let ( name, owner) = display.split_once ( " · " ) .unwrap_or ( ( display, " " ) ) ;
2fc88a7b Andy McCall 2 days ago ↶
161
println! (
162
" {} {} {} {} {} " ,
163
marks:: health( state) ,
164
marks:: presence( a[ " presence" ] .as_str ( ) .unwrap_or ( " offline" ) ) ,
165
ui:: subject( name) ,
166
ui:: sep( ) ,
167
ui:: author( owner)
168
) ;
169
if let Some ( st) = a[ " status" ] .as_str ( ) {
170
println! ( " {} {} " , ui:: note( ) , st) ;
171
}
09752034 Andy McCall 2 days ago ↶
172
println! (
5d695d86 Andy McCall 2 days ago ↶
173
" {} " ,
174
ui:: dim( format! (
175
" {} {} {} {} {} {until} " ,
176
a[ " kind" ] .as_str ( ) .unwrap_or ( " " ) ,
177
ui:: sep( ) ,
178
a[ " project" ] .as_str ( ) .unwrap_or ( " " ) ,
179
ui:: sep( ) ,
180
& a[ " key" ] .as_str ( ) .unwrap_or ( " " ) [ .. 12 ]
181
) )
09752034 Andy McCall 2 days ago ↶
182
) ;
183
if state ! = " ok" {
5d695d86 Andy McCall 2 days ago ↶
184
println! ( " {} {} " , ui:: caution( ) , ui:: warn( state) ) ;
09752034 Andy McCall 2 days ago ↶
185
}
186
}
187
Ok ( 0 )
188
}
189
190
pub async fn disable ( ctx : & Ctx, name : & str , owner_home_flag : Option< PathBuf> ) -> Result< i32 > {
191
let home = owner_home ( ctx, owner_home_flag) ;
571572f5 Andy McCall 2 days ago ↶
192
let w = ctx.owner_writer ( & home) .await? ;
193
let p = Principals:: load( w.ledger ( ) ) .await? ;
194
p.agent_key ( name) .with_context ( | | format! ( " no agent named {name} " ) ) ? ;
195
let mut events: Vec< _ > = refresh_event ( w.ledger ( ) , & home) .await? .into_iter ( ) .collect ( ) ;
09752034 Andy McCall 2 days ago ↶
196
let owner = KeyPair:: load_home( & home) ? ;
571572f5 Andy McCall 2 days ago ↶
197
events.push ( signed_event ( & owner, " human" , " agent.disabled" , serde_json:: json! ( { " name" : name } ) ) ) ;
198
w.append ( events) .await? ;
5d695d86 Andy McCall 2 days ago ↶
199
println! ( " {} {} " , ui:: done( " Disabled" ) , ui:: subject( name) ) ;
200
println! ( " {} {} " , ui:: note( ) , ui:: dim( " its key is refused from now on; earlier events keep their signatures" ) ) ;
09752034 Andy McCall 2 days ago ↶
201
Ok ( 0 )
202
}
203
204
pub fn valid_name ( n : & str ) -> bool {
205
! n.is_empty ( )
206
& & n.len ( ) < = 64
207
& & n.bytes ( ) .next ( ) .is_some_and ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) )
208
& & n.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | b = = b ' -' )
209
}