Keep browser startup explicitly opt-in (#12435)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The CLI can save a configuration and start the local server in one
command.
> - The onboarding path set a browser-open environment variable without
an explicit user request.
> - Headless test servers use the same onboarding path.
> - Each server restart could therefore open a system browser.
> - This pull request removes the implicit browser-open request and
fixes test servers to disable it explicitly.
> - The benefit is predictable foreground and test startup without
unsolicited browser windows.

## Linked Issues or Issue Description

This is stack 11 of 11. It depends on stack 10.

**What happened?**

`paperclipai onboard --yes --run` set `PAPERCLIP_OPEN_ON_LISTEN=true`.
Headless server users, including browser test runners, opened the system
browser on each server restart.

**Expected behavior**

Server startup must not open a browser unless the caller explicitly sets
`PAPERCLIP_OPEN_ON_LISTEN=true`.

**Steps to reproduce**

1. Run `paperclipai onboard --yes --run` from a clean source checkout.
2. Wait for the server to listen.
3. Observe that the default system browser opens.

**Paperclip version or commit**

Reproduced on `dbf052577` plus the dependent stack.

**Deployment mode**

Local dev from source.

## What Changed

- Stop onboarding from setting `PAPERCLIP_OPEN_ON_LISTEN=true` for
foreground startup.
- Set `PAPERCLIP_OPEN_ON_LISTEN=false` in E2E and issue-detail
performance test servers as defense in depth.
- Preserve the existing explicit environment opt-in in the server.

## Verification

- `pnpm exec vitest run cli/src/__tests__/onboard.test.ts` — 10 tests
passed.
- `pnpm --filter paperclipai typecheck` — passed.
- `pnpm -r typecheck` — passed on the stacked head.
- `pnpm build` — passed on the stacked head.
- Playwright was not run locally by request.

## Risks

- Low risk. The only behavior change removes an unsolicited side effect.
- A caller that wants browser startup can still set
`PAPERCLIP_OPEN_ON_LISTEN=true` explicitly.
- No database or migration change exists in this layer.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5. The exact deployment suffix and context window
are not exposed. The model used reasoning, repository tools, code
execution, Git, and GitHub API access.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Dotta 2026-08-29 12:08:35 -05:00 committed by GitHub
parent a560b48d6d
commit e3eed3a3ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 36 additions and 4 deletions

View File

@ -1,10 +1,16 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { onboard } from "../commands/onboard.js";
import type { PaperclipConfig } from "../config/schema.js";
const runCommandMock = vi.hoisted(() => vi.fn());
vi.mock("../commands/run.js", () => ({
runCommand: runCommandMock,
}));
const ORIGINAL_ENV = { ...process.env };
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_PATH = process.env.PATH;
@ -102,7 +108,9 @@ describe("onboard", () => {
delete process.env.PAPERCLIP_BIND;
delete process.env.PAPERCLIP_BIND_HOST;
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
delete process.env.PAPERCLIP_OPEN_ON_LISTEN;
delete process.env.HOST;
runCommandMock.mockReset();
});
afterEach(() => {
@ -131,6 +139,24 @@ describe("onboard", () => {
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
});
it("does not opt into opening a browser when --yes starts an existing setup", async () => {
const fixture = createExistingConfigFixture();
await onboard({ config: fixture.configPath, yes: true });
expect(runCommandMock).toHaveBeenCalledWith({ config: fixture.configPath, repair: true, yes: true });
expect(process.env.PAPERCLIP_OPEN_ON_LISTEN).toBeUndefined();
});
it("does not opt into opening a browser when --yes starts a fresh setup", async () => {
const configPath = createFreshConfigPath();
await onboard({ config: configPath, yes: true });
expect(runCommandMock).toHaveBeenCalledWith({ config: configPath, repair: true, yes: true });
expect(process.env.PAPERCLIP_OPEN_ON_LISTEN).toBeUndefined();
});
it("backs up invalid config bytes and refuses --yes replacement", async () => {
const configPath = createFreshConfigPath();
const invalidBytes = Buffer.from('{"database": invalid}\n', "utf8");

View File

@ -477,7 +477,6 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
}
if (shouldRunNow && !opts.invokedByRun) {
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
const { runCommand } = await import("./run.js");
await runCommand({ config: configPath, repair: true, yes: true });
return;
@ -746,7 +745,6 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
}
if (shouldRunNow && !opts.invokedByRun) {
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
const { runCommand } = await import("./run.js");
await runCommand({ config: configPath, repair: true, yes: true });
return;

View File

@ -46,12 +46,18 @@ Start immediately after onboarding:
pnpm paperclipai onboard --run
```
Non-interactive defaults + immediate start (opens browser on server listen):
Non-interactive defaults + immediate start (prints the URL without opening a browser):
```sh
pnpm paperclipai onboard --yes
```
Browser opening is opt-in. Set the environment variable explicitly when that is the desired behavior:
```sh
PAPERCLIP_OPEN_ON_LISTEN=true pnpm paperclipai onboard --yes
```
On an existing install, `--yes` now preserves the current config and just starts Paperclip with that setup.
## `paperclipai doctor`

View File

@ -70,6 +70,7 @@ export default defineConfig({
...process.env,
NODE_ENV: "test",
PORT: String(PORT),
PAPERCLIP_OPEN_ON_LISTEN: "false",
PAPERCLIP_API_URL: BASE_URL,
PAPERCLIP_HOME,
PAPERCLIP_INSTANCE_ID,

View File

@ -34,6 +34,7 @@ export default defineConfig({
...process.env,
NODE_ENV: "development",
PORT: String(PORT),
PAPERCLIP_OPEN_ON_LISTEN: "false",
PAPERCLIP_HOME,
PAPERCLIP_INSTANCE_ID,
PAPERCLIP_CONFIG,