Skip to content

fix(bsk): start the Windows daemon from a headless task when the host forbids breakaway - #35

Merged
code-yeongyu merged 2 commits into
code-yeongyu:mainfrom
LilMGenius:fix/windows-daemon-headless-task
Oct 4, 2026
Merged

code-yeongyu merged 2 commits into
code-yeongyu:mainfrom
LilMGenius:fix/windows-daemon-headless-task

Conversation

@LilMGenius

@LilMGenius LilMGenius commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

On Windows, an agent host that runs commands inside a Job Object forbidding breakaway (Codex on Windows 11 here) cannot start the BrowserSkill daemon through omowright. connectBrowserSkill auto-starts with bsk status --json, and bsk 0.3.2 refuses to detach:

cannot start an independent Windows daemon; the host may prohibit Job Object breakaway;
use `bsk daemon start --foreground` in a persistent host task ...

startDaemonWithCli (src/bsk/daemon-info.js) turned that into bsk status exited with 2, so the attached engine stopped at step one. Agents then improvise their own task, typically powershell -WindowStyle Hidden, which opens an empty Windows Terminal window at every logon because Windows Terminal ignores that flag.

Change

On that refusal (win32 only, matched on "Job Object"), startDaemonWithCli registers a per-user scheduled task bsk-daemon-<sha256(BSK_HOME)[0:12]> and starts it. Task Scheduler launches the action outside the caller's job, as bsk asks; discovery then reads daemon.json exactly as before. The action is

conhost.exe --headless powershell.exe -NoProfile -NonInteractive -EncodedCommand <base64 of:
  $env:BSK_HOME = '<home>'; & '<bsk>' daemon start --foreground; exit $LASTEXITCODE>

conhost --headless gives the process a console with no window. The home and binary are PowerShell single-quoted literals (' doubled), so no character in either path is interpreted. Every other exit of bsk status still throws as before.

A cmd.exe /c set "BSK_HOME=<home>"&& "<bsk>" ... line was tried first and does not hold under conhost --headless: measured on this machine, the task ran for a plain home and silently did nothing for homes containing &, (x) or ^ even inside the quotes, and % still expands there. The encoded form needs no refused characters.

Evidence (Windows 11, bsk 0.3.2, node 26.10.0, bun 1.4.2)

Input origin/main 8b57a56 this branch
new BskIpcClient({ autoStart: true }).call("system.status") from a Codex host, no daemon running bsk status exited with 2 daemon_version 0.3.2
visible console or Terminal windows opened during that call n/a 0
test/bsk-daemon-start.test.mjs: homes Tom&Jerry (x) ^ ;, 100% %PATH% home, O'Brien; bsk in bin & (x) fail 3 pass
same test against the first revision's unquoted cmd.exe action 3 fail
bun run test / bun run test:node 152 pass, 34 fail, 3 skip 155 pass, 34 fail, 3 skip

The 34 failures are identical by name on both sides and unrelated to this change (browser-signal, captcha and live-browser tests on this machine).

The test compiles a stand-in bsk with bun build --compile: status prints the refusal and exits 2, daemon start writes its arguments into $BSK_HOME/started.txt. Each case asserts the registered action and that the stand-in started with exactly that home, then unregisters by the exact task name it computed. Dropping --headless also fails all three. It is skipped off Windows, and the Linux CI job does not exercise it.

Limits

  • The task persists so later calls and logons reuse it; it has no logon trigger, so the daemon still starts on demand and exits on its own idle timeout.
  • Two BSK_HOMEs get separate tasks, but bsk itself binds port 52800, so only one daemon runs at a time, as before.

… forbids breakaway

An agent host on Windows runs its commands inside a Job Object that forbids breakaway. bsk 0.3.2 then refuses to detach its daemon: `bsk status --json` exits 2 with "cannot start an independent Windows daemon; the host may prohibit Job Object breakaway" and asks for `bsk daemon start --foreground` in a persistent host task. startDaemonWithCli surfaced that as "bsk status exited with 2", so connectBrowserSkill could not start the daemon at all on those hosts.

On that refusal, startDaemonWithCli now registers a per-user scheduled task named bsk-daemon-<hash of BSK_HOME> and runs it; Task Scheduler starts the action outside the caller's job, and discovery then reads daemon.json as before. The action runs under conhost.exe --headless: a task that starts a console program directly opens it in the default terminal, and Windows Terminal shows that window whatever -WindowStyle Hidden says.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is the right fix for a real problem: on the breakaway refusal, starting the daemon through Task Scheduler with conhost --headless is what bsk itself asks for, and the fallback stays confined to win32 and that exact refusal. Two things before it can merge, the first a blocker:

1. The task's cmd.exe line doesn't quote the home path (src/bsk/daemon-info.js:53):

--headless cmd.exe /d /c set BSK_HOME=<home>&& "<bsk>" daemon start --foreground

<home> is BSK_HOME or the user's home directory, inserted bare. A path containing a cmd metacharacter (&, |, <, >, ^, (, )), for example a profile folder like C:\Users\Tom&Jerry, splits the command line. The daemon start then fails, and the text after the & runs as a command of its own. Please use the quoted form set "BSK_HOME=<home>"&& "<bsk>" daemon start --foreground, which keeps &|<>^() literal. Also refuse, with a clear error, a home or binary path that contains " or %: the quotes can't protect those, since %VAR% still expands inside them. Please extend the Windows test with a home path that contains & (and one with %, which must be refused), asserting the registered action and that the daemon still starts.

2. The test registers a real scheduled task on the machine running it. That's acceptable because it is win32-only, scoped by BSK_HOME, and removed in finally. Please make the cleanup also cover a failure partway through: unregister by the exact task name windowsDaemonTaskName(home) rather than by the bsk-daemon-* pattern plus an argument filter, so the test can only ever remove the task it created.

Nothing else blocks. Errors still surface (every non-refusal exit throws as before), and the persisted task without a logon trigger is documented in the skill reference. CI here runs Linux only, so the Windows evidence is yours. Please include the re-run numbers for the quoting case in the PR body.

Review on code-yeongyu#35: the task action inserted BSK_HOME bare into a cmd.exe line, so a home such as C:\Users\Tom&Jerry split the command. Quoting it was not enough on this path: conhost --headless re-parses the line and still cut it at &, ( and ^ inside the quotes, and cmd.exe expands % even when quoted. The action now runs powershell.exe -EncodedCommand with the home and binary as single-quoted literals, which keep every one of those characters as written.

The Windows test compiles a stand-in bsk into a folder named "bin & (x)" and starts it for homes named "Tom&Jerry (x) ^ ;", "100% %PATH% home" and "O'Brien", asserting the registered action and that the daemon start ran with that exact home. It unregisters by the exact task name it computed.
@LilMGenius

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both items are in 4d8291e.

  • Home path quoting. The quoted cmd.exe form was not enough under conhost --headless: on this machine a task with set "BSK_HOME=<home>"&& ... ran for a plain home and silently did nothing for homes containing &, (x) or ^. So the action now runs powershell.exe -EncodedCommand with the home and binary as single-quoted literals. That keeps &|<>^(), % and ' literal, so nothing needs to be refused.
  • Test with & and %. The test now covers homes Tom&Jerry (x) ^ ;, 100% %PATH% home and O'Brien, with the stand-in bsk in a folder named bin & (x). Each case asserts the registered action and that the daemon start ran with that exact home. All three fail against the previous action and pass now. | is not a legal NTFS file-name character, so it has no case.
  • Exact-name cleanup. windowsDaemonTaskName is exported and the test unregisters exactly that name in finally.

Re-run on Windows 11: bun run test and bun run test:node are 155 pass / 34 fail / 3 skip, against 152 / 34 / 3 on main with the same failing names. The PR body has the updated table.

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both findings are resolved, more robustly than I asked:

  • Quoting: the home and binary paths no longer go through cmd.exe at all. They travel as PowerShell single-quoted literals (' doubled) inside -EncodedCommand (UTF-16LE base64), so no character in a path can be interpreted: &|^() aren't special there and %VAR% doesn't expand. Your own finding, that conhost re-parses a cmd line and drops it at the first &, is a good catch beyond my report.
  • Tests: homes Tom&Jerry (x) ^ ;, 100% %PATH% home and O'Brien, with bsk under bin & (x), against a compiled stand-in that records what it was started with. Each case asserts the registered action and that the daemon really started with exactly that home, and cleans up by the exact task name. You report the three cases fail against the first revision.

The wait for started.txt is a bounded readiness poll on the external effect of the scheduled task, which can't signal back to the test, so it's fine. Approving; I'll merge on green CI.

@code-yeongyu
code-yeongyu merged commit af8cfba into code-yeongyu:main Oct 4, 2026
3 checks passed
@code-yeongyu

Copy link
Copy Markdown
Owner

@LilMGenius a post-merge follow-up from a second look: PowerShell also treats the typographic single quotes (U+2018–U+201B) as quote characters, and literal() doubles only ASCII ', so a profile folder like O’Brien would end the literal early. Details and the one-line fix in #36. Would you take it, since you can run the Windows test? Thanks again for the thorough fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants