use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::context::ContextBudget;
use crate::error::{Error, Result};
use crate::mcp::McpServer;
use crate::policy::{Defaults, Effect, Layer, Policy};
use crate::pricing::{Price, PriceTable};
use crate::resilience::{RetryPolicy, StallPolicy};
use crate::sandbox::SandboxConfig;
use crate::toolchain::Toolchain;
use crate::tools::git::Identity;
use crate::web::WebAccess;
use crate::TaskContract;
pub const PROJECT_FILE: &str = "io.toml";
pub const LOCAL_FILE: &str = "io.local.toml";
pub const CONFIG_HOME_VAR: &str = "IO_CONFIG_HOME";
pub const CONFIG_VAR: &str = "IO_CONFIG";
const DEFAULT_INSTRUCTIONS: &[&str] = &["AGENTS.md"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
User,
Project,
Local,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct File {
#[serde(default)]
policy: Option<PolicySection>,
#[serde(default)]
sandbox: Option<SandboxSection>,
#[serde(default)]
run: Option<RunSection>,
#[serde(default)]
toolchain: BTreeMap<String, ToolchainSection>,
#[serde(default)]
prices: Option<PricesSection>,
#[serde(default)]
mcp: Vec<McpServer>,
#[serde(default)]
agent: Vec<crate::agent::AgentDef>,
#[serde(default)]
web: Option<WebAccess>,
#[serde(default)]
provider: Vec<ProviderSpec>,
#[serde(default)]
app: Option<toml::value::Table>,
#[serde(default)]
profile: BTreeMap<String, File>,
#[serde(default)]
instructions: Option<InstructionsSection>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", deny_unknown_fields)]
#[non_exhaustive]
pub enum ProviderSpec {
#[serde(rename = "openrouter")]
OpenRouter {
model: String,
#[serde(default)]
api_key: Option<String>,
},
#[serde(rename = "anthropic")]
Anthropic {
model: String,
#[serde(default)]
api_key: Option<String>,
},
#[serde(rename = "openai")]
OpenAi {
model: String,
#[serde(default)]
api_key: Option<String>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct InstructionsSection {
files: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PolicySection {
#[serde(default)]
defaults: Option<DefaultsSection>,
#[serde(default)]
layers: Vec<Layer>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct DefaultsSection {
read: Option<Effect>,
write: Option<Effect>,
exec: Option<Effect>,
net: Option<Effect>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SandboxSection {
#[serde(default)]
limits: Option<LimitsSection>,
allow_network: Option<bool>,
force_floor: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct LimitsSection {
max_cpu_secs: Option<u64>,
max_wall_secs: Option<u64>,
max_memory_bytes: Option<u64>,
max_processes: Option<u64>,
max_open_files: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RunSection {
max_steps: Option<u32>,
max_duration_secs: Option<u64>,
max_tokens: Option<u64>,
max_retries: Option<u32>,
exec_timeout_secs: Option<u64>,
skills: Option<PathBuf>,
templates: Option<PathBuf>,
retry: Option<RetryPolicy>,
stall: Option<StallPolicy>,
context: Option<ContextBudget>,
commit_identity: Option<Identity>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ToolchainSection {
manager: Option<String>,
install: Option<Vec<String>>,
build: Option<Vec<String>>,
test: Option<Vec<String>>,
lint: Option<Vec<String>>,
format: Option<Vec<String>>,
run: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PricesSection {
as_of: String,
#[serde(default)]
models: BTreeMap<String, Price>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
file: File,
sources: Vec<(Scope, PathBuf)>,
raw: toml::value::Table,
instructions: Vec<String>,
}
impl Config {
pub fn discover(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref();
let mut candidates: Vec<(Scope, PathBuf)> = Vec::new();
if let Some(user) = user_path() {
candidates.push((Scope::User, user));
}
candidates.push((Scope::Project, root.join(PROJECT_FILE)));
candidates.push((Scope::Local, root.join(LOCAL_FILE)));
let mut merged = toml::value::Table::new();
let mut sources = Vec::new();
for (scope, path) in candidates {
if !path.is_file() {
continue;
}
let table = read_scope(scope, &path)?;
merge(&mut merged, table, &mut Vec::new());
sources.push((scope, path));
}
let file: File = deserialize(toml::Value::Table(merged.clone()), Path::new("<merged>"))?;
let instructions = read_instructions(&file, root)?;
Ok(Self {
file,
sources,
raw: merged,
instructions,
})
}
pub fn from_toml(text: &str) -> Result<Self> {
let path = Path::new(PROJECT_FILE);
let table = parse(Scope::Project, text, path)?;
refuse_widening(&table, path)?;
Ok(Self {
file: deserialize(toml::Value::Table(table.clone()), path)?,
sources: Vec::new(),
raw: table,
instructions: Vec::new(),
})
}
pub fn with_profile(&self, name: &str) -> Result<Self> {
let overlay = self
.raw
.get("profile")
.and_then(toml::Value::as_table)
.and_then(|t| t.get(name))
.and_then(toml::Value::as_table)
.ok_or_else(|| Error::Config(format!("no `[profile.{name}]` in this configuration")))?
.clone();
let mut merged = self.raw.clone();
merged.remove("profile");
merge(&mut merged, overlay, &mut Vec::new());
Ok(Self {
file: deserialize(toml::Value::Table(merged.clone()), Path::new("<profile>"))?,
sources: self.sources.clone(),
raw: merged,
instructions: self.instructions.clone(),
})
}
pub fn sources(&self) -> &[(Scope, PathBuf)] {
&self.sources
}
pub fn is_empty(&self) -> bool {
self.file.policy.is_none()
&& self.file.sandbox.is_none()
&& self.file.run.is_none()
&& self.file.toolchain.is_empty()
&& self.file.prices.is_none()
&& self.file.mcp.is_empty()
&& self.file.agent.is_empty()
&& self.file.web.is_none()
&& self.file.provider.is_empty()
&& self.file.app.is_none()
&& self.file.profile.is_empty()
&& self.file.instructions.is_none()
}
pub fn provider_spec(&self) -> Option<&ProviderSpec> {
self.file.provider.first()
}
pub fn fallback_specs(&self) -> &[ProviderSpec] {
self.file.provider.get(1..).unwrap_or_default()
}
pub fn app<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
let Some(value) = self.file.app.as_ref().and_then(|t| t.get(key)) else {
return Ok(None);
};
value
.clone()
.try_into()
.map(Some)
.map_err(|e: toml::de::Error| Error::Config(format!("`[app.{key}]`: {}", e.message())))
}
pub fn instructions(&self) -> &[String] {
&self.instructions
}
pub fn policy(&self) -> Option<Policy> {
let section = self.file.policy.as_ref()?;
let base = Policy::default();
let defaults = match §ion.defaults {
None => base.defaults,
Some(d) => Defaults {
read: d.read.unwrap_or(base.defaults.read),
write: d.write.unwrap_or(base.defaults.write),
exec: d.exec.unwrap_or(base.defaults.exec),
net: d.net.unwrap_or(base.defaults.net),
},
};
let mut layers = base.layers;
layers.extend(section.layers.iter().cloned());
Some(Policy { layers, defaults })
}
pub fn sandbox(&self) -> Option<SandboxConfig> {
let section = self.file.sandbox.as_ref()?;
let mut config = SandboxConfig::new();
if let Some(limits) = §ion.limits {
let base = &mut config.limits;
for (from, to) in [
(limits.max_cpu_secs, &mut base.max_cpu_secs),
(limits.max_wall_secs, &mut base.max_wall_secs),
(limits.max_memory_bytes, &mut base.max_memory_bytes),
(limits.max_processes, &mut base.max_processes),
(limits.max_open_files, &mut base.max_open_files),
] {
if let Some(v) = from {
*to = if v == 0 { None } else { Some(v) };
}
}
}
if let Some(v) = section.allow_network {
config.allow_network = v;
}
if let Some(v) = section.force_floor {
config.force_floor = v;
}
Some(config)
}
pub fn prices(&self) -> Option<PriceTable> {
let section = self.file.prices.as_ref()?;
let mut table = PriceTable::new(section.as_of.clone());
for (model, price) in §ion.models {
table = table.with(model.clone(), *price);
}
Some(table)
}
pub fn toolchain(&self, detected: Toolchain) -> Toolchain {
let Some(section) = self.file.toolchain.get(&detected.ecosystem) else {
return detected;
};
let mut out = detected;
if let Some(v) = §ion.manager {
out.manager = v.clone();
}
for (from, to) in [
(§ion.install, &mut out.install),
(§ion.build, &mut out.build),
(§ion.test, &mut out.test),
(§ion.lint, &mut out.lint),
(§ion.format, &mut out.format),
(§ion.run, &mut out.run),
] {
if let Some(v) = from {
*to = v.clone();
}
}
out
}
pub fn mcp_servers(&self) -> &[McpServer] {
&self.file.mcp
}
pub fn agents(&self) -> crate::agent::Agents {
self.file
.agent
.iter()
.cloned()
.fold(crate::agent::Agents::new(), |roster, def| roster.with(def))
}
pub fn templates(&self) -> Option<&std::path::Path> {
self.file.run.as_ref().and_then(|r| r.templates.as_deref())
}
#[must_use]
pub fn apply_to(&self, contract: TaskContract) -> TaskContract {
let mut out = contract;
if !self.file.mcp.is_empty() {
out = out.with_mcp(self.file.mcp.iter().cloned());
}
if !self.file.agent.is_empty() {
out = out.with_agents(self.agents());
}
if let Some(web) = &self.file.web {
out = out.with_web(web.clone());
}
for instruction in &self.instructions {
out = out.with_constraint(instruction.clone());
}
let Some(run) = &self.file.run else {
return out;
};
if let Some(v) = run.max_steps {
out = out.with_max_steps(v);
}
if let Some(v) = run.max_duration_secs {
out = out.with_time_budget(Duration::from_secs(v));
}
if let Some(v) = run.max_tokens {
out = out.with_token_budget(v);
}
if let Some(v) = run.max_retries {
out = out.with_max_retries(v);
}
if let Some(v) = run.exec_timeout_secs {
out = out.with_exec_timeout(Duration::from_secs(v));
}
if let Some(v) = &run.skills {
out = out.with_skills(v.clone());
}
if let Some(v) = run.retry {
out = out.with_retry_policy(v);
}
if let Some(v) = run.stall {
out = out.with_stall_policy(v);
}
if let Some(v) = run.context {
out = out.with_context_budget(v);
}
if let Some(v) = &run.commit_identity {
out = out.with_commit_identity(v.name.clone(), v.email.clone());
}
out
}
}
#[must_use]
pub fn user_path() -> Option<PathBuf> {
if let Some(file) = env_dir(CONFIG_VAR) {
return Some(file);
}
if let Some(dir) = env_dir(CONFIG_HOME_VAR) {
return Some(dir.join(PROJECT_FILE));
}
#[cfg(windows)]
let base = env_dir("APPDATA")?;
#[cfg(not(windows))]
let base = match env_dir("XDG_CONFIG_HOME") {
Some(dir) => dir,
None => env_dir("HOME")?.join(".config"),
};
Some(base.join("io").join(PROJECT_FILE))
}
fn env_dir(var: &str) -> Option<PathBuf> {
let value = std::env::var_os(var)?;
if value.is_empty() {
return None;
}
Some(PathBuf::from(value))
}
fn read_scope(scope: Scope, path: &Path) -> Result<toml::value::Table> {
let text = std::fs::read_to_string(path)
.map_err(|e| Error::Config(format!("{}: {e}", path.display())))?;
let table = parse(scope, &text, path)?;
if scope == Scope::Project {
refuse_widening(&table, path)?;
}
let file: File = deserialize(toml::Value::Table(table.clone()), path)?;
refuse_nested_profiles(&file, path)?;
Ok(table)
}
fn parse(scope: Scope, text: &str, path: &Path) -> Result<toml::value::Table> {
let mut table: toml::value::Table = toml::from_str(text)
.map_err(|e| Error::Config(format!("{}: {}", path.display(), e.message())))?;
let dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
let mut key = Vec::new();
for (k, v) in table.iter_mut() {
key.push(k.clone());
substitute(scope, v, &dir, &mut key, path)?;
key.pop();
}
Ok(table)
}
const PROJECT_WIDENING: &[(&[&str], &str)] = &[
(&["policy", "defaults", "exec"], "allow"),
(&["policy", "defaults", "net"], "allow"),
(&["sandbox", "allow_network"], "true"),
(&["sandbox", "force_floor"], "false"),
];
fn refuse_widening(table: &toml::value::Table, path: &Path) -> Result<()> {
for (keys, widening) in PROJECT_WIDENING {
let mut node = table.get(keys[0]);
for key in &keys[1..] {
node = node
.and_then(toml::Value::as_table)
.and_then(|t| t.get(*key));
}
let Some(value) = node else { continue };
let written = match value {
toml::Value::String(s) => s.as_str(),
toml::Value::Boolean(true) => "true",
toml::Value::Boolean(false) => "false",
_ => continue,
};
if written == *widening {
return Err(Error::Config(format!(
"{}: key `{}`: `{written}` widens the boundary, and a project-scoped \
file may narrow it and never widen it. Write it in `{LOCAL_FILE}` or \
the user-scope file instead.",
path.display(),
keys.join(".")
)));
}
}
if let Some(profiles) = table.get("profile").and_then(toml::Value::as_table) {
for body in profiles.values().filter_map(toml::Value::as_table) {
refuse_widening(body, path)?;
}
}
Ok(())
}
fn refuse_nested_profiles(file: &File, path: &Path) -> Result<()> {
for (name, body) in &file.profile {
if !body.profile.is_empty() {
return Err(Error::Config(format!(
"{}: key `profile.{name}.profile`: a profile may not contain profiles",
path.display()
)));
}
}
Ok(())
}
fn read_instructions(file: &File, root: &Path) -> Result<Vec<String>> {
let Some(section) = &file.instructions else {
return Ok(Vec::new());
};
let names = match §ion.files {
Some(files) => files.clone(),
None => DEFAULT_INSTRUCTIONS.iter().map(PathBuf::from).collect(),
};
let mut out = Vec::new();
for name in names {
let at = root.join(&name);
if !at.is_file() {
continue;
}
let text = std::fs::read_to_string(&at)
.map_err(|e| Error::Config(format!("{}: {e}", at.display())))?;
let text = text.trim();
if text.is_empty() {
continue;
}
out.push(format!(
"Project instructions from `{}`:\n{text}",
name.display()
));
}
Ok(out)
}
fn deserialize(value: toml::Value, path: &Path) -> Result<File> {
value
.try_into()
.map_err(|e: toml::de::Error| Error::Config(format!("{}: {}", path.display(), e.message())))
}
fn substitute(
scope: Scope,
value: &mut toml::Value,
dir: &Path,
key: &mut Vec<String>,
path: &Path,
) -> Result<()> {
match value {
toml::Value::String(s) => *s = expand(scope, s, dir, key, path)?,
toml::Value::Array(items) => {
for (i, item) in items.iter_mut().enumerate() {
key.push(format!("[{i}]"));
substitute(scope, item, dir, key, path)?;
key.pop();
}
}
toml::Value::Table(table) => {
for (k, v) in table.iter_mut() {
key.push(k.clone());
substitute(scope, v, dir, key, path)?;
key.pop();
}
}
_ => {}
}
Ok(())
}
fn run_command(argv: &str) -> std::result::Result<String, String> {
let mut parts = argv.split_whitespace();
let Some(program) = parts.next() else {
return Err("`${cmd:}` names no program".to_string());
};
let output = std::process::Command::new(program)
.args(parts)
.output()
.map_err(|e| format!("cannot run `{program}`: {e}"))?;
if !output.status.success() {
return Err(format!("`{program}` exited with {}", output.status));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn expand(scope: Scope, raw: &str, dir: &Path, key: &[String], path: &Path) -> Result<String> {
let mut out = String::new();
let mut rest = raw;
while let Some(at) = rest.find("${") {
out.push_str(&rest[..at]);
let after = &rest[at + 2..];
let Some(end) = after.find('}') else {
return Err(bad_key(path, key, "unterminated `${` substitution"));
};
let inner = &after[..end];
let Some((kind, arg)) = inner.split_once(':') else {
return Err(bad_key(
path,
key,
format!("`${{{inner}}}` names neither `env:` nor `file:`"),
));
};
let value = match kind {
"env" => std::env::var(arg).map_err(|_| {
bad_key(
path,
key,
format!("environment variable `{arg}` is not set"),
)
})?,
"file" => {
let at = dir.join(arg);
std::fs::read_to_string(&at)
.map_err(|e| {
bad_key(path, key, format!("cannot read `{}`: {e}", at.display()))
})?
.trim()
.to_string()
}
"cmd" => {
if scope == Scope::Project {
return Err(bad_key(
path,
key,
format!(
"`${{cmd:}}` is refused in the project scope, because `{PROJECT_FILE}` \
travels with a clone. Write it in `{LOCAL_FILE}` or the user-scope \
file instead."
),
));
}
run_command(arg).map_err(|e| bad_key(path, key, e))?
}
other => {
return Err(bad_key(
path,
key,
format!("`{other}:` is not a substitution this crate knows"),
))
}
};
if value.is_empty() {
return Err(bad_key(
path,
key,
format!("`${{{inner}}}` resolved to nothing, and an empty value is never what a config meant"),
));
}
out.push_str(&value);
rest = &after[end + 1..];
}
out.push_str(rest);
Ok(out)
}
fn bad_key(path: &Path, key: &[String], why: impl std::fmt::Display) -> Error {
Error::Config(format!(
"{}: key `{}`: {why}",
path.display(),
key.join(".")
))
}
const APPENDING: &[&[&str]] = &[&["policy", "layers"], &["agent"]];
fn merge(base: &mut toml::value::Table, over: toml::value::Table, at: &mut Vec<String>) {
for (key, value) in over {
at.push(key.clone());
match (base.get_mut(&key), value) {
(Some(toml::Value::Table(b)), toml::Value::Table(o)) => merge(b, o, at),
(Some(toml::Value::Array(b)), toml::Value::Array(o))
if APPENDING.iter().any(|p| p == &at.as_slice()) =>
{
b.extend(o);
}
(_, value) => {
base.insert(key.clone(), value);
}
}
at.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table(text: &str) -> toml::value::Table {
toml::from_str(text).unwrap()
}
#[test]
fn a_later_scope_wins_one_key_and_leaves_its_siblings() {
let mut base = table("[sandbox.limits]\nmax_wall_secs = 120\nmax_cpu_secs = 60\n");
merge(
&mut base,
table("[sandbox.limits]\nmax_wall_secs = 5\n"),
&mut Vec::new(),
);
let limits = base["sandbox"]["limits"].as_table().unwrap();
assert_eq!(limits["max_wall_secs"].as_integer(), Some(5));
assert_eq!(
limits["max_cpu_secs"].as_integer(),
Some(60),
"a sibling key the later scope never named is not disturbed"
);
}
#[test]
fn policy_layers_append_and_every_other_array_replaces() {
let mut base = table(
"[[policy.layers]]\nname = \"ops\"\nrules = []\n\
[toolchain.cargo]\ntest = [\"cargo\", \"test\"]\n",
);
merge(
&mut base,
table(
"[[policy.layers]]\nname = \"mine\"\nrules = []\n\
[toolchain.cargo]\ntest = [\"cargo\", \"nextest\", \"run\"]\n",
),
&mut Vec::new(),
);
let layers = base["policy"]["layers"].as_array().unwrap();
assert_eq!(layers.len(), 2, "layers append");
assert_eq!(layers[0]["name"].as_str(), Some("ops"));
assert_eq!(layers[1]["name"].as_str(), Some("mine"));
let test = base["toolchain"]["cargo"]["test"].as_array().unwrap();
assert_eq!(test.len(), 3, "an ordinary array is replaced whole");
}
#[test]
fn substitution_resolves_or_fails_and_never_empties() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("token"), " s3cret\n").unwrap();
std::env::set_var("IO_HARNESS_TEST_SET", "from-the-environment");
std::env::set_var("IO_HARNESS_TEST_EMPTY", "");
let key = ["run".to_string()];
let path = Path::new("io.toml");
let at = Scope::Local;
assert_eq!(
expand(at, "${env:IO_HARNESS_TEST_SET}", dir.path(), &key, path).unwrap(),
"from-the-environment"
);
assert_eq!(
expand(at, "Bearer ${file:token}", dir.path(), &key, path).unwrap(),
"Bearer s3cret",
"a file's value is trimmed, and substitution is inside a larger string"
);
for (input, expect) in [
("${env:IO_HARNESS_TEST_UNSET}", "is not set"),
("${env:IO_HARNESS_TEST_EMPTY}", "resolved to nothing"),
("${file:absent}", "cannot read"),
("${nope:x}", "not a substitution"),
("${env:X", "unterminated"),
] {
let err = expand(at, input, dir.path(), &key, path)
.unwrap_err()
.to_string();
assert!(err.contains(expect), "{input}: {err}");
assert!(err.contains("key `run`"), "the error names the key: {err}");
}
assert_eq!(
expand(at, "plain $HOME value", dir.path(), &key, path).unwrap(),
"plain $HOME value"
);
}
#[test]
fn a_command_substitution_runs_outside_the_project_scope_and_never_inside_it() {
#[cfg(windows)]
let (echo, quiet, fail) = ("cmd /c echo s3cret", "cmd /c rem", "cmd /c exit 1");
#[cfg(not(windows))]
let (echo, quiet, fail) = ("printf s3cret", "true", "false");
let dir = tempfile::tempdir().unwrap();
let key = ["mcp".to_string()];
let path = Path::new("io.local.toml");
assert_eq!(
expand(
Scope::Local,
&format!("Bearer ${{cmd:{echo}}}"),
dir.path(),
&key,
path
)
.unwrap(),
"Bearer s3cret"
);
for (input, expect) in [
(format!("${{cmd:{fail}}}"), "exited with"),
(
"${cmd:io-harness-no-such-program}".to_string(),
"cannot run",
),
(format!("${{cmd:{quiet}}}"), "resolved to nothing"),
] {
let input = input.as_str();
let err = expand(Scope::Local, input, dir.path(), &key, path)
.unwrap_err()
.to_string();
assert!(err.contains(expect), "{input}: {err}");
}
let err = expand(
Scope::Project,
&format!("${{cmd:{echo}}}"),
dir.path(),
&key,
Path::new(PROJECT_FILE),
)
.unwrap_err()
.to_string();
assert!(err.contains("refused in the project scope"), "{err}");
std::env::set_var("IO_HARNESS_TEST_SET", "from-the-environment");
assert_eq!(
expand(
Scope::Project,
"${env:IO_HARNESS_TEST_SET}",
dir.path(),
&key,
Path::new(PROJECT_FILE),
)
.unwrap(),
"from-the-environment"
);
}
}