swarm-stt: the request subject, the service's NATS permissions and the --stt-client flag
This commit is contained in:
parent
40ec5ac9e0
commit
47bec6bef3
2 changed files with 171 additions and 1 deletions
|
|
@ -166,6 +166,11 @@ struct Args {
|
|||
#[arg(long = "notify-router-client")]
|
||||
notify_router_clients: Vec<String>,
|
||||
|
||||
/// Client ids that answer voice memo transcription requests.
|
||||
/// Repeatable, empty by default, which denies such a client.
|
||||
#[arg(long = "stt-client")]
|
||||
stt_clients: Vec<String>,
|
||||
|
||||
/// Role on the secret store's `cert` auth mount this responder logs in
|
||||
/// as to read the callout keys, its own OIDC client secret and agent
|
||||
/// credentials. The store is found
|
||||
|
|
@ -232,7 +237,8 @@ async fn serve(args: Args) -> anyhow::Result<()> {
|
|||
.with_review_classifiers(args.review_classifier_clients.clone())
|
||||
.with_builders(args.builder_clients.clone())
|
||||
.with_claude_logins(args.claude_login_clients.clone())
|
||||
.with_notify_routers(args.notify_router_clients.clone());
|
||||
.with_notify_routers(args.notify_router_clients.clone())
|
||||
.with_stts(args.stt_clients.clone());
|
||||
let http = reqwest::Client::new();
|
||||
let settings = swarm_secret_client::client::Settings::from_env()
|
||||
.context("reading the secret store's address and this responder's identity")?;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ pub struct Policy {
|
|||
review_classifiers: Vec<String>,
|
||||
builders: Vec<String>,
|
||||
claude_logins: Vec<String>,
|
||||
stts: Vec<String>,
|
||||
notify_routers: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +100,7 @@ impl Policy {
|
|||
review_classifiers: Vec::new(),
|
||||
builders: Vec::new(),
|
||||
claude_logins: Vec::new(),
|
||||
stts: Vec::new(),
|
||||
notify_routers: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
|
@ -142,6 +144,13 @@ impl Policy {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the client ids that answer voice memo transcription requests.
|
||||
#[must_use]
|
||||
pub fn with_stts(mut self, clients: Vec<String>) -> Self {
|
||||
self.stts = clients;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client ids that route forge events to agents' todo queues.
|
||||
#[must_use]
|
||||
pub fn with_notify_routers(mut self, clients: Vec<String>) -> Self {
|
||||
|
|
@ -223,9 +232,25 @@ impl Policy {
|
|||
if self.notify_routers.iter().any(|c| c == client_id) {
|
||||
return Some(Self::notify_router_permissions(client_id));
|
||||
}
|
||||
if self.stts.iter().any(|c| c == client_id) {
|
||||
return Some(Self::stt_permissions(client_id));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// What the speech-to-text service may do: hear every agent's request
|
||||
/// subject and its own inbox, and answer each request on its reply
|
||||
/// subject. It publishes only into its own inbox, which reaches no other
|
||||
/// client; the replies need no publish grant.
|
||||
fn stt_permissions(client_id: &str) -> Permissions {
|
||||
let inbox = Self::inbox(client_id);
|
||||
Permissions {
|
||||
publish: vec![inbox.clone()],
|
||||
subscribe: Some(vec![swarm_wire::stt::subject_wildcard(), inbox]),
|
||||
responses: Some(swarm_wire::stt::REPLY_DEADLINE),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the review classifier may do: hear try-out requests and answer
|
||||
/// each on its reply subject, for as long as a run may take.
|
||||
///
|
||||
|
|
@ -1931,6 +1956,60 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
const STT: &str = "swarm-stt";
|
||||
|
||||
#[test]
|
||||
fn the_stt_service_hears_every_agents_subject_and_its_inbox_and_answers_by_reply() {
|
||||
use swarm_queue_client::client_inbox_prefix;
|
||||
use swarm_wire::stt::{REPLY_DEADLINE, request_subject};
|
||||
|
||||
let p = policy()
|
||||
.with_stts(vec![STT.to_owned()])
|
||||
.permissions(STT)
|
||||
.expect("a configured service is admitted");
|
||||
let inbox = client_inbox_prefix(STT);
|
||||
assert!(may_subscribe(&p, &request_subject("iris")));
|
||||
assert!(may_subscribe(&p, &request_subject("damocles")));
|
||||
assert!(may_subscribe(&p, &reply(&inbox)));
|
||||
for refused in [
|
||||
">".to_owned(),
|
||||
"$SWARM.>".to_owned(),
|
||||
swarm_wire::claude_login::SUBJECT.to_owned(),
|
||||
reply(&client_inbox_prefix("swarm-controller")),
|
||||
] {
|
||||
assert!(
|
||||
!may_subscribe(&p, &refused),
|
||||
"the service may subscribe to {refused}"
|
||||
);
|
||||
}
|
||||
assert_eq!(p.publish, vec![format!("{inbox}.>")]);
|
||||
assert_eq!(p.responses, Some(REPLY_DEADLINE));
|
||||
// The service's id is nothing until it is configured.
|
||||
assert_eq!(policy().permissions(STT), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_token_sends_audio_on_its_own_subject_only() {
|
||||
use swarm_wire::stt::{SUBJECT_PREFIX, request_subject};
|
||||
|
||||
let p = policy()
|
||||
.with_agent_token_subjects(vec![format!("{SUBJECT_PREFIX}.{{agent}}")])
|
||||
.expect("the template names the agent");
|
||||
let grant = p.agent_token_permissions("iris").expect("configured");
|
||||
assert!(
|
||||
grant
|
||||
.publish
|
||||
.iter()
|
||||
.any(|g| covers(g, &request_subject("iris")))
|
||||
);
|
||||
assert!(
|
||||
!grant
|
||||
.publish
|
||||
.iter()
|
||||
.any(|g| covers(g, &request_subject("argus")))
|
||||
);
|
||||
}
|
||||
|
||||
const NOTIFY_ROUTER: &str = "swarm-notify-router";
|
||||
|
||||
/// The three router-todo templates as `swarm-nats.nix` spells them.
|
||||
|
|
|
|||
Loading…
Reference in a new issue