diff --git a/host/js/core-ext-test-bundle.js b/host/js/core-ext-test-bundle.js index eb3772c..baf8894 100644 --- a/host/js/core-ext-test-bundle.js +++ b/host/js/core-ext-test-bundle.js @@ -38402,7 +38402,6 @@ async function readTestConfig(fs2) { } async function writeTestConfig(fs2, value2) { const { roster, ...config } = value2; - await fs2.mkdir(directory); for (const entry2 of await fs2.list(directory)) if (entry2.name.endsWith(".md")) await fs2.remove(`${directory}/${entry2.name}`); for (const entry2 of roster) { const { name, prompt, path: _path, ...fields } = entry2; diff --git a/host/js/p3-test-bundle.js b/host/js/p3-test-bundle.js index 9e6486f..a91318e 100644 --- a/host/js/p3-test-bundle.js +++ b/host/js/p3-test-bundle.js @@ -38360,7 +38360,7 @@ async function openStorage(path) { var textOf = (content) => typeof content === "string" ? content : (content ?? []).filter((part) => part.type === "text").map((part) => part.text).join("\n"); function answerFor(rules, transcript) { const system = JSON.stringify(transcript.messages.filter((message) => message.role === "system")); - const role = /P3 role:([a-z]+)/.exec(system)?.[1]; + const role = /P3 role:([a-zA-Z0-9_-]+)/.exec(system)?.[1]; const last = transcript.messages.findLast((message) => message.role !== "system"); const rule = rules.find((rule2) => rule2.role === role && (!rule2.match.kind || rule2.match.kind === last?.role) && textOf(last?.content).includes(rule2.match.text)); if (!rule) throw new Error(`No scripted rule for ${role}: ${JSON.stringify(last)}`); diff --git a/host/js/roster-fixture.ts b/host/js/roster-fixture.ts index aeb90f0..84d2028 100644 --- a/host/js/roster-fixture.ts +++ b/host/js/roster-fixture.ts @@ -15,7 +15,6 @@ export async function readTestConfig(fs:any) { } export async function writeTestConfig(fs:any, value:any) { const {roster,...config}=value; - await fs.mkdir(directory); for (const entry of await fs.list(directory)) if (entry.name.endsWith('.md')) await fs.remove(`${directory}/${entry.name}`); for (const entry of roster) { const {name,prompt,path: _path,...fields}=entry; diff --git a/host/js/scripted-model.ts b/host/js/scripted-model.ts index 3c07f5d..2a64d72 100644 --- a/host/js/scripted-model.ts +++ b/host/js/scripted-model.ts @@ -5,7 +5,7 @@ export type Rule = { id: string; role: string; match: { kind?: string; text: str export const textOf = (content: any): string => typeof content === 'string' ? content : (content ?? []).filter((part: any) => part.type === 'text').map((part: any) => part.text).join('\n'); export function answerFor(rules: Rule[], transcript: any) { const system = JSON.stringify(transcript.messages.filter((message: any) => message.role === 'system')); - const role = /P3 role:([a-z]+)/.exec(system)?.[1]; + const role = /P3 role:([a-zA-Z0-9_-]+)/.exec(system)?.[1]; const last = transcript.messages.findLast((message: any) => message.role !== 'system'); const rule = rules.find(rule => rule.role === role && (!rule.match.kind || rule.match.kind === last?.role) && textOf(last?.content).includes(rule.match.text)); if (!rule) throw new Error(`No scripted rule for ${role}: ${JSON.stringify(last)}`); diff --git a/host/js/subagents-test-bundle.js b/host/js/subagents-test-bundle.js index 7d72f6d..38c20c2 100644 --- a/host/js/subagents-test-bundle.js +++ b/host/js/subagents-test-bundle.js @@ -37462,7 +37462,6 @@ async function readTestConfig(fs2) { } async function writeTestConfig(fs2, value2) { const { roster, ...config } = value2; - await fs2.mkdir(directory); for (const entry2 of await fs2.list(directory)) if (entry2.name.endsWith(".md")) await fs2.remove(`${directory}/${entry2.name}`); for (const entry2 of roster) { const { name, prompt, path: _path, ...fields } = entry2; diff --git a/host/src/harness/mod.rs b/host/src/harness/mod.rs index f224a4f..f94740c 100644 --- a/host/src/harness/mod.rs +++ b/host/src/harness/mod.rs @@ -148,7 +148,8 @@ pub async fn run_module_with_input( receiver: input::Receiver, source: &str, ) -> anyhow::Result<()> { - let provider_config = config::Config::read(&world.root).await?; + let config_root = world.root.clone(); + let provider_config = config::Config::read(&config_root).await?; let loader = std::rc::Rc::new(loader::Loader { root: world.root.clone(), modules: Default::default(), @@ -232,9 +233,14 @@ pub async fn run_module_with_input( Ok::<_, anyhow::Error>(()) } .await; + let current_config = config::Config::read(&config_root).await.unwrap_or_default(); result.map_err(|error| { let mut text = error.to_string(); - for provider in provider_config.providers.values() { + for provider in provider_config + .providers + .values() + .chain(current_config.providers.values()) + { if !provider.api_key.is_empty() { text = text.replace(&provider.api_key, "[redacted]"); } diff --git a/host/tests/it/dream.rs b/host/tests/it/dream.rs index 8a75d93..43c6b1f 100644 --- a/host/tests/it/dream.rs +++ b/host/tests/it/dream.rs @@ -50,23 +50,17 @@ fn prepare(dir: &Path) -> anyhow::Result<()> { repo.join("extensions/probe/server.ts"), "export default host => { globalThis.dreamHost=host; globalThis.dreamService=host.need({name:'dream'}); globalThis.dreamSubagents=host.need({name:'subagents'}); globalThis.dreamAutomations=host.need({name:'automations'}); return {name:'probe'}; }", )?; - let mut roster: Vec = ["mainAgent", "coordinator", "worker", "reviewer"].into_iter().map(|role| serde_json::json!({ + let roster: Vec = ["mainAgent", "coordinator", "worker", "reviewer"].into_iter().map(|role| serde_json::json!({ "name":role, "description":format!("P3 {role}"), "model":"faux/faux-1", "isolate":"none", "resident":role == "mainAgent", "prompt":format!("P3 role:{role}"), "denyTools":if role == "worker" || role == "reviewer" {vec!["subagent"]} else {vec![]} })).collect(); - let defaults: serde_json::Value = - serde_yaml_ng::from_str(include_str!("../../../config/config.yaml"))?; - let mut pass = defaults["roster"] - .as_array() - .unwrap() - .iter() - .find(|entry| entry["name"] == "dream-pass") - .unwrap() - .clone(); - pass["model"] = serde_json::json!("faux/faux-1"); - roster.push(pass); crate::common::write_config(dir, "mainAgent", serde_json::json!(roster))?; + std::fs::write( + disk.join("etc/sausage/roster/dream-pass.md"), + include_str!("../../../config/roster/dream-pass.md") + .replace("openai/gpt-6.1-sol", "faux/faux-1"), + )?; let demo = disk.join("src/demo"); std::fs::create_dir_all(&demo)?; std::fs::write(demo.join("README.md"), "P3 demo\n")?; diff --git a/host/tests/it/durable.rs b/host/tests/it/durable.rs index a826355..f7d1f21 100644 --- a/host/tests/it/durable.rs +++ b/host/tests/it/durable.rs @@ -146,7 +146,7 @@ async fn provider_child() -> anyhow::Result<()> { }; let dir = std::path::Path::new(&dir); let source = format!( - "globalThis.providerOnly = true; const provider = (await Deno.core.ops.op_model_providers()).test; if (provider.base_url !== 'https://provider.invalid' || provider.api_key !== 'faux-private-token') throw new Error('Wrong provider config');\n{}", + "globalThis.providerOnly = true; const configuredTestProvider = (await Deno.core.ops.op_model_providers()).test; if (configuredTestProvider.base_url !== 'https://provider.invalid' || configuredTestProvider.api_key !== 'faux-private-token') throw new Error('Wrong provider config');\n{}", include_str!("../../js/durable-test-bundle.js") ); run_module( @@ -158,11 +158,15 @@ async fn provider_child() -> anyhow::Result<()> { let error = run_module( World::open(dir)?, sausage::process::exec(World::open(dir)?)?, - "throw new Error((await Deno.core.ops.op_model_providers()).test.api_key)", + r#"const changedProviderConfig = await Deno.core.ops.op_model_providers(); + changedProviderConfig.test.api_key = 'next-private-token'; + await Deno.core.ops.op_file('write', '/etc/sausage/config.yaml', [...new TextEncoder().encode(JSON.stringify({providers:changedProviderConfig}))], null); + throw new Error(changedProviderConfig.test.api_key + ' faux-private-token')"#, ) .await .unwrap_err(); assert!(!error.to_string().contains("faux-private-token")); + assert!(!error.to_string().contains("next-private-token")); assert!(error.to_string().contains("[redacted]")); Ok(()) } @@ -172,16 +176,16 @@ fn provider_config_ignores_environment() -> anyhow::Result<()> { let dir = std::env::temp_dir().join(format!("sausage-durable-provider-{}", std::process::id())); let settings = dir.join("world/etc/sausage"); std::fs::create_dir_all(&settings)?; - std::fs::write( - settings.join("config.yaml"), - "providers:\n test:\n base_url: https://provider.invalid\n api_key: faux-private-token\n", - )?; for start in 0..2 { + std::fs::write( + settings.join("config.yaml"), + "providers:\n test:\n base_url: https://provider.invalid\n api_key: faux-private-token\n", + )?; let status = std::process::Command::new(std::env::current_exe()?) .args(["--exact", "durable::provider_child", "--nocapture"]) .env("DURABLE_PROVIDER_DIR", &dir) .env( - "PI_PROVIDER_BASE_URL", + "OPENAI_BASE_URL", if start == 0 { "https://provider.invalid" } else { @@ -189,7 +193,7 @@ fn provider_config_ignores_environment() -> anyhow::Result<()> { }, ) .env( - "PI_PROVIDER_API_KEY", + "OPENAI_API_KEY", if start == 0 { "faux-private-token" } else { @@ -200,13 +204,7 @@ fn provider_config_ignores_environment() -> anyhow::Result<()> { assert!(status.success()); } let cred = Cred::open(&dir.join("host"))?; - assert_eq!( - cred.item_list()? - .iter() - .filter(|i| i.name == "llm-provider") - .count(), - 0 - ); + assert!(cred.item_list()?.is_empty()); for file in [ "host/cred.sqlite", "host/cred.sqlite-wal",