fix: guide WorkBuddy and CodeBuddy daemon startup

This commit is contained in:
drakezhang
2026-09-29 23:59:03 +08:00
parent 6824001597
commit 72986b2490
14 changed files with 381 additions and 105 deletions
+24 -15
View File
@@ -68,24 +68,27 @@ may retain its old PATH even after a new terminal picks up the installation.
Check the install result and destination. Existing files are skipped; inspect
them before deciding whether to keep them or restore the bundled skill with
`--force`, which overwrites the file. Doctor explains paused automatic updates;
custom instructions are preserved. Verify discovery in Step 5.
custom instructions are preserved. Confirm the main skill and its references
were installed/synced for the intended harness. Session-start skill sync happens
only after daemon discovery succeeds, so a startup failure cannot repair an old
skill through that path. Verify discovery in Step 5, using a new chat if the
current one still has the old skill loaded.
## 3. Run `bsk doctor`
If this environment reaps child processes after every shell command, first follow
the [sandbox setup guide](docs/sandboxed-agents.md), including its PowerShell examples
for Windows. Reuse the host daemon's existing `BSK_HOME` (or its default if unset),
set `BSK_AUTO_START=0`, and check `bsk status --json`. Reuse a working daemon; a
permission error or timeout is not evidence that it is absent. Only when it is
missing and no host task is already starting it, launch
`bsk daemon start --foreground` in a persistent host task, or use a normal host
terminal as described in the guide. In another shell tool call, confirm status
succeeds before continuing; allow up to five checks with one-second pauses for
transient startup errors. If the task exits or never becomes ready, inspect its
output and `bsk logs`, then recheck for an existing daemon before another launch.
Use the same `BSK_HOME` and `BSK_AUTO_START=0` for every sandboxed command, including
`doctor` and session commands. Keep browser commands sandboxed; environment settings
may not persist across shell calls. Report unresolved errors instead of looping.
In WorkBuddy/CodeBuddy, or hosts that reap command children, first follow the
[host setup guide](docs/sandboxed-agents.md). Reuse the existing `BSK_HOME` and
probe with `BSK_AUTO_START=0`. If the daemon is missing and no task is starting
it, use the host's managed background facility (`run_in_background: true` when
supported) to run `bsk daemon start --foreground`. Keep that task running and
verify status from a separate tool call before continuing. A permission error
or timeout does not establish absence; inspect the actual error rather than
launching again. The guide covers PowerShell, unavailable background facilities
and the independent-terminal fallback.
Repeat the same `BSK_HOME` and `BSK_AUTO_START=0` for every client call in this
workflow, including doctor and sessions; do not rely on earlier shell exports.
Ordinary local hosts retain their normal automatic startup.
```bash
bsk doctor
@@ -130,6 +133,12 @@ session with `bsk session stop <id>` on success or failure. With multiple browse
use `bsk browsers` and add `--browser <id>` when starting the session.
For dsh, use its injected `browser_*` tools instead.
For the WorkBuddy/CodeBuddy or child-reaping-host workflow, use separate tool
calls for startup, status, session creation, navigation/observation and final
status after session cleanup. Confirm the daemon remains reachable and its
managed task (if used) stays running. A single successful doctor call, or running all
commands in one shell, does not verify this lifetime requirement.
Report success only after the page is read and the test session is stopped.
If a step remains blocked, report which part is ready and what remains unverified.
+1 -1
View File
@@ -149,7 +149,7 @@ Use `bsk --help` or `bsk <command> --help` for command options. Always stop your
</details>
If your agent sandbox removes background processes after each command, use the [sandbox setup guide](docs/sandboxed-agents.md). It explains how to keep the daemon in a persistent host environment and connect with shared `BSK_HOME` and `BSK_AUTO_START=0`.
In WorkBuddy/CodeBuddy, or hosts that reap command children, the agent should reuse an existing daemon or run `bsk daemon start --foreground` in a managed background task, then verify it from a separate tool call. Follow the [host setup guide](docs/sandboxed-agents.md) for shared `BSK_HOME`, `BSK_AUTO_START=0`, and the independent-terminal fallback when the host cannot keep a task alive.
## DeepSeek Harness plugin
+1 -1
View File
@@ -149,7 +149,7 @@ bsk session stop <id>
</details>
如果 Agent 沙盒会在每条命令后回收后台进程,请使用[沙盒配置指南](docs/sandboxed-agents.md):在宿主环境保持 daemon 运行,Agent 通过共享的 `BSK_HOME` 和 `BSK_AUTO_START=0` 连接。
在 WorkBuddy/CodeBuddy 或会回收命令子进程的宿主中,Agent 应先复用已有 daemon;需要启动时,在宿主管理的后台任务中运行 `bsk daemon start --foreground`,并在另一条工具调用中验证连接。[宿主配置指南](docs/sandboxed-agents.md)说明了如何共用 `BSK_HOME`、设置 `BSK_AUTO_START=0`,以及宿主无法维持任务时的独立终端兜底方式。
## DeepSeek Harness 插件
+5 -5
View File
@@ -21,11 +21,11 @@ advice-only tasks. Never extract credentials, cookies, tokens, or other secrets.
before starting. Verify its instance mapping, bind every new session explicitly,
and never substitute another instance or omit the selector to recover.
- Parallel work: [parallel tasks](references/tabs-and-profiles.md).
- Installing this skill does not install the `bsk` CLI or browser extension.
For a missing CLI, startup or connection failure, or remote pairing, read
[environment setup](references/environment.md). Commands normally auto-start the
daemon; if the host cleans up background children, read that guide before any
session command. Never restart a shared daemon or delete runtime files to recover.
- In WorkBuddy/CodeBuddy or hosts that reap children, read [environment](references/environment.md)
before sessions: reuse a reachable daemon or run `--foreground` in a managed background task.
Read it also for missing CLI, startup/connection failures or remote pairing.
Skill installation excludes the CLI/extension. Never restart shared daemons or
delete runtime files to recover.
- Borrow confirmation and human help follow the extension's Automation settings.
Never change settings or switch browser backends to bypass them.
+96 -24
View File
@@ -16,33 +16,105 @@ For remote setup or pairing, follow the [remote guide](https://github.com/Tencen
## Local daemon startup
Local commands normally auto-start the daemon. If the host terminates background
children after each shell call, including on Windows, complete these steps first:
Ordinary local commands auto-start the daemon. In WorkBuddy/CodeBuddy, or a host
that reaps shell children, use the following workflow before session commands.
This does not change the startup policy for other local agents.
1. Reuse the host daemon's existing `BSK_HOME` (or its default if unset). Set
`BSK_AUTO_START=0` and run `bsk status --json`. Reuse a working daemon; an empty
`browsers` list means the extension still needs connecting. Permission errors,
timeouts or invalid replies do not prove the daemon is absent.
2. Only if the check reports a missing daemon and no host task is already starting
it, run `bsk daemon start --foreground` with the same `BSK_HOME` in the host's
approved persistent background task outside the per-command sandbox. Keep that
task alive; `--foreground` alone cannot prevent host cleanup. The
[sandbox guide](https://github.com/Tencent/BrowserSkill/blob/main/docs/sandboxed-agents.md)
covers the normal host-terminal alternative and PowerShell examples.
3. After launching, or if a host task is already starting the daemon, run
`bsk status --json` in a **separate shell tool call** with the same `BSK_HOME`
and `BSK_AUTO_START=0`. While startup is pending, make at most five
checks with one-second pauses for missing-endpoint or transient startup errors;
stop on permission/protocol errors. Proceed only after a successful status
response. If the host task exits (including a lock error) or readiness never
succeeds, inspect its output and `bsk logs`, then recheck status for another
daemon before deciding whether startup is still needed. Report unresolved
errors; do not loop on launches, delete runtime files or restart a shared daemon.
### Reuse before starting
Use the same `BSK_HOME` and `BSK_AUTO_START=0` on EVERY sandboxed command;
environment settings may not persist between shell calls. Keep browser commands
sandboxed. For other startup failures, retry once, then use `bsk doctor`.
Use the same CLI executable and the host daemon's existing `BSK_HOME` (or its
default if unset). Set `BSK_AUTO_START=0` and run `bsk status --json`:
- **Status succeeds:** reuse the daemon. Empty `browsers` means the extension
needs connecting, not that another daemon is needed.
- **Missing daemon / no listener:** check whether a host task is already starting
it. Inspect that task and wait for readiness; launch only if none exists.
- **Permission error, timeout or invalid reply:** inspect the actual path and IPC
access. These errors do not establish that the daemon is absent.
`BSK_AUTO_START=0` disables implicit startup only. It still allows explicit
`daemon start`, including the managed task below. Repeat the same environment on
**every** client tool call; shell environment assignments may not persist.
Do not choose a new directory or change global `HOME` to work around a failure.
For an externally managed service, preserve its configuration and owning supervisor.
### WorkBuddy/CodeBuddy managed startup
Use the current tool schema's managed background facility, typically Bash or
PowerShell with `run_in_background: true`. The daemon runs in the foreground
**inside** that background task. These are tool arguments, not shell commands:
Bash, using the existing default daemon directory:
```json
{
"command": "BSK_AUTO_START=0 bsk daemon start --foreground",
"run_in_background": true
}
```
PowerShell, using the existing default daemon directory:
```json
{
"command": "$env:BSK_AUTO_START = '0'; bsk daemon start --foreground",
"run_in_background": true
}
```
For a custom directory, set its actual `BSK_HOME` in both startup and client
calls. Bash uses `BSK_HOME='/actual/path' BSK_AUTO_START=0 bsk ...`; PowerShell
uses `$env:BSK_HOME = 'C:\actual\path'; $env:BSK_AUTO_START = '0'; bsk ...`.
Use the verified CLI path if needed. PowerShell's `& 'path/to/bsk.exe'` is a
call operator, not the Unix trailing `&` used to background a command.
- Retain the returned task ID. A running daemon task is expected: do not wait
for completion or cancel it as leftover work. Read its status/output with
`TaskOutput` or the tool's equivalent, using a nonblocking or short query.
- Do not substitute `nohup`, `setsid`, `Start-Process`, trailing `&`, or a long
sleep for managed background execution. `--foreground` alone is insufficient.
- Full access or disabling a sandbox does not establish process lifetime. Use
the host's supported execution mechanism; if isolation prevents IPC or task
survival, use an available, authorized per-launch exception. Do not invent
tool parameters or change global permissions. Keep browser calls sandboxed.
Other hosts with child cleanup use their equivalent persistent task facility.
If no background facility is available, or the tool rejects/degrades it, do not
claim a task was started. If independent startup has not already failed and
there is no evidence of child cleanup, try ordinary `bsk daemon start` once,
then verify from a separate call with `BSK_AUTO_START=0`. Otherwise, use the
[sandbox guide](https://github.com/Tencent/BrowserSkill/blob/main/docs/sandboxed-agents.md)
for an independent-terminal fallback. Ask the user to launch it only after
establishing that this host cannot provide a working persistent launch path;
give the command for their actual shell, CLI path and daemon directory.
### Verify across tool calls
After launching, or if a startup task exists, query status in a **separate shell
tool call**. For example, PowerShell client arguments are:
```json
{
"command": "$env:BSK_AUTO_START = '0'; bsk status --json"
}
```
Repeat a custom `BSK_HOME` assignment if used above. During startup, make at most
five checks with one-second pauses for missing endpoints, discovery races or
transient timeouts; stop on permission/protocol errors. A task ID, PID or
`daemon ready` log alone is not readiness. Proceed only after status succeeds.
If the task exits (including a lock error) or readiness never succeeds, inspect
its output and `bsk logs`, then recheck status: another caller may have started
the daemon. Reuse it if reachable; otherwise report the observed error instead
of looping on launches, deleting runtime files or restarting a shared daemon.
Create the session and perform the next browser operation in separate tool calls
using the returned session ID. On completion, stop only your session; keep the
shared daemon task running. On later use, probe again rather than relying on an
old task ID. Idle exit or host shutdown may require a new launch. If the daemon
was replaced, check session state before continuing; do not blindly replay actions.
A local process identity warning permits browser commands when IPC works.
For other startup failures, retry once, then use `bsk doctor`.
## Extension connection
+42 -8
View File
@@ -10,7 +10,7 @@ use serde::Serialize;
use crate::cli::browser_wait::{
browser_query_ipc_timeout, doctor_browser_connect_wait, wait_for_browser_ms,
};
use crate::cli::ensure_daemon::{AUTO_START_DISABLED_HINT, auto_start_enabled, ensure_daemon};
use crate::cli::ensure_daemon::{auto_start_enabled, ensure_daemon};
use crate::cli::status::Output;
use crate::cli::update::{
self,
@@ -19,6 +19,7 @@ use crate::cli::update::{
use crate::daemon::info::DaemonInfo;
use crate::daemon::paths;
use crate::daemon::probe::{self, Probe};
use crate::daemon::start_error::{DaemonStartFailure, recovery_hint};
use crate::daemon::state::PROTOCOL_VERSION;
/// Chrome Web Store listing for the browser-skill extension.
@@ -132,7 +133,7 @@ fn resolve_daemon_state(output: Output) -> DaemonState {
if matches!(state, DaemonState::Missing | DaemonState::NoListener(_)) && auto_start_enabled() {
if let Err(err) = ensure_daemon() {
return DaemonState::ProbeError(format!("{err:#}"));
return DaemonState::ProbeError(err);
}
state = current_state(Duration::ZERO);
}
@@ -166,7 +167,7 @@ fn needs_browser_wait(state: &DaemonState) -> bool {
enum DaemonState {
Missing,
NoListener(DaemonInfo),
ProbeError(String),
ProbeError(anyhow::Error),
Verified {
status: StatusResult,
local_identity_error: Option<String>,
@@ -406,7 +407,7 @@ fn current_state(browser_wait: Duration) -> DaemonState {
},
Ok(Probe::Absent(Some(info))) => DaemonState::NoListener(info),
Ok(Probe::Absent(None)) => DaemonState::Missing,
Err(err) => DaemonState::ProbeError(format!("{err:#}")),
Err(err) => DaemonState::ProbeError(err),
}
}
@@ -515,7 +516,7 @@ fn check_daemon_running(state: &DaemonState) -> CheckResult {
if auto_start_enabled() {
"run `bsk daemon start` or any `bsk` command (daemon is auto-spawned)"
} else {
AUTO_START_DISABLED_HINT
DaemonStartFailure::AutoStartDisabled.hint()
},
),
DaemonState::NoListener(info) => CheckResult::fail(
@@ -528,13 +529,15 @@ fn check_daemon_running(state: &DaemonState) -> CheckResult {
if auto_start_enabled() {
"run `bsk daemon start`; check `bsk logs` if startup fails"
} else {
AUTO_START_DISABLED_HINT
DaemonStartFailure::AutoStartDisabled.hint()
},
),
DaemonState::ProbeError(err) => CheckResult::fail(
name,
err.clone(),
"check daemon IPC permissions and `bsk logs`; keep existing runtime files",
format!("{err:#}"),
recovery_hint(err).unwrap_or(
"check daemon IPC permissions and `bsk logs`; keep existing runtime files",
),
),
}
}
@@ -731,6 +734,37 @@ mod m2_tests {
use bsk_protocol::StatusResult;
use bsk_protocol::system::{BrowserStatusEntry, VersionSkewEntry};
#[test]
fn startup_probe_errors_preserve_recovery_and_cause() {
for failure in [
DaemonStartFailure::AutoStartDisabled,
DaemonStartFailure::IndependentStartFailed,
] {
let error = anyhow::anyhow!("startup fixture cause")
.context(failure)
.context("automatic daemon startup failed");
let check = check_daemon_running(&DaemonState::ProbeError(error));
assert_eq!(check.status, CheckStatus::Fail);
assert_eq!(check.hint.as_deref(), Some(failure.hint()));
assert!(check.detail.contains("startup fixture cause"));
let json = serde_json::to_value(check).unwrap();
assert_eq!(json["hint"], failure.hint());
assert_eq!(json["ok"], false);
}
}
#[test]
fn ordinary_probe_errors_keep_ipc_guidance() {
let error = anyhow::anyhow!("IPC fixture failure").context("query daemon status");
let check = check_daemon_running(&DaemonState::ProbeError(error));
assert_eq!(check.status, CheckStatus::Fail);
assert_eq!(check.detail, "query daemon status: IPC fixture failure");
assert_eq!(
check.hint.as_deref(),
Some("check daemon IPC permissions and `bsk logs`; keep existing runtime files")
);
}
fn fake_status(browsers: Vec<BrowserStatusEntry>, skew: Vec<VersionSkewEntry>) -> StatusResult {
StatusResult {
daemon_version: env!("CARGO_PKG_VERSION").into(),
+2 -4
View File
@@ -17,6 +17,7 @@ use crate::cli::daemon::StartArgs;
use crate::daemon::info::DaemonInfo;
use crate::daemon::probe::{self, PROBE_TIMEOUT, Probe};
use crate::daemon::start::start_background;
use crate::daemon::start_error::DaemonStartFailure;
/// Maximum time to wait for an auto-spawned daemon to become ready.
pub const SPAWN_DEADLINE: Duration = Duration::from_millis(3_000);
@@ -27,9 +28,6 @@ pub(crate) fn auto_start_enabled() -> bool {
std::env::var_os("BSK_AUTO_START").as_deref() != Some(std::ffi::OsStr::new("0"))
}
pub(crate) const AUTO_START_DISABLED_HINT: &str = "automatic daemon startup is disabled (BSK_AUTO_START=0); \
run `bsk daemon start` in the owning host environment with the same BSK_HOME, then retry";
/// Return verified discovery info, starting a daemon only when its discovery
/// file or IPC listener is absent and auto-start is enabled.
pub fn ensure_daemon() -> Result<DaemonInfo> {
@@ -37,6 +35,6 @@ pub fn ensure_daemon() -> Result<DaemonInfo> {
if let Probe::Ready(daemon) = probe::probe(PROBE_TIMEOUT)? {
return Ok(daemon.info);
}
ensure!(auto_start_enabled(), AUTO_START_DISABLED_HINT);
ensure!(auto_start_enabled(), DaemonStartFailure::AutoStartDisabled);
start_background(&StartArgs::default(), deadline).context("automatic daemon startup failed")
}
+55 -4
View File
@@ -18,6 +18,7 @@ use serde::Serialize;
use thiserror::Error;
use super::render_error;
use crate::daemon::start_error::recovery_hint;
/// Strongly-typed CLI error. Wraps either a structured daemon error or
/// a transport / setup failure (`anyhow::Error`).
@@ -178,10 +179,11 @@ fn hint_for(
) -> Option<&'static str> {
render_info
.and_then(|info| info.hint)
.or(if matches!(err, CliError::Local(_)) {
Some("is the daemon running? try `bsk daemon start` or `bsk status`")
} else {
None
.or_else(|| match err {
CliError::Local(error) => recovery_hint(error).or(Some(
"is the daemon running? try `bsk daemon start` or `bsk status`",
)),
_ => None,
})
}
@@ -423,6 +425,55 @@ mod tests {
);
}
#[test]
fn startup_recovery_survives_contexts_in_human_and_json_output() {
use crate::daemon::start_error::DaemonStartFailure;
for failure in [
DaemonStartFailure::AutoStartDisabled,
DaemonStartFailure::IndependentStartFailed,
] {
let error = anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"startup fixture cause",
))
.context(failure)
.context("automatic daemon startup failed")
.context("ensure daemon is running");
let cli = CliError::Local(error);
let hint = hint_for(&cli, render_info_for(&cli).as_ref());
assert_eq!(hint, Some(failure.hint()));
let json: serde_json::Value =
serde_json::from_str(&json_error_string(&cli, cli.exit_code(), hint)).unwrap();
assert!(json["code"].is_null());
assert_eq!(json["exit_code"], 2);
assert_eq!(json["hint"], failure.hint());
assert!(
json["message"]
.as_str()
.unwrap()
.contains("startup fixture cause")
);
assert!(json.get("data").is_none());
let human = render_human_to_string(&cli, None);
assert!(human.contains("startup fixture cause"));
assert!(human.contains(&format!("hint: {}", failure.hint())));
assert!(!human.contains("try `bsk daemon start` or `bsk status`"));
}
}
#[test]
fn ordinary_local_errors_keep_their_existing_hint() {
// Even identical wording must not classify an untyped error as startup recovery.
let cli = CliError::Local(anyhow::anyhow!(
"automatic daemon startup is disabled (BSK_AUTO_START=0)"
));
assert_eq!(
hint_for(&cli, None),
Some("is the daemon running? try `bsk daemon start` or `bsk status`")
);
}
#[test]
fn permission_denied_element_not_visible_renders_geometry_hint() {
let cli = CliError::from_rpc(RpcError {
+1
View File
@@ -17,6 +17,7 @@ pub mod session_interrupt;
pub mod session_requests;
pub mod sessions;
pub mod start;
pub(crate) mod start_error;
pub mod state;
pub mod ws;
+10 -12
View File
@@ -12,13 +12,9 @@ use windows_sys::Win32::System::Threading::{
};
use super::{DAEMON_REPLACEMENT_WAIT_ENV, DAEMONIZED_ENV, StartArgs, apply_start_args};
use crate::daemon::start_error::DaemonStartFailure;
use crate::windows_process::{self, Process};
const DETACH_HINT: &str = "cannot start an independent Windows daemon; the host may prohibit Job Object breakaway. \
Run `bsk daemon start --foreground` in a persistent host task outside the per-command Job, \
or start the daemon from an independent terminal, using the same BSK_HOME and OS user. \
Then use BSK_AUTO_START=0 in the agent";
pub(super) fn spawn(exe: &Path, args: &StartArgs, predecessor_pid: Option<u32>) -> Result<Process> {
let mut command = std::process::Command::new(exe);
apply_start_args(&mut command, args);
@@ -46,12 +42,12 @@ pub(super) fn spawn(exe: &Path, args: &StartArgs, predecessor_pid: Option<u32>)
[&input, &output, &output],
DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB | CREATE_SUSPENDED,
)
.context(DETACH_HINT)?;
.context(DaemonStartFailure::IndependentStartFailed)?;
if let Err(err) = child.resume_outside_job() {
// A suspended child must never be left behind if validation/resume fails.
let _ = child.kill();
let _ = child.wait();
return Err(err).context(DETACH_HINT);
return Err(err).context(DaemonStartFailure::IndependentStartFailed);
}
Ok(child)
}
@@ -74,14 +70,16 @@ pub(super) fn check_breakaway(exe: &Path) -> Result<()> {
[&input, &output, &output],
DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB | CREATE_SUSPENDED,
)
.context(DETACH_HINT)?;
.context(DaemonStartFailure::IndependentStartFailed)?;
let outside = probe.outside_job();
let _ = probe.kill();
let _ = probe.wait();
anyhow::ensure!(
outside.context("check the probe's Job membership")?,
"{DETACH_HINT}: breakaway from an outer Job is not allowed"
);
if !outside.context("check the probe's Job membership")? {
return Err(anyhow::anyhow!(
"breakaway from an outer Job is not allowed"
))
.context(DaemonStartFailure::IndependentStartFailed);
}
Ok(())
}
+31
View File
@@ -0,0 +1,31 @@
//! Recovery guidance for startup failures, preserved through anyhow contexts.
#[derive(Debug, Clone, Copy, thiserror::Error)]
pub(crate) enum DaemonStartFailure {
#[error("automatic daemon startup is disabled (BSK_AUTO_START=0)")]
AutoStartDisabled,
#[cfg(any(windows, test))]
#[error(
"cannot start an independent Windows daemon; the host may prohibit Job Object breakaway"
)]
IndependentStartFailed,
}
impl DaemonStartFailure {
pub(crate) fn hint(self) -> &'static str {
// Both failures need an owner that can keep the daemon alive. Preserve
// custom/server deployments instead of replacing them with local defaults.
"With BSK_AUTO_START=0, check for an existing host task or restore the managed service \
in its owning host environment with its original configuration. For a new local daemon, \
run `bsk daemon start --foreground` in a managed persistent host task \
(run_in_background=true when supported), using the same BSK_HOME and OS user. \
Keep that task running; verify `bsk status --json` with BSK_AUTO_START=0 in a separate tool call. \
If no persistent task is available, use `bsk daemon start` from an independent terminal."
}
}
pub(crate) fn recovery_hint(error: &anyhow::Error) -> Option<&'static str> {
error
.downcast_ref::<DaemonStartFailure>()
.map(|failure| failure.hint())
}
+13 -1
View File
@@ -153,7 +153,19 @@ fn disabled_auto_start_leaves_missing_and_stale_discovery_alone() {
assert!(!out.status.success());
let error = String::from_utf8_lossy(&out.stdout);
assert!(error.contains("BSK_AUTO_START=0"), "{error}");
assert!(error.contains("owning host environment"), "{error}");
let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(json["exit_code"], 2);
assert!(json["code"].is_null());
let hint = json["hint"].as_str().unwrap();
assert!(hint.contains("--foreground"), "{hint}");
assert!(hint.contains("run_in_background=true"), "{hint}");
assert!(!hint.contains("try `bsk daemon start` or `bsk status`"));
let human = command(&home, &["status"])
.env("BSK_AUTO_START", "0")
.output()
.unwrap();
assert_eq!(human.status.code(), Some(2));
assert!(String::from_utf8_lossy(&human.stderr).contains(hint));
assert_eq!(std::fs::read(&info_path).ok(), original);
assert!(!home.join("daemon.lock").exists());
}
@@ -461,6 +461,41 @@ fn foreground_stays_owned_by_the_host_job() {
let _ = launcher.finish();
}
#[test]
fn foreground_in_a_persistent_job_survives_client_job_cleanup() {
let mut fixture = Fixture::new();
fixture.auto_start = false;
let owner = Job::new(0);
let mut launcher = fixture.launch(
&["daemon", "start", "--foreground", "--port", "0"],
&[&owner],
);
let original = fixture.wait_for_info();
let process = open_process(original.pid);
assert!(original.host_managed);
assert!(in_job(process.as_raw_handle(), owner.0.as_raw_handle()));
for _ in 0..2 {
let client_job = Job::new(0);
success(fixture.run(&["status", "--json"], &[&client_job]));
assert!(!in_job(
process.as_raw_handle(),
client_job.0.as_raw_handle()
));
drop(client_job);
assert_alive(&process);
success(fixture.run(&["status", "--json"], &[]));
assert_eq!(fixture.info(), original);
}
drop(owner);
assert_eq!(
unsafe { WaitForSingleObject(process.as_raw_handle(), 5000) },
WAIT_OBJECT_0
);
let _ = launcher.finish();
}
#[test]
fn failed_start_is_bounded_and_releases_the_daemon_lock() {
let fixture = Fixture::new();
+65 -30
View File
@@ -7,8 +7,11 @@ In such an environment, keep the daemon in a persistent host execution context
and run browser commands inside the sandbox over shared local IPC.
The same setup applies to Windows agents whose shell tasks terminate child processes.
Ordinary local use still auto-starts the daemon. No sandbox detection, service
installation or global change to home-directory resolution is required.
In WorkBuddy/CodeBuddy, follow the reuse/startup checks below before the first
session command. Other local agents retain normal automatic startup unless their
host reaps children. No runtime host detection or global configuration change is
required. Full access, sandbox isolation and task lifetime are separate settings:
disabling isolation does not ensure a command's children survive its completion.
## Windows background startup
@@ -24,10 +27,11 @@ error with setup instructions instead of retrying as a host-owned background
process. An already reachable daemon is still reused, even from a restrictive
Job. Use the persistent host setup below when breakaway is unavailable.
`--foreground` deliberately remains owned by its host task. Run that task
outside the per-command Job and keep it alive; the flag does not bypass Job
termination. Breakaway is also not a guarantee against an explicit process-tree
kill or host shutdown. Windows Job termination does not give a daemon an
`--foreground` deliberately remains owned by its host task. A persistent host
Job can own it even when breakaway is forbidden; keep that task alive across
client calls. It must not share a short-lived client's cleanup lifetime. The flag
does not bypass termination of its owning Job. Breakaway is also not a guarantee
against an explicit process-tree kill or host shutdown. Windows Job termination does not give a daemon an
opportunity to log a shutdown reason.
Query commands retain their existing automatic-start behavior. Set
@@ -92,31 +96,52 @@ successful check lets you skip Step 3 and continue with Step 4.
## 3. Start only when needed, then verify readiness
If the agent host provides a persistent background-task facility, let that task
own the foreground daemon outside the per-command sandbox, using the directory
checked above:
Use the host's managed background-task facility to own the foreground daemon.
For WorkBuddy/CodeBuddy tools that expose `run_in_background`, these are Bash
**tool arguments**, using the actual directory checked above:
```bash
BSK_HOME=/absolute/shared/bsk bsk daemon start --foreground
```json
{
"command": "BSK_HOME='/absolute/shared/bsk' BSK_AUTO_START=0 bsk daemon start --foreground",
"run_in_background": true
}
```
For Windows hosts using PowerShell, set the same directory in that host task:
For PowerShell tools:
```powershell
$env:BSK_HOME = 'C:\path\to\shared\bsk'
bsk daemon start --foreground
```json
{
"command": "$env:BSK_HOME = 'C:\\path\\to\\shared\\bsk'; $env:BSK_AUTO_START = '0'; bsk daemon start --foreground",
"run_in_background": true
}
```
Keep the host task running across subsequent browser commands. `--foreground`
keeps the daemon attached to that task; it does not make an ordinary short-lived
shell persistent. If no persistent task facility is available, the user can
instead start the daemon from a normal host terminal:
If using the existing default directory, omit only the `BSK_HOME` assignment.
Use the verified executable path if `bsk` is not on PATH. PowerShell's `&` call
operator for a quoted executable path is distinct from a Unix trailing `&`.
Keep the returned task ID and leave the task running. Inspect status/output with
`TaskOutput` or its equivalent without waiting for daemon completion. Do not use
`nohup`, `setsid`, `Start-Process`, trailing `&`, or a long sleep as a substitute
for a managed task. The tool's background flag keeps a task available to later
calls; `--foreground` keeps bsk attached to that task. Neither alone establishes
that the host will keep it alive.
Read the current tool schema: availability depends on the host version and mode.
Do not assume an unsupported or downgraded background request succeeded. If no
persistent task facility is available and independent startup has not failed or
been observed to be reaped, try ordinary `bsk daemon start` once and verify it
from another call with `BSK_AUTO_START=0`. If the host has no working persistent
launch path, give the user an independent-terminal command using the actual CLI
path and the same daemon directory, for example:
```bash
BSK_HOME=/absolute/shared/bsk bsk daemon start
```
In PowerShell, set `BSK_HOME` as above and run `bsk daemon start`.
In PowerShell, set `$env:BSK_HOME` to that directory, then run `bsk daemon start`.
A configured service or server must instead be restored through its owning
supervisor with its original flags; do not replace it with the local defaults.
**Verify readiness in a separate shell tool call.** Repeat Step 2's status check
from the agent environment. During startup, allow at most five checks with
@@ -133,16 +158,19 @@ startup does not reuse a discovered daemon. Reuse it if the recheck succeeds;
otherwise resolve or report the observed error instead of repeatedly launching.
Keep runtime files and shared daemons intact.
Use the host's approved mechanism for that launch to run outside the sandbox.
CodeBuddy's [tool reference](https://www.codebuddy.ai/docs/cli/tools-reference)
documents background tasks and per-command sandbox exceptions; availability
depends on the host's settings. Do not turn off sandbox protection for all
browser commands. Verify that the chosen host task survives subsequent shell
commands. Cancelling that task or shutting down its host can still stop the
daemon; bsk cannot make a process outlive the environment that owns it.
If isolation prevents IPC access or task survival, use an available, authorized
per-launch exception supplied by the host. Do not invent unsupported parameters
or disable sandbox protection for all browser commands. CodeBuddy's
[tool reference](https://www.codebuddy.ai/docs/cli/tools-reference) documents
managed background tasks and per-command exceptions. Its
[headless-mode guide](https://www.codebuddy.ai/docs/cli/headless) also describes
modes that disable background tasks. Verify actual behavior: cancelling the
owning task or shutting down its host can still stop the daemon.
Start and stop the shared daemon in this owning environment. Browser task
cleanup is `bsk session stop`, which leaves other sessions and the daemon alone.
Do not cancel the shared daemon's background task at the end of a browser task.
On later use, probe again instead of relying on a remembered task ID.
A foreground daemon never replaces itself: when a new release is available it
logs the version and the CLI suggests `bsk update`. `bsk update` installs the
release but leaves a foreground daemon running on the previous version, so
@@ -218,13 +246,20 @@ checks or delete lock files to work around a refused stop.
checks above. Confirm status works from a separate sandboxed tool call with
the same `BSK_HOME` and `BSK_AUTO_START=0`.
2. Create a browser session, let that shell invocation finish, then navigate and
take a snapshot in another invocation using the same session ID. Confirm the
daemon instance in `daemon.json` has not changed.
3. Stop only that session. Confirm another status call still reaches the daemon.
observe in another invocation using the same session ID. With successful IPC,
compare the PID, start time and endpoint in `daemon.json` to confirm the same
instance is serving; a PID alone is not sufficient evidence.
3. Stop only that session. Confirm another status call still reaches the daemon
and its managed task (if used) remains running. Repeat a browser task in a later turn.
4. In a controlled setup with no other active sessions, stop the daemon from its
owning environment. The next sandboxed command must report it unavailable
without starting a new daemon. Restart it from the host when needed.
Record the host version, CLI path/version, tool startup arguments and task status
while testing. Run startup, status, session creation, browser operations and final
status as separate tool calls, not one combined shell command. The agent or
maintainer collects these details; ordinary users need not diagnose Job Objects.
This check validates the host's actual process lifetime and IPC permissions.
Successful local CLI tests alone do not establish that a particular WorkBuddy
version or configuration keeps its background tasks alive.