mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-10-01 23:24:44 +08:00
fix: guide WorkBuddy and CodeBuddy daemon startup
This commit is contained in:
+24
-15
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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 插件
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user