From 18ed34d2ebe8c717d5d37c23eb20351ce3b4ab5b Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 15:39:54 +0100
Subject: [PATCH 1/5] Keep copied Unix installation input private on BusyBox
hosts
Select the BusyBox stty applet only when standalone stty is absent, fail before unsafe entry on terminal-control errors, and restore modes with the selected tool. Expose the existing private-file installation alternative on Linux/NAS alongside FreeBSD without changing issuance, TLS, removal or privilege.
Change-source: pulse-maintainer
---
.../v6/internal/subsystems/agent-lifecycle.md | 14 +-
.../v6/internal/subsystems/api-contracts.md | 11 +-
.../build-unix-private-install.mjs | 100 ++++++++++
.../browser-tests/unix-private-install.cjs | 177 ++++++++++++++----
.../InfrastructureOperationsModel.test.tsx | 23 +++
.../infrastructureOperationsModel.tsx | 49 +++--
.../__tests__/agentInstallCommand.test.ts | 166 +++++++++++++++-
.../src/utils/agentInstallCommand.ts | 14 +-
8 files changed, 485 insertions(+), 69 deletions(-)
create mode 100644 frontend-modern/browser-tests/build-unix-private-install.mjs
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index 50e9dc861d..73932f39db 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -9543,7 +9543,7 @@ cross-compiled with `GOOS=windows go build ./internal/hostagent/`.
The shared frontend Unix installer preserves single-line paste grammar,
quoted destinations, explicit custom CA/insecure continuity and profile flags.
Issued credentials are separate from copied commands. A privileged Bash child
-owns the Core-aligned silent bounded reader and temporary 0700/0600 handoff;
+owns the silent bounded reader and temporary 0700/0600 handoff;
maximum-size input must survive unchanged, overflow must not reach the caller's
history, and every exit must restore terminal modes and remove temporary files.
Download/preflight failures never reach credential entry or installation.
@@ -9551,8 +9551,16 @@ Sudo validation occurs before download; download and unprivileged preflight
still precede privileged installation. Token-optional and saved-state update
paths need no terminal or replacement credential.
-For non-terminal FreeBSD command fields, the existing page offers an absolute
-local private-token-file variant, with ownership/regular-file/symlink/mode
+The frontend reader selects standalone `stty`, or `busybox stty` when standalone
+`stty` is absent, and uses the same selected utility to restore terminal modes.
+Missing terminal, missing/unsupported utility, unreadable settings, echo-disable
+failure and restoration failure stop before installation with distinct errors;
+no echoed, inline-token or broader-privilege fallback is permitted. A mode-save
+failure never requests input. Interrupted/oversized input remains private.
+The backend bootstrap mirror remains separately owned by Core until composed.
+
+For Linux/NAS and non-terminal FreeBSD command fields, the existing page offers
+an absolute local private-token-file variant, with ownership/regular-file/symlink/mode
checks and the installer's bounded trusted reader. The operator provisions and
removes that file through a trusted file path; the command neither contains its
contents nor deletes it. A GUI without private file provisioning requires a
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index dfab6e4712..538a6bdcec 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -12034,7 +12034,7 @@ requirements. No runtime paths or required contract updates are exempted.
shell grammar for paste hosts (#2123), canonical URLs, custom CA/insecure
continuity and complete-download-before-execution. Token values select private
entry only and never become shell source, argv or environment. The privileged
-Bash child uses the bounded Core bootstrap reader; 4096-character input is
+Bash child uses the bounded private bootstrap reader; 4096-character input is
preserved, overflow is drained before returning to the caller, and terminal
modes and temporary 0700/0600 files are restored/removed on exit. Download and
unprivileged preflight precede credential entry and installation; sudo
@@ -12044,10 +12044,17 @@ A no-terminal command can use an absolute local `tokenFilePath`. It requires a
privileged-user-owned regular non-symlink 0600 file in an owned non-symlink 0700
parent, passes only its path, and leaves this operator-owned file untouched.
The existing installer's bounded trusted token reader still validates content
-and ancestor trust. The FreeBSD card explains this existing private-file route
+and ancestor trust. The Linux/NAS and FreeBSD cards explain this existing private-file route
and does not put credentials in GUI command fields. It does not claim a GUI can
create private files or establish native appliance installation.
+The frontend terminal reader can select `busybox stty` only when standalone
+`stty` is absent. It distinguishes missing TTY, absent/unsupported tool and
+mode-save/echo-disable/restoration failures, refuses unsafe entry/installation,
+and restores using the selected tool on normal and signal exits. A host without
+a usable tool keeps the existing private-file option; credential issuance and
+transport do not change. Core owns the separate backend-generated reader mirror.
+
Unix install, credential replacement, upgrade and uninstall use the shared
transport. Explicit replacement/uninstall retain canonical agent ID and host;
ordinary updates recover their existing credential/identity from installer
diff --git a/frontend-modern/browser-tests/build-unix-private-install.mjs b/frontend-modern/browser-tests/build-unix-private-install.mjs
new file mode 100644
index 0000000000..62a3920a3e
--- /dev/null
+++ b/frontend-modern/browser-tests/build-unix-private-install.mjs
@@ -0,0 +1,100 @@
+// Compile in the source-proof VM, not the bounded browser process tree.
+// This is a consuming preview, not the production build or a native agent.
+import { build } from 'vite';
+import solid from 'vite-plugin-solid';
+import tailwind from '@tailwindcss/vite';
+import { stop } from 'esbuild';
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import identity from './publication-fixture-source.cjs';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const output = path.resolve(root, '../tmp/unix-private-install/static');
+const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
+const binding = {
+ ...identity.publicationSourceIdentity(process.argv.slice(2), () =>
+ execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }),
+ ),
+ sources: {},
+ artifacts: {},
+};
+try {
+ await build({
+ root,
+ base: '/',
+ configFile: false,
+ publicDir: false,
+ plugins: [
+ {
+ name: 'private-input-consumer-binding',
+ generateBundle() {
+ for (const id of this.getModuleIds()) {
+ const file = id.split('?')[0];
+ if (
+ file.startsWith(root + '/') &&
+ !file.includes('/node_modules/') &&
+ fs.existsSync(file) &&
+ fs.statSync(file).isFile()
+ )
+ binding.sources[path.relative(root, file)] = hash(fs.readFileSync(file));
+ }
+ for (const file of [
+ 'src/utils/agentInstallCommand.ts',
+ 'src/components/Settings/infrastructureOperationsModel.tsx',
+ 'src/components/Settings/InfrastructureInstallerSection.tsx',
+ 'src/components/Settings/InfrastructureAgentDoctorPage.tsx',
+ 'src/components/Settings/useInfrastructureOperationsState.tsx',
+ 'src/components/Settings/useInfrastructureInstallState.tsx',
+ 'browser-tests/unix-private-install.tsx',
+ ])
+ if (!binding.sources[file]) throw new Error(`Consumer not compiled: ${file}`);
+ },
+ },
+ tailwind(),
+ solid(),
+ ],
+ resolve: { alias: { '@': path.join(root, 'src') } },
+ // This fixture targets the installed current browser, not a legacy-engine
+ // qualification. Match the other consuming preview's non-lowering target.
+ build: {
+ target: 'esnext',
+ outDir: output,
+ emptyOutDir: true,
+ rollupOptions: { input: path.join(root, 'browser-tests/unix-private-install.html') },
+ },
+ });
+ const bind = (directory) => {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const file = path.join(directory, entry.name);
+ if (entry.isDirectory()) bind(file);
+ else if (/\.(css|js|jsx|ts|tsx|html|mjs|cjs)$/.test(file))
+ binding.sources[path.relative(root, file)] = hash(fs.readFileSync(file));
+ }
+ };
+ // Tailwind scans source beyond the consumer graph. Bind those inputs too.
+ bind(path.join(root, 'src'));
+ bind(path.join(root, 'browser-tests'));
+ for (const file of ['package.json', 'package-lock.json', 'tsconfig.json', 'index.html'])
+ binding.sources[file] = hash(fs.readFileSync(path.join(root, file)));
+ const artifacts = (directory) => {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const file = path.join(directory, entry.name);
+ if (entry.isDirectory()) artifacts(file);
+ else binding.artifacts[path.relative(output, file)] = hash(fs.readFileSync(file));
+ }
+ };
+ artifacts(output);
+ fs.writeFileSync(path.join(output, 'binding.json'), JSON.stringify(binding, null, 2) + '\n');
+ console.log(
+ JSON.stringify({
+ source: binding.source_sha,
+ source_inputs: Object.keys(binding.sources).length,
+ artifacts: Object.keys(binding.artifacts).length,
+ }),
+ );
+} finally {
+ stop();
+}
diff --git a/frontend-modern/browser-tests/unix-private-install.cjs b/frontend-modern/browser-tests/unix-private-install.cjs
index 96e520158d..3c8c10a1fb 100644
--- a/frontend-modern/browser-tests/unix-private-install.cjs
+++ b/frontend-modern/browser-tests/unix-private-install.cjs
@@ -2,31 +2,56 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
+const http = require('node:http');
+const crypto = require('node:crypto');
const { chromium, webkit } = require('playwright');
+const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
(async () => {
const root = '/workspace/frontend-modern';
- process.chdir(root);
- console.log(
- JSON.stringify({
- fixtureHTML: fs.existsSync(path.join(root, 'browser-tests/unix-private-install.html')),
- fixtureTSX: fs.existsSync(path.join(root, 'browser-tests/unix-private-install.tsx')),
- }),
- );
- const artifacts = path.join(root, 'node_modules', 'unix-private-browser');
- fs.mkdirSync(artifacts, { recursive: true });
- const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
- const server = await createServer({
- root,
- configFile: path.join(root, 'vite.config.ts'),
- server: { host: '127.0.0.1', port: 5241, strictPort: true },
+ const artifacts = '/workspace/tmp/unix-private-install';
+ const preview = path.join(artifacts, 'static');
+ const bindingBytes = fs.readFileSync(path.join(preview, 'binding.json'));
+ const binding = JSON.parse(bindingBytes);
+ for (const [file, expected] of Object.entries(binding.sources))
+ assert.equal(hash(fs.readFileSync(path.join(root, file))), expected, file);
+ for (const [file, expected] of Object.entries(binding.artifacts))
+ assert.equal(hash(fs.readFileSync(path.join(preview, file))), expected, file);
+ const served = [];
+ const server = http.createServer((req, res) => {
+ const file = new URL(req.url, 'http://fixture.invalid').pathname.slice(1);
+ if (!['GET', 'HEAD'].includes(req.method) || !Object.hasOwn(binding.artifacts, file)) {
+ res.writeHead(404);
+ res.end();
+ return;
+ }
+ const bytes = fs.readFileSync(path.join(preview, file));
+ assert.equal(hash(bytes), binding.artifacts[file]);
+ served.push({ file, sha256: hash(bytes) });
+ res.writeHead(200, {
+ 'Content-Type': file.endsWith('.js')
+ ? 'text/javascript'
+ : file.endsWith('.css')
+ ? 'text/css'
+ : 'text/html',
+ 'Cache-Control': 'no-store',
+ });
+ res.end(bytes);
});
+ const captures = [],
+ consumerCommands = [],
+ requests = [];
+ let cleanup = { browser_closed: false, server_closed: false };
const results = [];
let browser;
const secret = 'browser-synthetic-install-token';
try {
- await server.listen();
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
+ const origin = `http://127.0.0.1:${server.address().port}`;
+ assert.equal(require('playwright/package.json').version, '1.56.1');
for (const [engine, width, tone] of [
['chromium', 1280, 'light'],
+ ['chromium', 1280, 'dark'],
+ ['webkit', 390, 'light'],
['webkit', 390, 'dark'],
]) {
browser = await { chromium, webkit }[engine].launch(
@@ -39,6 +64,36 @@ const { chromium, webkit } = require('playwright');
...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
});
const errors = [];
+ const capture = async (name, locator) => {
+ if (locator)
+ await locator.evaluate((el) => window.scrollBy(0, el.getBoundingClientRect().top - 48));
+ await page.evaluate(
+ () =>
+ new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))),
+ );
+ assert.equal(
+ await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
+ true,
+ name,
+ );
+ const file = `${engine}-${tone}-${name}.png`;
+ await page.screenshot({ path: path.join(artifacts, file) });
+ captures.push({
+ file,
+ sha256: hash(fs.readFileSync(path.join(artifacts, file))),
+ engine,
+ tone,
+ width,
+ height: 900,
+ });
+ };
+ const activate = async (locator) => {
+ if (engine === 'webkit') await locator.tap();
+ else {
+ await locator.focus();
+ await page.keyboard.press('Enter');
+ }
+ };
page.on('pageerror', (e) => errors.push(e.message));
page.on('console', (m) => {
if (m.type() === 'error') {
@@ -55,6 +110,9 @@ const { chromium, webkit } = require('playwright');
value: { writeText: async (text) => window.__copies.push(text) },
});
});
+ page.on('request', (req) =>
+ requests.push({ path: new URL(req.url()).pathname, method: req.method() }),
+ );
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url());
if (!url.pathname.startsWith('/api/')) return route.continue();
@@ -88,7 +146,7 @@ const { chromium, webkit } = require('playwright');
return route.fulfill({ json: { connectedInfrastructure: [] } });
return route.fulfill({ json: { data: [] } });
});
- await page.goto('http://127.0.0.1:5241/browser-tests/unix-private-install.html');
+ await page.goto(`${origin}/browser-tests/unix-private-install.html`);
await page.waitForTimeout(1000);
console.log('PAGE_TITLE', await page.title());
console.log(
@@ -115,7 +173,7 @@ const { chromium, webkit } = require('playwright');
await copyToken.focus();
await page.keyboard.press('Enter');
assert.equal(await page.evaluate(() => window.__copies.at(-1)), secret);
- await page.screenshot({ path: path.join(artifacts, `${engine}-token.png`), fullPage: true });
+ await capture('token-dialog');
await page.keyboard.press('Escape');
await dialog.waitFor({ state: 'hidden' });
// The real component's platform cards and copy handlers.
@@ -126,10 +184,14 @@ const { chromium, webkit } = require('playwright');
assert.ok(!command.includes(secret));
assert.ok(command.includes('--token-file "$token_file"'));
assert.ok(!/[\r\n]/.test(command));
+ assert.ok(command.includes('tty_control=(busybox stty)'));
+ assert.ok(command.includes('Cannot disable terminal echo'));
+ assert.ok(command.includes('private token-file command'));
}
const guiCode = page.locator('code').filter({ hasText: 'token_parent=${token_file%/*}' });
- assert.equal(await guiCode.count(), 1);
- const guiCommand = await guiCode.innerText();
+ assert.equal(await guiCode.count(), 2);
+ const guiCommand = await guiCode.first().innerText();
+ assert.equal(await guiCode.last().innerText(), guiCommand);
assert.ok(guiCommand.includes('/root/.config/pulse-agent/bootstrap-token'));
assert.ok(!guiCommand.includes('read -r'));
assert.ok(!guiCommand.includes(secret));
@@ -138,22 +200,45 @@ const { chromium, webkit } = require('playwright');
'Never put the token in a GUI command field',
),
);
- await page.getByRole('button', { name: 'Copy Install command', exact: true }).click();
+ await activate(page.getByRole('button', { name: 'Copy Install command', exact: true }));
assert.equal(await page.evaluate(() => window.__copies.at(-1)), unixCommands[0]);
- await page
+ const fileCopy = page
.getByRole('button', {
name: 'Copy Install from a private token file (no terminal) command',
exact: true,
})
- .click();
+ .first();
+ await activate(fileCopy);
+ const linuxCard = page
+ .getByRole('heading', { name: 'Install on Linux', exact: true })
+ .locator('..');
+ assert.match(await linuxCard.innerText(), /BusyBox/);
+ assert.match(await linuxCard.innerText(), /root-owned/);
+ await capture('linux-private-file', fileCopy);
assert.equal(await page.evaluate(() => window.__copies.at(-1)), guiCommand);
+ consumerCommands.push({ engine, tone, kind: 'linux-file', command: guiCommand });
+ await activate(
+ page.getByRole('button', { name: 'Show details for doctor-fixture', exact: true }),
+ );
await page
.getByRole('button', {
name: 'Copy authentication repair command for doctor-fixture',
exact: true,
})
.click();
- assert.ok(!(await page.evaluate(() => window.__copies.at(-1))).includes(secret));
+ const doctorCommand = await page.evaluate(() => window.__copies.at(-1));
+ assert.ok(!doctorCommand.includes(secret));
+ assert.ok(doctorCommand.includes('tty_control=(busybox stty)'));
+ assert.ok(doctorCommand.includes('--update --enable-docker'));
+ assert.ok(doctorCommand.includes('doctor-fixture'));
+ consumerCommands.push({ engine, tone, kind: 'doctor-repair', command: doctorCommand });
+ await capture(
+ 'doctor-repair',
+ page.getByRole('button', {
+ name: 'Copy authentication repair command for doctor-fixture',
+ exact: true,
+ }),
+ );
await page.getByRole('button', { name: 'Show token only', exact: true }).click();
await dialog.waitFor();
@@ -203,12 +288,13 @@ const { chromium, webkit } = require('playwright');
'Never insert the token into the command',
),
);
- await page.screenshot({
- path: path.join(artifacts, `${engine}-commands.png`),
- fullPage: true,
- });
+ await capture(
+ 'trust-options',
+ page.getByLabel('Custom CA certificate path (optional)', { exact: true }),
+ );
+ consumerCommands.push({ engine, tone, kind: 'linux-install', command: unixCommands[0] });
optionalAuth = true;
- await page.goto('http://127.0.0.1:5241/browser-tests/unix-private-install.html?optional=1');
+ await page.goto(`${origin}/browser-tests/unix-private-install.html?optional=1`);
await page.getByRole('button', { name: 'Confirm without token', exact: true }).click();
const optionalCode = page.locator('code').filter({ hasText: 'bootstrap_dir=$(mktemp' });
const optionalCommands = await optionalCode.allTextContents();
@@ -242,15 +328,38 @@ const { chromium, webkit } = require('playwright');
await browser.close();
browser = undefined;
}
- fs.writeFileSync(
- path.join(artifacts, 'result.json'),
- JSON.stringify({ playwright: require('playwright/package.json').version, results }, null, 2),
- );
- console.log(JSON.stringify({ result: 'passed', artifacts, results }));
+ assert.equal(captures.length, 16);
} finally {
if (browser) await browser.close();
- await server.close();
+ cleanup.browser_closed = true;
+ await new Promise((resolve) => server.close(resolve));
+ cleanup.server_closed = true;
+ fs.writeFileSync(
+ path.join(artifacts, 'consumer-commands.json'),
+ JSON.stringify({ source: binding.source_sha, consumerCommands }, null, 2) + '\n',
+ );
+ fs.writeFileSync(
+ path.join(artifacts, 'result.json'),
+ JSON.stringify(
+ {
+ source: binding.source_sha,
+ binding_sha256: hash(bindingBytes),
+ source_inputs: Object.keys(binding.sources).length,
+ playwright: require('playwright/package.json').version,
+ results,
+ captures,
+ requests,
+ served,
+ cleanup,
+ },
+ null,
+ 2,
+ ) + '\n',
+ );
}
+ console.log(
+ JSON.stringify({ result: 'passed', artifacts, results, captures: captures.length, cleanup }),
+ );
})().catch((e) => {
console.error(e);
process.exitCode = 1;
diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx
index 42ccaecf25..a5c2b4087a 100644
--- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx
@@ -8,6 +8,7 @@ import useInfrastructureInstallStateSource from '../useInfrastructureInstallStat
import { resolveAgentCommandPlatform } from '@/utils/agentInstallCommand';
import {
INSTALL_PROFILE_OPTIONS,
+ buildCommandsByPlatform,
getCapabilityManagementPath,
getCapabilitySurfaceLabel,
getPlatformConnectionsViewForCapability,
@@ -18,6 +19,28 @@ import {
} from '../infrastructureOperationsModel';
describe('infrastructure operations model', () => {
+ it('offers the existing private-file alternative on Linux/NAS and FreeBSD only when supplied', () => {
+ const cards = buildCommandsByPlatform(
+ 'prompt-command',
+ 'windows',
+ 'windows-args',
+ 'file-command',
+ );
+ for (const platform of ['linux', 'freebsd'] as const) {
+ expect(cards[platform].snippets.map((snippet) => snippet.command)).toEqual([
+ 'prompt-command',
+ 'file-command',
+ ]);
+ expect(cards[platform].snippets[1].label).toBe(
+ 'Install from a private token file (no terminal)',
+ );
+ }
+ const tokenless = buildCommandsByPlatform('no-token', 'windows', 'windows-args');
+ expect(tokenless.linux.snippets).toHaveLength(1);
+ expect(tokenless.freebsd.snippets).toHaveLength(1);
+ // Do not silently point a macOS administrator at Linux's /root path.
+ expect(cards.macos.snippets).toHaveLength(1);
+ });
it('builds unified rows from connected infrastructure surfaces', () => {
const item: ConnectedInfrastructureItem = {
id: 'agent-1',
diff --git a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
index dbc1fafc4b..305f1da96c 100644
--- a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
+++ b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
@@ -329,6 +329,30 @@ export const INSTALL_PROFILE_OPTIONS: InstallProfileOption[] = [
},
];
+// The same already-supported file handoff is useful on NAS/Linux hosts whose
+// console has no usable stty, not only in FreeBSD GUI command fields.
+const privateFileInstallSnippets = (command?: string): InfrastructureCommandSnippet[] =>
+ command
+ ? [
+ {
+ label: 'Install from a private token file (no terminal)',
+ command,
+ note: (
+
+ Use this when there is no terminal or no usable stty or BusyBox{' '}
+ stty. Before running, save the separately revealed token to{' '}
+ /root/.config/pulse-agent/bootstrap-token using a trusted file editor or
+ upload path. The file must be root-owned with mode 0600 in a root-owned{' '}
+ 0700 directory. Never put the token in a GUI command field. This command
+ does not prompt or delete your file. Remove it through the same trusted file path
+ afterwards. If your GUI cannot create private files, use console or SSH instead. Bash
+ must already be installed.
+
+ ),
+ },
+ ]
+ : [];
+
export const buildCommandsByPlatform = (
unixCommand: string,
windowsInteractiveCommand: string,
@@ -348,10 +372,13 @@ export const buildCommandsByPlatform = (
Command auto-escalates with sudo when available. Otherwise run from a root
shell (for example su -). If a token is required, run this command first,
then paste the separately revealed token at its silent prompt. Auto-detects your init
- system and works on Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more.
+ system and works on Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more.{' '}
+ The prompt needs stty or BusyBox stty; otherwise use the
+ private-file command below.
),
},
+ ...privateFileInstallSnippets(unixPrivateFileCommand),
],
},
macos: {
@@ -392,25 +419,7 @@ export const buildCommandsByPlatform = (
),
},
- ...(unixPrivateFileCommand
- ? [
- {
- label: 'Install from a private token file (no terminal)',
- command: unixPrivateFileCommand,
- note: (
-
- Before running, save the separately revealed token to{' '}
- /root/.config/pulse-agent/bootstrap-token using a trusted file editor
- or upload path. The file must be root-owned with mode 0600 in a
- root-owned 0700 directory. Never put the token in a GUI command
- field. This command does not prompt or delete your file. Remove it through the
- same trusted file path afterwards. If your GUI cannot create private files, use
- console or SSH instead. Bash must already be installed.
-
- ),
- },
- ]
- : []),
+ ...privateFileInstallSnippets(unixPrivateFileCommand),
],
},
windows: {
diff --git a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
index b5b1f87ba1..6b66dd6068 100644
--- a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
+++ b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
@@ -444,7 +444,8 @@ sys.stdout.buffer.write(output)
sys.exit(code if code >= 0 else 128 - code)
`;
-const makeUnixFixture = () => {
+type TtyTool = 'stty' | 'busybox' | 'both' | 'none' | 'busybox-without-stty';
+const makeUnixFixture = (ttyTool: TtyTool = 'stty', isolatedPath = false) => {
const dir = mkdtempSync(join(tmpdir(), 'pulse-private-unix-'));
chmodSync(dir, 0o700);
const trace = join(dir, 'trace');
@@ -454,10 +455,37 @@ const makeUnixFixture = () => {
const fake = (name: string, body: string) => {
writeFileSync(join(bin, name), `#!/bin/sh\n${body}\n`, { mode: 0o700 });
};
- fake(
- 'stty',
- 'if [ \"$1\" = -echo ]; then echo \"$PPID\" > \"$READER_PID\"; fi; exec /bin/stty \"$@\"',
- );
+ const ttyBody = `
+printf '%s\\n' "$0 $*" >> "$TTY_TRACE"
+[ "$FAIL_AT:$1" != tty-state:-g ] || exit 1
+[ "$FAIL_AT:$1" != tty-state-empty:-g ] || exit 0
+[ "$FAIL_AT:$1" != tty-echo:-echo ] || exit 1
+if [ "$FAIL_AT" = tty-restore ] && [ "$1" != -g ] && [ "$1" != -echo ] && [ ! -e "$RESTORE_FAILED" ]; then
+ : > "$RESTORE_FAILED"; exit 1
+fi
+if [ "$1" = -echo ]; then echo "$PPID" > "$READER_PID"; fi
+`;
+ if (ttyTool === 'stty' || ttyTool === 'both') fake('stty', ttyBody + 'exec /bin/stty "$@"');
+ if (ttyTool === 'busybox' || ttyTool === 'both')
+ fake(
+ 'busybox',
+ `[ "$1" = stty ] || exit 127; shift
+${ttyBody}
+# CI's shim exercises dispatch/modes without adding a package dependency.
+# Source proof supplies an actual static BusyBox binary as an explicit input.
+if [ -n "\${PULSE_TEST_BUSYBOX:-}" ]; then exec "$PULSE_TEST_BUSYBOX" stty "$@"; fi
+exec /bin/stty "$@"`,
+ );
+ if (ttyTool === 'busybox-without-stty') fake('busybox', 'exit 127');
+ if (isolatedPath) {
+ // No /bin in PATH: an accidentally inherited standalone stty must not
+ // turn the absent-tool control into a successful prompt.
+ for (const program of ['bash', 'sh', 'python3', 'cp', 'cat', 'stat', 'dirname', 'rm', 'rmdir'])
+ symlinkSync(
+ execFileSync('which', [program], { encoding: 'utf8' }).trim(),
+ join(bin, program),
+ );
+ }
fake('id', 'if [ "$1" = -u ]; then echo "$FAKE_UID"; else exec /usr/bin/id "$@"; fi');
fake(
'sudo',
@@ -507,7 +535,7 @@ if [ "$FAIL_AT" = TERM ] || [ "$FAIL_AT" = HUP ]; then kill -s "$FAIL_AT" "$PPID
trace,
env: (uid = '0', failure = '') => ({
...process.env,
- PATH: `${bin}:${process.env.PATH}`,
+ PATH: isolatedPath ? bin : `${bin}:${process.env.PATH}`,
FAKE_UID: uid,
FAIL_AT: failure,
TRACE: trace,
@@ -517,6 +545,8 @@ if [ "$FAIL_AT" = TERM ] || [ "$FAIL_AT" = HUP ]; then kill -s "$FAIL_AT" "$PPID
CAPTURE: join(dir, 'capture'),
SECRET_ENV: join(dir, 'secret-env'),
READER_PID: join(dir, 'reader-pid'),
+ TTY_TRACE: join(dir, 'tty-trace'),
+ RESTORE_FAILED: join(dir, 'restore-failed'),
PULSE_TOKEN: '',
}),
close: () => rmSync(dir, { recursive: true, force: true }),
@@ -852,6 +882,130 @@ describe('credential-free Unix lifecycle execution', { timeout: 25_000 }, () =>
);
});
+describe('terminal-control portability', { timeout: 25_000 }, () => {
+ const command = () =>
+ buildUnixAgentInstallCommand({ baseUrl: 'https://pulse.invalid', token: 'present' });
+
+ it.each(['stty', 'busybox', 'both'] as const)(
+ 'keeps maximum input private and restores modes with %s',
+ (tool) => {
+ const fixture = makeUnixFixture(tool, true);
+ const input = 'portable-private-'.padEnd(4096, 'x');
+ const history = join(fixture.dir, 'history');
+ try {
+ const result = runPrivateUnix(command(), input, fixture.env(), history);
+ expect(result.status, result.stdout + result.stderr).toBe(0);
+ expect(readFileSync(join(fixture.dir, 'capture'), 'utf8')).toBe(input);
+ expect(readFileSync(join(fixture.dir, 'secret-env'), 'utf8')).toBe(':');
+ const trace = readFileSync(join(fixture.dir, 'tty-trace'), 'utf8');
+ expect(trace).toContain(`${tool === 'busybox' ? 'busybox' : 'stty'} -g`);
+ if (tool === 'both') expect(trace).not.toContain('busybox');
+ for (const text of [
+ result.stdout,
+ result.stderr,
+ readFileSync(history, 'utf8'),
+ readFileSync(join(fixture.dir, 'argv'), 'utf8'),
+ ])
+ expect(text).not.toContain(input);
+ assertUnixCleanup(fixture.dir);
+ } finally {
+ fixture.close();
+ }
+ },
+ );
+
+ it.each(['blank', 'overflow', 'HUP', 'INT', 'TERM'])(
+ 'refuses %s with applet-only input, without leaks or leftover modes/files',
+ (failure) => {
+ const fixture = makeUnixFixture('busybox', true);
+ const input = failure === 'overflow' ? 'x'.repeat(10000) + 'PRIVATE_OVERFLOW_SENTINEL' : '';
+ const history = join(fixture.dir, 'history');
+ try {
+ const result = runPrivateUnix(
+ command(),
+ input,
+ fixture.env(),
+ history,
+ ['HUP', 'INT', 'TERM'].includes(failure) ? failure : undefined,
+ );
+ expect(result.status, result.stdout + result.stderr).not.toBe(0);
+ expect(result.stdout).not.toMatch(/deadline expired|did not restore/);
+ expect(existsSync(join(fixture.dir, 'capture'))).toBe(false);
+ for (const text of [result.stdout, result.stderr, readFileSync(history, 'utf8')])
+ expect(text).not.toMatch(/PRIVATE_OVERFLOW_SENTINEL|x{512}/);
+ assertUnixCleanup(fixture.dir);
+ } finally {
+ fixture.close();
+ }
+ },
+ );
+
+ it.each([
+ ['none', '', 'Silent token input requires stty or BusyBox stty'],
+ ['busybox-without-stty', '', 'Cannot read terminal settings'],
+ ['stty', 'tty-state', 'Cannot read terminal settings'],
+ ['busybox', 'tty-state', 'Cannot read terminal settings'],
+ ['stty', 'tty-state-empty', 'Cannot read terminal settings'],
+ ['busybox', 'tty-state-empty', 'Cannot read terminal settings'],
+ ['stty', 'tty-echo', 'Cannot disable terminal echo'],
+ ['busybox', 'tty-echo', 'Cannot disable terminal echo'],
+ ['stty', 'tty-restore', 'Cannot restore terminal settings'],
+ ['busybox', 'tty-restore', 'Cannot restore terminal settings'],
+ ] as const)(
+ 'diagnoses %s/%s without pretending a real TTY is missing',
+ (tool, failure, error) => {
+ const fixture = makeUnixFixture(tool, true);
+ try {
+ const result = runPrivateUnix(command(), 'private-input', fixture.env('0', failure));
+ expect(result.status, result.stdout + result.stderr).not.toBe(0);
+ expect(result.stdout).toContain(error);
+ expect(result.stdout).toContain('No installation was attempted');
+ expect(result.stdout).not.toMatch(
+ /A terminal is required|command not found|deadline expired|did not restore|private-input/,
+ );
+ expect(existsSync(join(fixture.dir, 'capture'))).toBe(false);
+ assertUnixCleanup(fixture.dir);
+ } finally {
+ fixture.close();
+ }
+ },
+ );
+
+ it.each(['stty', 'busybox', 'none'] as const)('no TTY fails before selecting %s', (tool) => {
+ const fixture = makeUnixFixture(tool, true);
+ try {
+ const result = spawnSync('sh', ['-c', command()], { env: fixture.env(), encoding: 'utf8' });
+ expect(result.status, result.stdout + result.stderr).not.toBe(0);
+ expect(result.stderr).toContain('A terminal is required');
+ expect(result.stderr).toContain('private token-file command');
+ expect(existsSync(join(fixture.dir, 'tty-trace'))).toBe(false);
+ expect(existsSync(join(fixture.dir, 'capture'))).toBe(false);
+ assertUnixCleanup(fixture.dir);
+ } finally {
+ fixture.close();
+ }
+ });
+
+ it('uses the existing private-file handoff with neither TTY nor terminal utilities', () => {
+ const fixture = makeUnixFixture('none', true);
+ try {
+ const file = join(fixture.dir, 'operator-token');
+ writeFileSync(file, 'file-input', { mode: 0o600 });
+ const cmd = buildUnixAgentInstallCommand({
+ baseUrl: 'https://pulse.invalid',
+ tokenFilePath: file,
+ });
+ const result = spawnSync('sh', ['-c', cmd], { env: fixture.env(), encoding: 'utf8' });
+ expect(result.status, result.stdout + result.stderr).toBe(0);
+ expect(readFileSync(join(fixture.dir, 'capture'), 'utf8')).toBe('file-input');
+ expect(readFileSync(file, 'utf8')).toBe('file-input');
+ assertUnixCleanup(fixture.dir);
+ } finally {
+ fixture.close();
+ }
+ });
+});
+
describe('saved-credential uninstall commands', () => {
it.each(['unused-new-install-token', null])(
'never asks for installation credentials (%s)',
diff --git a/frontend-modern/src/utils/agentInstallCommand.ts b/frontend-modern/src/utils/agentInstallCommand.ts
index c82158b381..130a54db37 100644
--- a/frontend-modern/src/utils/agentInstallCommand.ts
+++ b/frontend-modern/src/utils/agentInstallCommand.ts
@@ -205,13 +205,19 @@ export const buildUnixAgentInstallCommand = ({
'token_dir=$(mktemp -d /tmp/pulse-agent-bootstrap.XXXXXX);',
'token_file="$token_dir/token";',
'unset pulse_token pulse_discard tty_state;',
- 'cleanup() { unset pulse_token pulse_discard; if [ -n "${tty_state:-}" ]; then stty "$tty_state" /dev/null || :; fi; rm -f -- "$token_file"; rmdir -- "$token_dir"; };',
+ 'tty_control=();',
+ 'cleanup() { unset pulse_token pulse_discard; if [ -n "${tty_state:-}" ]; then "${tty_control[@]}" "$tty_state" /dev/null || :; fi; rm -f -- "$token_file"; rmdir -- "$token_dir"; };',
"trap cleanup EXIT; trap 'exit 129' HUP; trap 'exit 130' INT; trap 'exit 143' TERM;",
- 'if ! tty_state=$(stty -g &2; exit 1; fi;',
- 'stty -echo /dev/null; then echo "A terminal is required to enter the token. Use a private token-file command instead. No installation was attempted." >&2; exit 1; fi;',
+ 'if command -v stty >/dev/null 2>&1; then tty_control=(stty); elif command -v busybox >/dev/null 2>&1; then tty_control=(busybox stty); else echo "Silent token input requires stty or BusyBox stty. Use a private token-file command instead. No installation was attempted." >&2; exit 1; fi;',
+ 'if ! tty_state=$("${tty_control[@]}" -g /dev/null) || [ -z "$tty_state" ]; then echo "Cannot read terminal settings with stty or BusyBox stty. Use a private token-file command instead. No installation was attempted." >&2; exit 1; fi;',
+ 'if ! "${tty_control[@]}" -echo /dev/null; then echo "Cannot disable terminal echo. No token was requested and no installation was attempted." >&2; exit 1; fi;',
`if ! IFS= read -r -s -p ${shellQuoteArg('Pulse agent token (paste at this prompt, not in the command): ')} -n 4097 pulse_token &2; exit 1; fi;`,
'if [ "${#pulse_token}" -gt 4096 ]; then while IFS= read -r -s -n 4097 pulse_discard /dev/null; then echo "Cannot restore terminal settings. No installation was attempted." >&2; exit 1; fi; unset tty_state;',
"printf '\\n' >/dev/tty;",
'if [ -z "$pulse_token" ] || [ "${#pulse_token}" -gt 4096 ]; then echo "A non-empty token of at most 4096 characters is required." >&2; exit 1; fi;',
'printf %s "$pulse_token" > "$token_file"; unset pulse_token;',
From 0a20fdc3ef8236983ac35454e77dc2e88f87a48a Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 15:48:04 +0100
Subject: [PATCH 2/5] Check all saved terminal attributes after private input
The PTY fixture compares the complete saved terminal state, not only echo and canonical flags.
Contract-Neutral: Existing terminal-restoration obligations are unchanged; this strengthens their executable verification.
Change-source: pulse-maintainer
---
.../src/utils/__tests__/agentInstallCommand.test.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
index 6b66dd6068..b56e8c0f33 100644
--- a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
+++ b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
@@ -431,8 +431,7 @@ else:
output += b"\\nPTY fixture deadline expired\\n"
code = child.wait()
final_tty = termios.tcgetattr(tty_watch)
-mode_mask = termios.ECHO | termios.ICANON
-if (initial_tty[3] & mode_mask) != (final_tty[3] & mode_mask):
+if initial_tty != final_tty:
output += b"\\nBootstrap did not restore terminal echo/canonical mode\\n"
code = 1
if echo_at_prompt:
From 5b996ef9f63354a2c665e11c874e91a433bb2ba5 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 15:48:36 +0100
Subject: [PATCH 3/5] Use sentences in private-input fallback guidance
Correct the copy-audit finding without changing the reader, file handoff or platform choices.
Contract-Neutral: Copy-style correction only, with no behaviour or contract delta.
Change-source: pulse-maintainer
---
.../src/components/Settings/infrastructureOperationsModel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
index 305f1da96c..09d42c907c 100644
--- a/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
+++ b/frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx
@@ -373,7 +373,7 @@ export const buildCommandsByPlatform = (
shell (for example su -). If a token is required, run this command first,
then paste the separately revealed token at its silent prompt. Auto-detects your init
system and works on Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more.{' '}
- The prompt needs stty or BusyBox stty; otherwise use the
+ The prompt needs stty or BusyBox stty. Otherwise use the
private-file command below.
),
From 14b9b3fbaca62696741aa19845eedbde1cbbb28c Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:22:48 +0100
Subject: [PATCH 4/5] Emit the private reader once and exercise the real caller
prompts
Use a POSIX root/sudo prefix inside the isolated subshell rather than duplicating the long quoted reader. Synchronise PTY command entry and exit with the caller prompt, retain full terminal/privacy/overflow assertions, and follow current help rows by their labels. The browser driver now inspects the full Linux card.
Change-source: pulse-maintainer
---
.../v6/internal/subsystems/agent-lifecycle.md | 6 +++
.../v6/internal/subsystems/api-contracts.md | 6 +++
.../browser-tests/unix-private-install.cjs | 1 +
...reOperationsState.branchcov0722pm.test.tsx | 6 ++-
.../troubleshootingEntryGuide.test.ts | 36 +++++++++---------
.../__tests__/agentInstallCommand.test.ts | 37 ++++++++++++++++---
.../src/utils/agentInstallCommand.ts | 6 ++-
7 files changed, 73 insertions(+), 25 deletions(-)
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index 73932f39db..9c83d2411f 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -9551,6 +9551,12 @@ Sudo validation occurs before download; download and unprivileged preflight
still precede privileged installation. Token-optional and saved-state update
paths need no terminal or replacement credential.
+Root/sudo dispatch emits the quoted private reader only once via an isolated
+POSIX positional-argument prefix. Normal quoted URL/profile/identity variants
+remain below the common 4096-byte terminal line boundary. Actual interactive
+PTY controls wait for the caller prompt before pasting and before exiting, so
+startup truncation or a queued exit is not mistaken for a reader failure.
+
The frontend reader selects standalone `stty`, or `busybox stty` when standalone
`stty` is absent, and uses the same selected utility to restore terminal modes.
Missing terminal, missing/unsupported utility, unreadable settings, echo-disable
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 538a6bdcec..627ca0c045 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -12048,6 +12048,12 @@ and ancestor trust. The Linux/NAS and FreeBSD cards explain this existing privat
and does not put credentials in GUI command fields. It does not claim a GUI can
create private files or establish native appliance installation.
+Root/sudo dispatch emits the quoted private reader only once via an isolated
+POSIX positional-argument prefix. Normal quoted URL/profile/identity variants
+remain below the common 4096-byte terminal line boundary. Actual interactive
+PTY controls wait for the caller prompt before pasting and before exiting, so
+startup truncation or a queued exit is not mistaken for a reader failure.
+
The frontend terminal reader can select `busybox stty` only when standalone
`stty` is absent. It distinguishes missing TTY, absent/unsupported tool and
mode-save/echo-disable/restoration failures, refuses unsafe entry/installation,
diff --git a/frontend-modern/browser-tests/unix-private-install.cjs b/frontend-modern/browser-tests/unix-private-install.cjs
index 3c8c10a1fb..62f3566191 100644
--- a/frontend-modern/browser-tests/unix-private-install.cjs
+++ b/frontend-modern/browser-tests/unix-private-install.cjs
@@ -211,6 +211,7 @@ const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
await activate(fileCopy);
const linuxCard = page
.getByRole('heading', { name: 'Install on Linux', exact: true })
+ .locator('..')
.locator('..');
assert.match(await linuxCard.innerText(), /BusyBox/);
assert.match(await linuxCard.innerText(), /root-owned/);
diff --git a/frontend-modern/src/components/Settings/__tests__/useInfrastructureOperationsState.branchcov0722pm.test.tsx b/frontend-modern/src/components/Settings/__tests__/useInfrastructureOperationsState.branchcov0722pm.test.tsx
index 18254c397a..0a3fe7187c 100644
--- a/frontend-modern/src/components/Settings/__tests__/useInfrastructureOperationsState.branchcov0722pm.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/useInfrastructureOperationsState.branchcov0722pm.test.tsx
@@ -167,7 +167,8 @@ describe('useInfrastructureOperationsState command-building closures', () => {
state.setCustomAgentUrl(HTTPS_URL);
const cmd = state.getUninstallCommand();
expect(cmd).toContain('--uninstall');
- expect(cmd).toContain('sudo bash -c');
+ expect(cmd).toContain('set -- sudo');
+ expect(cmd).toContain('\"$@\" bash -c');
expect(cmd).not.toContain('--preflight-only');
expect(cmd).not.toContain('| bash');
expect(cmd).not.toContain("--token '");
@@ -634,7 +635,8 @@ describe('useInfrastructureOperationsState command-building closures', () => {
const cmd = state.getAgentConnectionUpgradeCommand(baseConnection, [], 'linux');
expect(cmd).toContain('--update');
- expect(cmd).toContain('sudo bash -c');
+ expect(cmd).toContain('set -- sudo');
+ expect(cmd).toContain('\"$@\" bash -c');
expect(cmd).not.toContain('--token-file');
expect(cmd).not.toContain('token_dir=');
dispose();
diff --git a/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts b/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts
index 5f203970eb..88bf145b14 100644
--- a/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts
+++ b/frontend-modern/src/features/docs/__tests__/troubleshootingEntryGuide.test.ts
@@ -44,7 +44,7 @@ describe('troubleshooting workflow entry', () => {
const section = entry();
const link = section.querySelector('a');
expect(link?.getAttribute('href')).toBe('/docs/VM_DISK_MONITORING#backup-safety');
- expect(section.querySelectorAll('li')).toHaveLength(9);
+ expect(section.querySelectorAll('li')).toHaveLength(10);
expect(section.querySelector('pre')).toBeNull();
const beforeList = [...section.children]
.slice(
@@ -62,30 +62,32 @@ describe('troubleshooting workflow entry', () => {
it('keeps server updates, workload updates and reporting acceptance distinct', () => {
const section = entry();
- const rows = section.querySelectorAll('li');
- expect(prose(rows[1] as HTMLElement)).toContain(
- 'An update of a monitored container is a different job',
- );
- const updateLinks = [...rows[1].querySelectorAll('a')].map((link) => link.getAttribute('href'));
+ const row = (label: string) => {
+ const rows = [...section.querySelectorAll('li')].filter((element) =>
+ prose(element.querySelector('strong') as HTMLElement).startsWith(label),
+ );
+ expect(rows, label).toHaveLength(1);
+ return rows[0] as HTMLElement;
+ };
+ const update = row('Update failed or its result is uncertain');
+ const install = row('Agent host is missing or reporting is rejected');
+ const reporting = row('Guest readings are missing or disagree');
+ const memory = row('Pulse itself uses more memory, CPU or disk writes');
+ expect(prose(update)).toContain('An update of a monitored container is a different job');
+ const updateLinks = [...update.querySelectorAll('a')].map((link) => link.getAttribute('href'));
expect(updateLinks).toEqual([
'/docs/AUTO_UPDATE#update-failed',
'/docs/DOCKER#check-a-failed-or-pending-workload-update',
]);
- expect(prose(rows[1] as HTMLElement)).toContain(
- 'Preserve the current state and rollback evidence',
- );
- expect(prose(rows[2] as HTMLElement)).toContain('Installer success is not an accepted report');
- expect(prose(rows[3] as HTMLElement)).toContain(
- 'a working Pulse Agent view does not prove API recovery',
- );
- expect(prose(rows[6] as HTMLElement)).toContain(
- 'Guest usage and database size are different measurements',
- );
+ expect(prose(update)).toContain('Preserve the current state and rollback evidence');
+ expect(prose(install)).toContain('Installer success is not an accepted report');
+ expect(prose(reporting)).toContain('a working Pulse Agent view does not prove API recovery');
+ expect(prose(memory)).toContain('Guest usage and database size are different measurements');
});
it('routes every entry to an existing shipped heading in the production viewer', () => {
const links = [...entry().querySelectorAll('a[href]')];
- expect(links).toHaveLength(16);
+ expect(links).toHaveLength(17);
for (const link of links) {
const href = link.getAttribute('href')!;
const match = /^\/docs\/([^#]+)#(.+)$/.exec(href);
diff --git a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
index b56e8c0f33..c0c7be1b52 100644
--- a/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
+++ b/frontend-modern/src/utils/__tests__/agentInstallCommand.test.ts
@@ -31,7 +31,8 @@ describe('agentInstallCommand', () => {
expect(command).toContain("--url 'http://pulse.example:7655'");
expect(command).toContain('token_dir=$(mktemp -d /tmp/pulse-agent-bootstrap.XXXXXX)');
- expect(command).toContain('sudo bash -c');
+ expect(command).toContain('set -- sudo');
+ expect(command).toContain('\"$@\" bash -c');
expect(command).not.toContain('token-123');
expect(command).toContain('--token-file "$token_file"');
expect(command).toContain('--insecure');
@@ -60,7 +61,7 @@ describe('agentInstallCommand', () => {
});
const preflightIndex = command.indexOf('--preflight-only');
- const sudoIndex = command.indexOf('sudo bash -c');
+ const sudoIndex = command.indexOf('\"$@\" bash -c');
expect(command).toContain('bootstrap_dir=$(mktemp -d /tmp/pulse-bootstrap.XXXXXX)');
expect(command).toContain('trap cleanup EXIT');
@@ -336,6 +337,21 @@ const variants = [
];
describe('single-line Unix install commands', () => {
+ it.each(variants)('keeps one private body below the usual terminal line limit: %j', (options) => {
+ const command = buildUnixAgentInstallCommand({
+ ...options,
+ token: 'present',
+ extraArgs: [
+ '--update',
+ '--enable-docker',
+ "--agent-id 'agent-42'",
+ "--hostname 'node.example'",
+ ],
+ });
+ expect(Buffer.byteLength(command, 'utf8')).toBeLessThan(4096);
+ expect(command.match(/bash -c /g)).toHaveLength(1);
+ expect(command).toContain('set -- sudo');
+ });
it.each(variants)('survives text-input normalization: %j', (options) => {
const command = buildUnixAgentInstallCommand(options);
const input = document.createElement('input');
@@ -391,7 +407,8 @@ if history:
env.update(HISTFILE=history, HISTSIZE="100", HISTFILESIZE="100", HISTCONTROL="", PS1="PULSE_TEST$ ")
program = ["bash", "--noprofile", "--norc", "-i"] if history else ["sh", "-c", p["command"]]
child = subprocess.Popen(program, stdin=slave, stdout=slave, stderr=slave, preexec_fn=session, env=env)
-if history: os.write(master, (p["command"] + "\\n").encode())
+command_sent = not bool(history)
+exit_sent = False
os.close(slave)
output = b""
sent = False
@@ -406,6 +423,17 @@ while time.monotonic() < deadline:
raise
if not chunk: break
output += chunk
+ # A human pastes after the caller's prompt. Injecting a longer command
+ # while the PTY is still in its startup canonical mode can truncate it
+ # before Bash has even entered readline.
+ if history and not command_sent and output.endswith(b"PULSE_TEST$ "):
+ os.write(master, (p["command"] + "\\n").encode())
+ command_sent = True
+ continue
+ # Do not queue exit while the private reader/installer owns the tty.
+ if history and sent and not exit_sent and output.endswith(b"PULSE_TEST$ "):
+ os.write(master, b"exit\\n")
+ exit_sent = True
# The interactive shell's echo of the command line quotes the prompt
# text too, so a chunk can end there long before the bootstrap runs.
# Only the bootstrap's own stty -echo (recorded by the stty fixture)
@@ -424,7 +452,6 @@ while time.monotonic() < deadline:
else:
os.write(master, (p["input"] + "\\n").encode())
sent = True
- if history: os.write(master, b"exit\\n")
if child.poll() is not None and not ready: break
else:
os.killpg(child.pid, signal.SIGKILL)
@@ -958,7 +985,7 @@ describe('terminal-control portability', { timeout: 25_000 }, () => {
const result = runPrivateUnix(command(), 'private-input', fixture.env('0', failure));
expect(result.status, result.stdout + result.stderr).not.toBe(0);
expect(result.stdout).toContain(error);
- expect(result.stdout).toContain('No installation was attempted');
+ expect(result.stdout).toMatch(/no installation was attempted/i);
expect(result.stdout).not.toMatch(
/A terminal is required|command not found|deadline expired|did not restore|private-input/,
);
diff --git a/frontend-modern/src/utils/agentInstallCommand.ts b/frontend-modern/src/utils/agentInstallCommand.ts
index 130a54db37..c2f9f2bd42 100644
--- a/frontend-modern/src/utils/agentInstallCommand.ts
+++ b/frontend-modern/src/utils/agentInstallCommand.ts
@@ -239,7 +239,11 @@ export const buildUnixAgentInstallCommand = ({
`curl ${curlFlags}${caCertArg} ${shellQuoteArg(`${normalizedBaseUrl}/install.sh`)} -o "$install_script";`,
// Detachment must not require a downloadable new agent binary.
...(!uninstall ? [`bash "$install_script" ${preflightArgs}${caCertArg}${insecureArg};`] : []),
- `if [ "$(id -u)" -eq 0 ]; then ${child} else sudo ${child} fi;`,
+ // Expand the root/sudo prefix once. Repeating the entire quoted reader
+ // makes a normal pasted command exceed some terminals' canonical line
+ // limit. Positional arguments live only inside this isolated subshell.
+ 'if [ "$(id -u)" -eq 0 ]; then set --; else set -- sudo; fi;',
+ `"$@" ${child}`,
')',
].join(' ');
};
From 41813ba2a12744ca3815e2bc6c29f552441e7b22 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:41:40 +0100
Subject: [PATCH 5/5] Record desktop and phone proof for portable private input
Actual Settings and Agent Doctor copy consumers retain separate credentials, private-file fallback, identity and trust. All sixteen final captures were opened with the native image tool; no native or release acceptance is claimed.
Contract-Neutral: Source-bound consuming browser evidence only; production bytes and contracts are unchanged.
Change-source: pulse-maintainer
---
.../75c45c0a6c8bb147.json | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 frontend-modern/browser-verification/75c45c0a6c8bb147.json
diff --git a/frontend-modern/browser-verification/75c45c0a6c8bb147.json b/frontend-modern/browser-verification/75c45c0a6c8bb147.json
new file mode 100644
index 0000000000..ec0eebd7f4
--- /dev/null
+++ b/frontend-modern/browser-verification/75c45c0a6c8bb147.json
@@ -0,0 +1,41 @@
+{
+ "version": 1,
+ "base_sha": "14b9b3fbaca62696741aa19845eedbde1cbbb28c",
+ "verified_at": "2026-10-13T15:41:16.054533Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/utils/agentInstallCommand.ts",
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/utils/agentInstallCommand.ts": "b6bdfc4a8a8b9253d1dfedb651ed07dc7945542382a672baf2c89b8d2482b18b",
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "f2a75e62b318b48f528262c77b0fd3f778d95c535575da6d1873869632e126ff"
+ },
+ "routes": [
+ "/browser-tests/unix-private-install.html (offline actual Settings installer/state/TokenRevealDialog/Agent Doctor consuming fixture)"
+ ],
+ "viewports": [
+ {
+ "width": 1280,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 900
+ }
+ ],
+ "states": [
+ "Chromium141 desktop and WebKit26 phone, both light and dark; Playwright1.56.1 matches integration lock. 2811 source inputs and three served artifact hashes match14b9b3fb.",
+ "Synthetic token reveal stays separate from Unix commands; Linux/NAS and FreeBSD private-file alternatives are credential-free and preserve trusted-file instructions. Actual clipboard values match displayed commands.",
+ "Doctor authentication repair uses the new selected-tool reader and existing identity/profile; ordinary saved-state update and uninstall still need no new token/prompt. Custom CA and URL survive the real state/copy consumers.",
+ "Optional authentication confirms tokenless commands without any private-file or prompt block. Token reveal does not mint a second credential.",
+ "All16 final captures opened natively; readable fields/advice and keyboard outlines, no page horizontal overflow; browser/server closed."
+ ],
+ "interactions": [
+ "Generate synthetic host token; copy separate token; dismiss with Escape and reopen existing reveal without reminting.",
+ "Keyboard Enter/phone tap on actual Linux installer/private-file copy and Doctor row expansion; actual Doctor repair copy.",
+ "Advanced URL/custom CA edits into actual lifecycle commands; preserve identity/flags for credential repair and saved-state update/removal semantics.",
+ "Confirm optional tokenless installation after synthetic auth transition."
+ ],
+ "limits": "Modelled API/agent/privilege/installer data, no native QNAP or backend-mirror, deployment/publication/release/reporter acceptance."
+}