Watch
0
0
Fork
You've already forked hyperhive
0

swarm-stt: the request subject, the service's NATS permissions and the --stt-client flag

This commit is contained in:
damocles 2026-11-11 21:53:10 +01:00 • committed by mara
commit 47bec6bef3
2 changed files with 171 additions and 1 deletions

View file

@ -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")?;

View file

@ -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.