use crate::approval_facts::ApprovalFacts;
use crate::interactive::session_runner::RUNNER_GAP_REMEDY;
pub(crate) fn composition_refusal(token: &str, facts: &ApprovalFacts) -> Option<String> {
reason(token, facts).map(|reason| {
format!(
"scope `{token}` is not carried by this session's composition: {reason}. It \
parses, but nothing this session composed would consume it, so approving it \
would gate nothing. Re-issue /allow without it."
)
})
}
fn reason(token: &str, facts: &ApprovalFacts) -> Option<String> {
match token {
"workspace-write" if facts.workspace_root.is_none() => Some(
"no workspace root is bound, so the write-shaped file tools are not \
composed and no workspace_write call can occur"
.to_owned(),
),
"scratch" if facts.scratch.is_none() => Some(
"this session composed no scratch database, so no scratch_sql call can \
occur"
.to_owned(),
),
_ if token.starts_with("fetch:") && facts.fetch.is_none() => Some(
"this session composed no fetch member, so no http_fetch call can \
occur"
.to_owned(),
),
_ if token.starts_with("runner:") || token.starts_with("interpreter:") => {
runner_family_reason(token, facts)
}
_ if token.starts_with("command:") && facts.host.is_none() => Some(
"the host-command lane is not composed in this session, so the token gates \
nothing in this session; no workspace root is bound, so the lane composed \
nothing — bind one with `--workspace <dir>` or launch inside a worktree"
.to_owned(),
),
_ => None,
}
}
fn runner_family_reason(token: &str, facts: &ApprovalFacts) -> Option<String> {
let Some(runner) = facts.runner.as_ref() else {
return Some(format!(
"this session composed no runner, so no run_program call can \
occur — an unproven host or an unstaged config composes nothing, \
and {RUNNER_GAP_REMEDY}"
));
};
if let Some(program) = token.strip_prefix("runner:") {
return (!runner
.runner_programs
.iter()
.any(|allowed| allowed == program))
.then(|| {
format!(
"the composed [jobs.runner] allow carries no `{program}`, so the \
runner door refuses every call for it, and {RUNNER_GAP_REMEDY}"
)
});
}
let program = token.strip_prefix("interpreter:")?;
(!runner
.interpreter_programs
.iter()
.any(|staged| staged == program))
.then(|| {
format!(
"[jobs.interpreter] allow staged no `{program}`, so the interpreter \
door does not exist and an interpreter call for it refuses by name"
)
})
}