mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
fix(swarm): route declared env sources and default private workdirs
A {env: VAR} source reached every harness, not only the agents that
declare it: sibling processes inherited a per-agent credential from the
parent environment. Remove every declared source (agent env, auth_tag,
and raw defaults, including disabled agents) from the inherited
environment; declarers still receive the value under the declared name.
Agents defaulted to the config directory as workdir, which is where
generated keys/ live, so each shell-capable agent started beside its
siblings' private keys. Default to workspaces/<name> beside the config,
created 0700 on start and reported by validate. The default now lives
only in the plan.
README: state exactly what shutdown cleans up. Swarm kills the harness's
own process group; buzz-acp moves agents into their own groups and stops
them on SIGTERM, so a forced shutdown or harness crash relies on host
containment.
Also build Secret directly and format executable().
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -53,7 +53,8 @@ Missing agent keys are generated once and saved as `keys/<name>.nsec` beside
|
||||
the config, with mode `0600`. Subsequent runs reuse them. Mount that directory
|
||||
on persistent storage in ephemeral containers, or supply keys from your secret
|
||||
manager. `validate` checks swarm wiring, identities, and the harness executable;
|
||||
it writes no keys and starts nothing. Generated preview identities are temporary.
|
||||
it writes no keys or directories and starts nothing. Generated preview
|
||||
identities are temporary.
|
||||
The runtime validates its own model and ACP settings when started.
|
||||
|
||||
The owner must have access to every target relay, and each relay must support
|
||||
@@ -71,7 +72,7 @@ See [buzz-swarm.example.yaml](buzz-swarm.example.yaml). Agent fields replace
|
||||
| `owner.nsec`, `owner.conditions` | Owner key and optional NIP-OA conditions |
|
||||
| `name`, `nsec`, `auth_tag`, `enabled` | Per-agent identity and selection |
|
||||
| `relays` | Non-empty relay list, shared or per agent |
|
||||
| `workdir` | Working directory, defaulting to the config directory |
|
||||
| `workdir` | Existing working directory; default `workspaces/<name>` beside the config, created `0700` on start |
|
||||
| `harness` | Executable path or name on PATH, default `buzz-acp` |
|
||||
| `restart`, `max_restarts` | `on-failure` (default), `always`, or `never`; default budget 10 |
|
||||
| `env` | Existing ACP/agent options and credentials |
|
||||
@@ -100,9 +101,11 @@ Swarm defaults `BUZZ_ACP_AGENT_COMMAND` to `buzz-agent` and
|
||||
ACP runtime; an empty MCP command disables it. See `buzz-acp --help` and
|
||||
[buzz-agent](../buzz-agent/README.md) for runtime options. Ambient `BUZZ_ACP_*`
|
||||
settings are cleared; use YAML `env` to configure them explicitly. Model/provider
|
||||
environment variables are inherited. Swarm owns relay and identity variables;
|
||||
YAML `env` cannot override them. Owner and sibling agent key source variables
|
||||
are removed before launching each harness. Processes sharing an OS user still
|
||||
environment variables are inherited unless the file names them as a source.
|
||||
Swarm owns relay and identity variables; YAML `env` cannot override them. Every
|
||||
`{env: VARIABLE}` source in the file, including in `defaults` and disabled
|
||||
agents, is removed from the inherited environment; only agents that declare it
|
||||
receive its value, under the declared name. Processes sharing an OS user still
|
||||
share that user's filesystem access.
|
||||
|
||||
An existing agent key and pre-signed `auth_tag` can be supplied without `owner`,
|
||||
@@ -119,11 +122,13 @@ second backoff; the restart budget resets after 60 seconds of uptime. Clean
|
||||
exits stay ended under the default policy. Final failures produce exit code 1.
|
||||
|
||||
Ctrl-C or SIGTERM stops harness process groups with 15 seconds of grace
|
||||
(`start --grace SECONDS`). A second signal forces shutdown. Remaining group
|
||||
members are killed before their leader is reaped, including on natural exit.
|
||||
Custom runtimes must manage subprocesses they move into other process groups;
|
||||
container/service containment is the backstop. Workdirs are ordinary directories,
|
||||
not sandboxes.
|
||||
(`start --grace SECONDS`). A second signal forces shutdown. Remaining members of
|
||||
the harness's own group are killed before its leader is reaped, including on
|
||||
natural exit. Swarm does not track processes a harness moves into other groups.
|
||||
The default `buzz-acp` does this for each agent and stops those groups itself on
|
||||
SIGTERM; after a forced shutdown or a harness crash they can outlive Swarm. Run
|
||||
Swarm under container or service containment when that matters. Workdirs are
|
||||
ordinary directories, not sandboxes.
|
||||
|
||||
```sh
|
||||
cargo test -p buzz-swarm
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::config::{expand_tilde, SwarmFile, DEFAULT_CONFIG_FILE};
|
||||
@@ -71,6 +72,9 @@ async fn run(cli: Cli) -> Result<bool> {
|
||||
key.path.display()
|
||||
);
|
||||
}
|
||||
for workdir in &plan.new_workdirs {
|
||||
println!("{}: created on start", workdir.display());
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Command::Start { grace } => {
|
||||
@@ -78,6 +82,13 @@ async fn run(cli: Cli) -> Result<bool> {
|
||||
for key in &plan.generated_keys {
|
||||
key.persist()?;
|
||||
}
|
||||
for workdir in &plan.new_workdirs {
|
||||
std::fs::DirBuilder::new()
|
||||
.recursive(true)
|
||||
.mode(0o700)
|
||||
.create(workdir)
|
||||
.with_context(|| format!("creating workdir {}", workdir.display()))?;
|
||||
}
|
||||
let summary = supervisor::run(
|
||||
plan,
|
||||
Options {
|
||||
|
||||
@@ -147,6 +147,10 @@ impl SecretRef {
|
||||
pub struct Secret(Zeroizing<String>);
|
||||
|
||||
impl Secret {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(Zeroizing::new(value))
|
||||
}
|
||||
|
||||
pub fn expose(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
@@ -238,9 +242,6 @@ impl SwarmFile {
|
||||
if let Some(path) = &mut spec.workdir {
|
||||
*path = self.directory.join(expand_tilde(path, env));
|
||||
}
|
||||
if spec.workdir.is_none() && !self.directory.as_os_str().is_empty() {
|
||||
spec.workdir = Some(self.directory.clone());
|
||||
}
|
||||
if let Some(command) = &mut spec.harness {
|
||||
if command.contains('/') || command.starts_with('~') {
|
||||
*command = self
|
||||
@@ -254,11 +255,15 @@ impl SwarmFile {
|
||||
Ok(specs)
|
||||
}
|
||||
|
||||
/// `defaults:` on its own, including values every agent overrides.
|
||||
pub fn defaults(&self) -> Result<Spec> {
|
||||
serde_yaml::from_value(Value::Mapping(self.defaults.clone())).context("parsing `defaults:`")
|
||||
}
|
||||
|
||||
/// Type-check `defaults:` on its own, then produce one fully merged `Spec`
|
||||
/// per agent entry (agent keys win; `env:` maps merge key-wise).
|
||||
pub fn specs(&self) -> Result<Vec<Spec>> {
|
||||
let defaults: Spec = serde_yaml::from_value(Value::Mapping(self.defaults.clone()))
|
||||
.context("parsing `defaults:`")?;
|
||||
let defaults = self.defaults()?;
|
||||
for (field, present) in [
|
||||
("name", defaults.name.is_some()),
|
||||
("nsec", defaults.nsec.is_some()),
|
||||
|
||||
@@ -132,7 +132,10 @@ agents:
|
||||
matches!(file.owner(&env).unwrap().nsec, SecretRef::File(path) if path == directory.path().join("owner.nsec"))
|
||||
);
|
||||
let specs = file.resolved_specs(&env).unwrap();
|
||||
assert_eq!(specs[0].workdir.as_deref(), Some(directory.path()));
|
||||
assert_eq!(
|
||||
specs[0].workdir, None,
|
||||
"the plan owns the per-agent default"
|
||||
);
|
||||
assert_eq!(
|
||||
Path::new(specs[0].harness.as_deref().unwrap()),
|
||||
directory.path().join("bin/harness")
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Identity {
|
||||
)
|
||||
.context("`auth_tag` does not authorize this agent key")?;
|
||||
Ok(Self {
|
||||
secret: SecretRef::Literal(keys.secret_key().to_bech32()?).resolve(env)?,
|
||||
secret: Secret::new(keys.secret_key().to_bech32()?),
|
||||
pubkey: keys.public_key().to_hex(),
|
||||
owner: delegated_owner.to_hex(),
|
||||
auth_tag,
|
||||
|
||||
@@ -45,6 +45,8 @@ pub struct Plan {
|
||||
pub owner_pubkey: String,
|
||||
pub agents: Vec<AgentPlan>,
|
||||
pub generated_keys: Vec<GeneratedKey>,
|
||||
/// Default workdirs that do not exist yet; created on start.
|
||||
pub new_workdirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
@@ -70,14 +72,26 @@ impl Plan {
|
||||
.filter_map(|source| source.env_key())
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
// A declared source reaches only the agents that declare it, under the
|
||||
// name they declare; no child inherits it ambiently. Raw defaults count
|
||||
// even when every agent overrides them.
|
||||
let defaults = file.defaults()?;
|
||||
let declared_sources = specs
|
||||
.iter()
|
||||
.chain([&defaults])
|
||||
.flat_map(|spec| spec.auth_tag.iter().chain(spec.env.values()))
|
||||
.filter_map(|source| source.env_key())
|
||||
.map(str::to_owned);
|
||||
let env_remove: Vec<_> = env
|
||||
.keys()
|
||||
.filter(|key| key.starts_with("BUZZ_ACP_") || RESERVED.contains(key))
|
||||
.map(str::to_owned)
|
||||
.chain(private_sources.iter().cloned())
|
||||
.chain(declared_sources)
|
||||
.collect();
|
||||
let mut agents = Vec::new();
|
||||
let mut generated_keys = Vec::new();
|
||||
let mut new_workdirs = Vec::new();
|
||||
let mut seen_names = BTreeSet::new();
|
||||
let mut seen_connections = BTreeSet::new();
|
||||
let mut selected = BTreeSet::new();
|
||||
@@ -111,15 +125,22 @@ impl Plan {
|
||||
None => owner_pubkey = Some(identity.owner.clone()),
|
||||
}
|
||||
let program = executable(spec.harness.as_deref().unwrap_or("buzz-acp"), env)?;
|
||||
let workdir = spec
|
||||
.workdir
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.directory.clone());
|
||||
// Default to a private directory per agent, away from `keys/`.
|
||||
// Only that default may be created; an explicit workdir must exist.
|
||||
let workdir = spec.workdir.clone().unwrap_or_else(|| {
|
||||
file.directory
|
||||
.join("workspaces")
|
||||
.join(name.to_ascii_lowercase())
|
||||
});
|
||||
let create = spec.workdir.is_none() && !workdir.try_exists()?;
|
||||
ensure!(
|
||||
workdir.is_dir(),
|
||||
create || workdir.is_dir(),
|
||||
"agent `{name}`: workdir {} is not a directory",
|
||||
workdir.display()
|
||||
);
|
||||
if create {
|
||||
new_workdirs.push(workdir.clone());
|
||||
}
|
||||
let mut child_env = BTreeMap::from([
|
||||
("BUZZ_ACP_AGENT_COMMAND".into(), "buzz-agent".into()),
|
||||
("BUZZ_ACP_MCP_COMMAND".into(), "buzz-dev-mcp".into()),
|
||||
@@ -201,6 +222,7 @@ impl Plan {
|
||||
owner_pubkey: owner_pubkey.context("no agent owner")?,
|
||||
agents,
|
||||
generated_keys,
|
||||
new_workdirs,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -216,10 +238,18 @@ fn executable(name: &str, env: &Env) -> Result<PathBuf> {
|
||||
.map(|directory| directory.join(name))
|
||||
.collect()
|
||||
};
|
||||
candidates.into_iter().find(|path| std::fs::metadata(path)
|
||||
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0))
|
||||
.map(std::path::absolute).transpose()?
|
||||
.with_context(|| format!("cannot find executable `{name}`; install buzz-acp or Sprig's personality symlinks on PATH"))
|
||||
let is_executable = |path: &PathBuf| {
|
||||
std::fs::metadata(path)
|
||||
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
|
||||
};
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(is_executable)
|
||||
.map(std::path::absolute)
|
||||
.transpose()?
|
||||
.with_context(|| {
|
||||
format!("cannot find executable `{name}`; install buzz-acp or Sprig's personality symlinks on PATH")
|
||||
})
|
||||
}
|
||||
|
||||
/// Normalize equivalent relay addresses before checking duplicate identities.
|
||||
|
||||
@@ -89,6 +89,33 @@ fn inherited_identity_and_acp_policy_are_removed_but_provider_env_is_inherited()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_sources_reach_only_their_declarers() {
|
||||
let fixture = Fixture::new();
|
||||
let plan = fixture
|
||||
.plan(
|
||||
" env:\n SHARED: {env: DEFAULT_SOURCE}\n ANTHROPIC_API_KEY: {env: ANTHROPIC_API_KEY}",
|
||||
" - name: alpha\n env:\n SHARED: alpha\n MY_KEY: {env: ALPHA_SOURCE}\n - name: beta\n env:\n SHARED: beta",
|
||||
&Env::from_iter([
|
||||
("DEFAULT_SOURCE", "overridden-everywhere"),
|
||||
("ALPHA_SOURCE", "alpha-only"),
|
||||
("ANTHROPIC_API_KEY", "shared-provider-key"),
|
||||
]),
|
||||
)
|
||||
.expect("plan");
|
||||
let [alpha, beta] = &plan.agents[..] else {
|
||||
panic!("two connections");
|
||||
};
|
||||
assert_eq!(alpha.env["MY_KEY"], "alpha-only");
|
||||
assert!(!beta.env.contains_key("MY_KEY"));
|
||||
for agent in [alpha, beta] {
|
||||
assert_eq!(agent.env["ANTHROPIC_API_KEY"], "shared-provider-key");
|
||||
for source in ["ALPHA_SOURCE", "DEFAULT_SOURCE", "ANTHROPIC_API_KEY"] {
|
||||
assert!(agent.env_remove.iter().any(|key| key == source), "{source}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_cannot_override_identity_sources_or_use_invalid_names() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -158,7 +185,18 @@ fn file_credentials_resolve_relative_to_the_config_directory() {
|
||||
)
|
||||
.expect("plan");
|
||||
assert_eq!(plan.agents[0].env["API_CREDENTIAL"], "test-value");
|
||||
assert_eq!(plan.agents[0].workdir, fixture.root.path());
|
||||
assert_eq!(
|
||||
plan.agents[0].workdir,
|
||||
fixture.root.path().join("workspaces/helper")
|
||||
);
|
||||
assert_eq!(plan.new_workdirs, [plan.agents[0].workdir.clone()]);
|
||||
// Another agent's pending default does not make an explicit workdir exist.
|
||||
for agents in [
|
||||
" - name: helper\n - name: other\n workdir: workspaces/helper",
|
||||
" - name: other\n workdir: workspaces/helper\n - name: helper",
|
||||
] {
|
||||
assert!(fixture.plan("", agents, &Env::default()).is_err());
|
||||
}
|
||||
assert_eq!(
|
||||
format!("{:?}", SecretRef::Literal("hidden".into())),
|
||||
"SecretRef(<redacted>)"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use nostr::{Keys, ToBech32};
|
||||
@@ -23,11 +24,12 @@ impl Fixture {
|
||||
set -eu
|
||||
test -z "${OWNER_SOURCE+x}"
|
||||
test -z "${OTHER_AGENT_SOURCE+x}"
|
||||
test -z "${SIBLING_SOURCE+x}"
|
||||
test -z "${TAG_SOURCE+x}"
|
||||
test -z "${BUZZ_ACP_HEARTBEAT_PROMPT+x}"
|
||||
test "$BUZZ_ACP_SYSTEM_PROMPT_FILE" = "persona.txt"
|
||||
test -f "$BUZZ_ACP_SYSTEM_PROMPT_FILE"
|
||||
test -z "$BUZZ_ACP_MCP_COMMAND"
|
||||
printf '%s\n%s\n%s\n' "$BUZZ_PRIVATE_KEY" "$BUZZ_AUTH_TAG" "$BUZZ_RELAY_URL" > "launch-${BUZZ_RELAY_URL##*/}"
|
||||
printf '%s\n%s\n%s\n%s\n' "$BUZZ_PRIVATE_KEY" "$BUZZ_AUTH_TAG" "$BUZZ_RELAY_URL" "$(pwd -P)" > "launch-${BUZZ_RELAY_URL##*/}"
|
||||
"#).expect("harness");
|
||||
fs::set_permissions(script, fs::Permissions::from_mode(0o755)).expect("chmod");
|
||||
fs::write(root.path().join("persona.txt"), "Test agent").expect("persona");
|
||||
@@ -53,6 +55,11 @@ defaults:
|
||||
BUZZ_ACP_SYSTEM_PROMPT_FILE: persona.txt
|
||||
agents:
|
||||
{agents}
|
||||
- name: sibling
|
||||
enabled: false
|
||||
auth_tag: {{env: TAG_SOURCE}}
|
||||
env:
|
||||
SIBLING_CREDENTIAL: {{env: SIBLING_SOURCE}}
|
||||
"#
|
||||
),
|
||||
)
|
||||
@@ -70,6 +77,8 @@ agents:
|
||||
self.owner.secret_key().to_bech32().expect("nsec"),
|
||||
)
|
||||
.env("OTHER_AGENT_SOURCE", "must-not-reach-the-child")
|
||||
.env("SIBLING_SOURCE", "declared-by-a-sibling-only")
|
||||
.env("TAG_SOURCE", "declared-by-a-disabled-sibling")
|
||||
.env(
|
||||
"BUZZ_ACP_HEARTBEAT_PROMPT",
|
||||
std::ffi::OsString::from_vec(vec![0xff]),
|
||||
@@ -81,6 +90,14 @@ agents:
|
||||
command
|
||||
}
|
||||
|
||||
/// Launch records land in the default per-agent workdir.
|
||||
fn launch(&self, host: &str) -> std::path::PathBuf {
|
||||
self.root
|
||||
.path()
|
||||
.join("workspaces/helper")
|
||||
.join(format!("launch-{host}"))
|
||||
}
|
||||
|
||||
fn run(&self, args: &[&str]) -> Output {
|
||||
let output = self.command().args(args).output().expect("run swarm");
|
||||
assert!(
|
||||
@@ -98,12 +115,14 @@ fn starts_from_another_directory_and_reuses_one_identity_across_relays_and_resta
|
||||
fixture.write_config(" - name: helper\n - name: disabled\n enabled: false\n nsec: {env: OTHER_AGENT_SOURCE}");
|
||||
let preview = fixture.run(&["validate"]);
|
||||
assert!(String::from_utf8_lossy(&preview.stdout).contains("created on start"));
|
||||
for written in ["keys", "workspaces"] {
|
||||
assert!(
|
||||
!fixture.root.path().join(written).exists(),
|
||||
"validation wrote {written}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!fixture.root.path().join("keys").exists(),
|
||||
"validation wrote keys"
|
||||
);
|
||||
assert!(
|
||||
!fixture.root.path().join("launch-one.example").exists(),
|
||||
!fixture.launch("one.example").exists(),
|
||||
"validation launched a harness"
|
||||
);
|
||||
|
||||
@@ -119,10 +138,8 @@ fn starts_from_another_directory_and_reuses_one_identity_across_relays_and_resta
|
||||
0o600
|
||||
);
|
||||
let keys = Keys::parse(persisted.trim()).expect("agent key");
|
||||
let first =
|
||||
fs::read_to_string(fixture.root.path().join("launch-one.example")).expect("first launch");
|
||||
let second =
|
||||
fs::read_to_string(fixture.root.path().join("launch-two.example")).expect("second launch");
|
||||
let first = fs::read_to_string(fixture.launch("one.example")).expect("first launch");
|
||||
let second = fs::read_to_string(fixture.launch("two.example")).expect("second launch");
|
||||
let first: Vec<_> = first.lines().collect();
|
||||
let second: Vec<_> = second.lines().collect();
|
||||
assert!(
|
||||
@@ -131,6 +148,20 @@ fn starts_from_another_directory_and_reuses_one_identity_across_relays_and_resta
|
||||
);
|
||||
assert_eq!(first[1], second[1], "relays received different auth tags");
|
||||
assert_ne!(first[2], second[2]);
|
||||
// Agents run in a private workdir, not beside `keys/`.
|
||||
let workdir = fixture.root.path().join("workspaces/helper");
|
||||
assert_eq!(
|
||||
Path::new(first[3]),
|
||||
workdir.canonicalize().expect("workdir")
|
||||
);
|
||||
assert_eq!(
|
||||
fs::metadata(&workdir)
|
||||
.expect("workdir")
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o700
|
||||
);
|
||||
assert_eq!(
|
||||
buzz_sdk::nip_oa::verify_auth_tag(first[1], &keys.public_key()).expect("tag"),
|
||||
fixture.owner.public_key()
|
||||
@@ -190,7 +221,7 @@ fn invalid_launches_start_nothing_and_persist_no_keys() {
|
||||
let output = fixture.command().output().expect("run");
|
||||
assert!(!output.status.success(), "invalid configuration succeeded");
|
||||
assert!(!fixture.root.path().join("keys").exists());
|
||||
assert!(!fixture.root.path().join("launch-one.example").exists());
|
||||
assert!(!fixture.root.path().join("workspaces").exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +251,7 @@ fn unknown_selection_and_mismatched_delegation_fail_closed() {
|
||||
.expect("key");
|
||||
fs::write(fixture.root.path().join("auth.json"), tag).expect("tag file");
|
||||
assert!(!fixture.command().output().expect("run").status.success());
|
||||
assert!(!fixture.root.path().join("launch-one.example").exists());
|
||||
assert!(!fixture.launch("one.example").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -234,5 +265,5 @@ fn an_expired_delegation_fails_before_launch() {
|
||||
fs::write(path, config).expect("write");
|
||||
assert!(!fixture.command().output().expect("run").status.success());
|
||||
assert!(!fixture.root.path().join("keys").exists());
|
||||
assert!(!fixture.root.path().join("launch-one.example").exists());
|
||||
assert!(!fixture.launch("one.example").exists());
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ fn blocked_stdout_does_not_prevent_shutdown_or_orphan_the_harness() {
|
||||
.expect("start swarm");
|
||||
let mut running = Running {
|
||||
swarm,
|
||||
pid_file: directory.path().join("harness.pid"),
|
||||
pid_file: directory.path().join("workspaces/noisy/harness.pid"),
|
||||
};
|
||||
let startup_deadline = Instant::now() + Duration::from_secs(5);
|
||||
let harness_pid = loop {
|
||||
|
||||
Reference in New Issue
Block a user