use crate::commands::run::scopes::{self, Surface};
use saya_agent::{ApprovalChoice, ApprovalPolicy, SessionGrants, SessionPolicy};
use saya_store::{GrantSource, SessionJournal};
pub(crate) fn journal_warning(error: &saya_store::StoreError) -> String {
format!(
"warning: the session journal could not record this ({error}) — the grant stands, \
but the audit line is missing"
)
}
pub(crate) fn record_prompt_answer(
policy: &SessionPolicy,
choice: &ApprovalChoice,
journal: Option<&SessionJournal>,
) -> (bool, Option<String>) {
let new_grant = policy.record(choice.clone());
if !new_grant {
return (false, None);
}
let ApprovalChoice::AllowSession { token } = choice else {
return (true, None);
};
let Some(journal) = journal else {
return (true, None);
};
match journal.granted(token, GrantSource::Prompt) {
Ok(()) => (true, None),
Err(error) => (true, Some(journal_warning(&error))),
}
}
pub(crate) fn allow(
tokens: &[String],
composition: &crate::approval_facts::ApprovalFacts,
grants: &SessionGrants,
journal: &SessionJournal,
) -> Result<String, String> {
let approved = scopes::parse(tokens, Surface::Session)?;
if approved.tokens.iter().any(|token| token == "none") {
return Ok(
"`none` states the empty approval: nothing seeded, nothing revoked — \
the store keeps whatever this session already holds."
.to_owned(),
);
}
for token in &approved.tokens {
if denied_payload(token, composition).is_some() {
return Err(super::session_deny::allow_of_denied_refusal(token));
}
if let Some(refusal) = super::allow_refusal::composition_refusal(token, composition) {
return Err(refusal);
}
}
let mut seeded = Vec::new();
let mut already = Vec::new();
let mut warnings = Vec::new();
for token in &approved.tokens {
if grants.grant(token) {
if let Err(error) = journal.granted(token, GrantSource::Seed) {
warnings.push(journal_warning(&error));
}
seeded.push(token.clone());
} else {
already.push(token.clone());
}
}
let mut message = String::new();
if !seeded.is_empty() {
message.push_str(&format!(
"granted for this session (dies with it): {}",
seeded.join(", ")
));
}
if !already.is_empty() {
if !message.is_empty() {
message.push('\n');
}
message.push_str(&format!(
"already granted (nothing changed): {}",
already.join(", ")
));
}
for warning in warnings {
if !message.is_empty() {
message.push('\n');
}
message.push_str(&warning);
}
Ok(message)
}
fn denied_payload(
token: &str,
composition: &crate::approval_facts::ApprovalFacts,
) -> Option<String> {
let payload = token
.strip_prefix("command:")
.or_else(|| token.strip_prefix("runner:"))
.or_else(|| token.strip_prefix("interpreter:"))?;
composition
.denied_programs
.iter()
.any(|denied| denied == payload)
.then(|| payload.to_owned())
}
pub(crate) fn seed_launch_allow(
tokens: &[String],
composition: &crate::approval_facts::ApprovalFacts,
grants: &SessionGrants,
) -> Result<Vec<String>, String> {
let approved = scopes::parse(tokens, Surface::Session)?;
let mut seeded = Vec::new();
for token in &approved.tokens {
if denied_payload(token, composition).is_some() {
return Err(super::session_deny::allow_of_denied_refusal(token));
}
if let Some(refusal) = super::allow_refusal::composition_refusal(token, composition) {
return Err(refusal);
}
grants.grant(token);
seeded.push(token.clone());
}
Ok(seeded)
}
pub(crate) fn listing(mode: ApprovalPolicy, grants: &SessionGrants) -> String {
let mut out = String::new();
if mode == ApprovalPolicy::Bypass {
out.push_str("mode bypass: every call runs without asking; grants are not consulted\n");
}
let tokens = grants.tokens();
out.push_str(&format!(
"session grants (die with this session): {}",
tokens.len()
));
if tokens.is_empty() {
out.push_str(
"\n (none — nothing pre-answers this session yet; /allow <scopes> \
or answer [s] at an ask)",
);
} else {
for token in tokens {
out.push_str("\n ");
out.push_str(&token);
}
}
out
}
pub(crate) struct RunSeed {
pub(crate) forwarded: Vec<String>,
pub(crate) dropped: Vec<String>,
}
pub(crate) fn run_seed(tokens: &[String]) -> RunSeed {
let mut forwarded = Vec::new();
let mut dropped = Vec::new();
for token in tokens {
match scopes::parse(std::slice::from_ref(token), Surface::Run) {
Ok(_) => forwarded.push(token.clone()),
Err(_) => dropped.push(token.clone()),
}
}
RunSeed { forwarded, dropped }
}
pub(crate) fn seed_message(seed: &RunSeed) -> String {
if seed.forwarded.is_empty() && seed.dropped.is_empty() {
return "no session grants to seed — the run's --allow is yours to state".to_owned();
}
let mut lines = Vec::new();
if !seed.forwarded.is_empty() {
lines.push(format!(
"seeded the run's --allow from this session's grants: {}",
seed.forwarded.join(", ")
));
}
if !seed.dropped.is_empty() {
lines.push(format!(
"not forwarded — a run refuses these scopes (they would gate nothing there): {}",
seed.dropped.join(", ")
));
}
lines.join("\n")
}