use crate::agent::tools::DatabaseTools;
use crate::approval_facts::{ApprovalFacts, RunnerFacts};
use crate::commands::run::scopes;
use crate::connection::ConnectionRegistry;
use crate::grant_token::{SQL_FAMILY, grant_token, session_answers_line};
use async_trait::async_trait;
use saya_agent::{ApprovalDecision, ApprovalPolicy, LocalStateEffect, SessionPolicy};
use saya_connectors::DatabaseConnector;
use saya_harness::fetch::FetchDestination;
use saya_types::{ConnectionError, QueryRequest, QueryResult, SchemaTree, SqlDialect};
use serde_json::{Value, json};
use std::path::PathBuf;
fn runner_facts(runner: &[&str], interpreters: &[&str]) -> Option<RunnerFacts> {
Some(RunnerFacts {
runner_programs: runner.iter().map(|p| (*p).to_owned()).collect(),
interpreter_programs: interpreters.iter().map(|p| (*p).to_owned()).collect(),
..RunnerFacts::default()
})
}
fn facts_with_runner(runner: Option<RunnerFacts>) -> ApprovalFacts {
ApprovalFacts {
runner,
..ApprovalFacts::default()
}
}
fn composed_facts() -> ApprovalFacts {
ApprovalFacts {
runner: runner_facts(&["bench", "ripgrep"], &["python3", "bash"]),
workspace_root: Some(PathBuf::from("/home/user/proj")),
scratch: Some(crate::approval_facts::ScratchFacts {
row_cap: 50,
timeout_seconds: 30,
}),
fetch: Some(crate::approval_facts::FetchFacts {
fetch_body_bytes: 61_440,
fetch_seconds: 30,
fetch_redirects: 5,
download: None,
}),
..ApprovalFacts::default()
}
}
fn token_for(tool: &str, arguments: Value, facts: &ApprovalFacts) -> String {
grant_token(tool, &arguments, None, facts)
.unwrap_or_else(|| panic!("{tool} with {arguments} must suggest a token for this test"))
}
fn sql_suggestion(tool: &str, arguments: Value, registry: &ConnectionRegistry) -> Option<String> {
grant_token(
tool,
&arguments,
registry.primary(),
&ApprovalFacts::default(),
)
}
fn approves_what_it_names(token: &str, approved: &scopes::Approved) -> bool {
if token.starts_with("sql:") {
return approved.tokens.iter().any(|stated| stated == token);
}
let capabilities = &approved.capabilities;
match token {
"workspace-write" => capabilities.workspace_write,
"scratch" => capabilities.scratch,
other => match other.split_once(':') {
Some(("fetch", rest)) => capabilities.fetch.as_ref().is_some_and(|scope| {
scope
.destinations
.iter()
.any(|d| format!("{}+{}", d.scheme, d.host) == rest)
}),
Some(("runner", program)) => capabilities
.runner
.as_ref()
.is_some_and(|scope| scope.programs.iter().any(|allowed| allowed == program)),
Some(("interpreter", program)) => capabilities
.interpreter
.as_ref()
.is_some_and(|scope| scope.programs.iter().any(|allowed| allowed == program)),
_ => false,
},
}
}
struct DummyConnector {
dialect: SqlDialect,
}
#[async_trait]
impl DatabaseConnector for DummyConnector {
fn dialect(&self) -> SqlDialect {
self.dialect
}
async fn connect(&self) -> Result<(), ConnectionError> {
Ok(())
}
async fn schema(&self) -> Result<SchemaTree, ConnectionError> {
Err(ConnectionError::schema_failed("dummy"))
}
async fn execute(&self, _: QueryRequest) -> Result<QueryResult, ConnectionError> {
Err(ConnectionError::query_failed("dummy"))
}
}
pub(crate) fn registry_with_primary(name: &str) -> ConnectionRegistry {
let mut registry = ConnectionRegistry::new(name);
registry.insert(
name,
crate::connection::ConnectionEntry {
connector: Box::new(DummyConnector {
dialect: SqlDialect::Postgres,
}),
dialect: SqlDialect::Postgres,
profile_id: None,
},
);
registry
}
fn empty_registry() -> ConnectionRegistry {
ConnectionRegistry::new("")
}
#[test]
fn every_suggestible_token_parses_to_the_capability_it_names() {
let cases: Vec<(String, Value)> = vec![
("workspace_write".to_owned(), json!({})),
(
"workspace_write".to_owned(),
json!({"path": "a.md", "content": "x"}),
),
(
"scratch_sql".to_owned(),
json!({"sql": "CREATE TABLE t (a int)"}),
),
(
"http_fetch".to_owned(),
json!({"url": "https://a.example/x"}),
),
(
"http_download".to_owned(),
json!({"url": "https://b.example/f.bin", "destination": "f.bin"}),
),
("run_program".to_owned(), json!({"program": "bench"})),
("run_program".to_owned(), json!({"program": "ripgrep"})),
("run_program".to_owned(), json!({"program": "python3"})),
("run_program".to_owned(), json!({"program": "bash"})),
(
"bounded_sql_query".to_owned(),
json!({"sql": "SELECT 1", "connection": "analytics"}),
),
(
"result_shape".to_owned(),
json!({"sql": "SELECT 1", "connection": "analytics"}),
),
(
"column_health".to_owned(),
json!({"sql": "SELECT 1", "connection": "analytics"}),
),
(
"join_check".to_owned(),
json!({"sql": "SELECT 1", "connection": "analytics"}),
),
];
for (tool, arguments) in cases {
let token = token_for(&tool, arguments, &composed_facts());
let approved = scopes::parse(std::slice::from_ref(&token), scopes::Surface::Session)
.unwrap_or_else(|error| panic!("`{token}` must parse under /allow: {error}"));
assert!(
approves_what_it_names(&token, &approved),
"`{token}` must approve exactly the capability it names, got: {:?}",
approved.capabilities
);
}
}
#[test]
fn the_sql_family_is_the_definitions_read_sql_shape() {
let definitions = DatabaseTools::definitions(true, false, false, false, true);
let declared: Vec<&str> = definitions
.iter()
.filter(|tool| {
let shaped_arguments = tool.parameters.get("properties").is_some_and(|properties| {
properties.get("sql").is_some() && properties.get("connection").is_some()
});
!tool.effect.external_side_effect
&& tool.effect.requires_approval
&& tool.effect.local_state == LocalStateEffect::None
&& shaped_arguments
})
.map(|tool| tool.name.as_str())
.collect();
assert_eq!(
declared, SQL_FAMILY,
"the suggester's SQL family must be exactly the tools the definitions \
declare as one read-only statement against a named connection"
);
let registry = registry_with_primary("analytics");
for tool in SQL_FAMILY {
assert_eq!(
sql_suggestion(
tool,
json!({"sql": "SELECT 1", "connection": "staging"}),
®istry
),
Some("sql:staging".to_owned()),
"every family member grants under one word per connection"
);
}
}
#[test]
fn one_sql_grant_covers_the_family_on_one_connection_only() {
let definitions = DatabaseTools::definitions(true, false, false, false, true);
let policy = SessionPolicy::new(ApprovalPolicy::Ask);
policy.grants().grant("sql:analytics");
for tool in SQL_FAMILY {
let definition = definitions
.iter()
.find(|definition| definition.name == *tool)
.unwrap_or_else(|| panic!("{tool} must be defined when the gate is open"));
assert_eq!(
policy.resolve(&definition.effect, Some("sql:analytics")),
ApprovalDecision::Allow,
"the one SQL grant pre-answers {tool} on the granted connection"
);
assert_eq!(
policy.resolve(&definition.effect, Some("sql:staging")),
ApprovalDecision::Ask,
"the one SQL grant allows nothing on a different connection"
);
}
}
#[test]
fn render_chart_and_the_fan_out_suggest_no_token() {
let registry = registry_with_primary("analytics");
assert_eq!(
grant_token(
"bounded_sql_query_all",
&json!({"sql": "SELECT 1"}),
registry.primary(),
&composed_facts()
),
None,
"the fan-out's referent can grow after approval — it suggests no token"
);
assert_eq!(
grant_token(
"render_chart",
&json!({"sql": "SELECT 1", "chart_type": "bar", "connection": "analytics"}),
registry.primary(),
&composed_facts()
),
None,
"render_chart writes a file and opens a browser — not a `sql:` grant's \
words"
);
}
#[test]
fn a_call_naming_no_connection_suggests_the_primary_s_real_name() {
let arguments = json!({"sql": "SELECT 1"});
let registry = registry_with_primary("analytics");
for tool in SQL_FAMILY {
assert_eq!(
sql_suggestion(tool, arguments.clone(), ®istry),
Some("sql:analytics".to_owned()),
"{tool} with no connection names the primary's registry name"
);
}
assert_eq!(
sql_suggestion(
"bounded_sql_query",
json!({"sql": "SELECT 1", "connection": ""}),
®istry
),
Some("sql:analytics".to_owned()),
"an empty connection names no connection — the primary's own rule"
);
assert_eq!(
sql_suggestion("bounded_sql_query", arguments.clone(), &empty_registry()),
None,
"no primary resolves, no token — never a guessed name"
);
let mut odd = ConnectionRegistry::new("prod eu");
odd.insert(
"prod eu",
crate::connection::ConnectionEntry {
connector: Box::new(DummyConnector {
dialect: SqlDialect::Postgres,
}),
dialect: SqlDialect::Postgres,
profile_id: None,
},
);
assert_eq!(
sql_suggestion("bounded_sql_query", arguments, &odd),
None,
"a non-name-shaped primary is never a grant word"
);
}
#[test]
fn a_named_connection_is_judged_by_the_name_shape_rule() {
let registry = registry_with_primary("analytics");
assert_eq!(
sql_suggestion(
"bounded_sql_query",
json!({"sql": "SELECT 1", "connection": "bad name"}),
®istry
),
None,
"whitespace is never a connection name"
);
assert_eq!(
sql_suggestion(
"bounded_sql_query",
json!({"sql": "SELECT 1", "connection": 7}),
®istry
),
None,
"a non-string connection is not a name"
);
assert_eq!(
sql_suggestion(
"bounded_sql_query",
json!({"sql": "SELECT 1", "connection": "Analytics_2"}),
®istry
),
Some("sql:Analytics_2".to_owned()),
"a name-shaped payload rides verbatim"
);
}
#[test]
fn the_privacy_gate_stands_above_the_grants() {
let closed = DatabaseTools::definitions(false, false, false, false, false);
let open = DatabaseTools::definitions(true, false, false, false, true);
for tool in SQL_FAMILY {
assert!(
!closed.iter().any(|definition| definition.name == *tool),
"the gate is closed: `{tool}` must be hidden, so no ask exists a \
grant could pre-answer"
);
assert!(
open.iter().any(|definition| definition.name == *tool),
"the gate open: `{tool}` is the family this slice grants"
);
}
let granted = SessionPolicy::new(ApprovalPolicy::Ask);
granted.grants().grant("sql:analytics");
assert!(!granted.grants().is_empty(), "the store holds a grant");
for tool in SQL_FAMILY {
assert!(
!closed.iter().any(|definition| definition.name == *tool),
"`{tool}` stays hidden with a grant in the store — a grant cannot \
re-open the gate"
);
}
}
#[test]
fn tools_outside_the_grammar_s_families_get_no_token() {
let registry = registry_with_primary("analytics");
for tool in [
"schema_discovery",
"workspace_read",
"workspace_list",
"glob",
"grep",
"designate_answer",
"contract_search",
"contract_read",
"no_such_tool",
] {
assert_eq!(
grant_token(
tool,
&json!({"sql": "SELECT 1"}),
registry.primary(),
&composed_facts()
),
None,
"{tool} must keep asking every call — no token names it"
);
}
}
#[test]
fn the_fetch_token_spells_the_host_the_run_engine_s_way() {
for url in [
"https://a.example/x",
"https://A.Example/deep/path?q=1",
"https://a.example:8443/x",
"https://a.example",
] {
let token = token_for("http_fetch", json!({"url": url}), &composed_facts());
let parsed = url::Url::parse(url).expect("the test URL parses");
let engine = FetchDestination::new(
parsed.scheme(),
parsed.host_str().expect("the test URL names a host"),
);
assert_eq!(
token,
format!("fetch:{}+{}", engine.scheme(), engine.host()),
"the token must name the destination as the run engine normalises it"
);
}
}
#[test]
fn a_seeded_token_and_a_suggested_token_are_the_same_string_for_a_destination() {
let suggested = token_for(
"http_fetch",
json!({"url": "https://Example.com/x"}),
&composed_facts(),
);
let approved = scopes::parse(
&["fetch:HTTPS+Example.com".to_string()],
scopes::Surface::Session,
)
.expect("a mixed-case fetch token parses on the session surface");
assert_eq!(
approved.tokens,
vec![suggested],
"the seeded token and the suggested token are one string for one destination"
);
}
#[test]
fn a_malformed_or_absent_argument_yields_none_never_a_token() {
let facts = composed_facts();
assert_eq!(
grant_token("http_fetch", &json!({}), None, &facts),
None,
"no url"
);
assert_eq!(
grant_token("http_fetch", &json!({"url": "not a url"}), None, &facts),
None,
"not a URL"
);
assert_eq!(
grant_token(
"http_fetch",
&json!({"url": "mailto:someone@example.com"}),
None,
&facts
),
None,
"a scheme with no host"
);
assert_eq!(
grant_token("http_fetch", &json!({"url": ""}), None, &facts),
None,
"empty url"
);
assert_eq!(
grant_token("http_fetch", &json!({"url": 7}), None, &facts),
None,
"non-string url"
);
assert_eq!(
grant_token(
"http_download",
&json!({"destination": "f.bin"}),
None,
&facts
),
None,
"download without url"
);
assert_eq!(
grant_token("run_program", &json!({}), None, &facts),
None,
"no program"
);
assert_eq!(
grant_token("run_program", &json!({"program": ""}), None, &facts),
None,
"empty program"
);
assert_eq!(
grant_token(
"run_program",
&json!({"program": "/usr/bin/env"}),
None,
&facts
),
None,
"paths are never programs"
);
assert_eq!(
grant_token("run_program", &json!({"program": ".."}), None, &facts),
None,
"traversal is never a program"
);
assert_eq!(
grant_token("run_program", &json!({"program": 7}), None, &facts),
None,
"non-string program"
);
}
#[test]
fn run_program_s_family_rule_is_the_run_engine_s() {
let both_doors = facts_with_runner(runner_facts(
&["ripgrep", "ls", "git"],
&["python3", "python", "bash", "sh", "node", "script"],
));
for (program, family) in [
("python3", "interpreter"),
("python", "interpreter"),
("bash", "interpreter"),
("sh", "interpreter"),
("node", "interpreter"),
("ripgrep", "runner"),
("ls", "runner"),
("git", "runner"),
("script", "interpreter"),
] {
assert_eq!(
token_for("run_program", json!({"program": program}), &both_doors),
format!("{family}:{program}"),
"the family must match the run engine's refusal list"
);
}
}
#[test]
fn the_answers_line_names_the_token_only_when_one_exists() {
assert_eq!(
session_answers_line(Some("workspace-write")),
"[a] allow once [s] allow workspace-write for this session [d] deny",
"the token is named verbatim — the word the grant records"
);
let without = session_answers_line(None);
assert_eq!(
without, "[a] allow once [d] deny (no session grant for this tool)",
"with no token the line offers two answers and says so"
);
}
#[test]
fn an_unstaged_interpreter_is_never_offered() {
let runner_composed = facts_with_runner(runner_facts(&["bench"], &[]));
assert_eq!(
grant_token(
"run_program",
&json!({"program": "python3", "args": ["-c", "print(1)"]}),
None,
&runner_composed
),
None,
"an unstaged interpreter is never offered — `[jobs.interpreter]` \
carries nothing it could run"
);
let answers = session_answers_line(None);
assert!(
!answers.contains("[s]"),
"no token, no session-grant offer: {answers}"
);
assert!(
answers.contains("(no session grant for this tool)"),
"the two-answer line says why the third is absent: {answers}"
);
}
#[test]
fn a_runner_program_outside_the_allowlist_is_never_offered() {
let runner_composed = facts_with_runner(runner_facts(&["bench"], &[]));
assert_eq!(
grant_token(
"run_program",
&json!({"program": "deploy"}),
None,
&runner_composed
),
None,
"a program outside the composed [jobs.runner] allow is never offered"
);
assert_eq!(
grant_token(
"run_program",
&json!({"program": "bench"}),
None,
&ApprovalFacts::default()
),
None,
"no composed runner, no runner offer at all"
);
}
#[test]
fn a_workspace_write_call_with_no_workspace_root_is_never_offered() {
assert_eq!(
grant_token(
"workspace_write",
&json!({"path": "notes.md", "content": "hello"}),
None,
&ApprovalFacts::default()
),
None,
"no workspace root, no workspace-write offer"
);
assert_eq!(
grant_token(
"scratch_sql",
&json!({"sql": "SELECT 1"}),
None,
&ApprovalFacts::default()
),
None,
"no scratch member, no scratch offer"
);
assert_eq!(
grant_token(
"http_fetch",
&json!({"url": "https://a.example/x"}),
None,
&ApprovalFacts::default()
),
None,
"no fetch member, no fetch offer"
);
}
#[test]
fn a_composed_staged_interpreter_is_still_offered() {
let staged = facts_with_runner(runner_facts(&["bench"], &["python3"]));
assert_eq!(
grant_token(
"run_program",
&json!({"program": "python3", "args": ["-c", "print(1)"]}),
None,
&staged
),
Some("interpreter:python3".to_owned()),
"a staged interpreter is offered — the honest case must not go silent"
);
assert_eq!(
grant_token("run_program", &json!({"program": "bench"}), None, &staged),
Some("runner:bench".to_owned()),
"an allowlisted runner program is offered"
);
}