Compose reviewed portable private input with current maintenance

Preserve the exact reviewed private-input candidate and its source-bound receipts. Qualify changed drawer composition separately; no native or release acceptance is claimed.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-13 17:47:33 +01:00
11 changed files with 599 additions and 95 deletions
@@ -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,22 @@ 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
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
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
@@ -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,23 @@ 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.
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,
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
@@ -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();
}
@@ -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,46 @@ 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('..')
.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 +289,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 +329,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;
@@ -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."
}
@@ -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',
@@ -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();
@@ -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: (
<span>
Use this when there is no terminal or no usable <code>stty</code> or BusyBox{' '}
<code>stty</code>. Before running, save the separately revealed token to{' '}
<code>/root/.config/pulse-agent/bootstrap-token</code> using a trusted file editor or
upload path. The file must be root-owned with mode <code>0600</code> in a root-owned{' '}
<code>0700</code> 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.
</span>
),
},
]
: [];
export const buildCommandsByPlatform = (
unixCommand: string,
windowsInteractiveCommand: string,
@@ -348,10 +372,13 @@ export const buildCommandsByPlatform = (
Command auto-escalates with <code>sudo</code> when available. Otherwise run from a root
shell (for example <code>su -</code>). 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 <code>stty</code> or BusyBox <code>stty</code>. Otherwise use the
private-file command below.
</span>
),
},
...privateFileInstallSnippets(unixPrivateFileCommand),
],
},
macos: {
@@ -392,25 +419,7 @@ export const buildCommandsByPlatform = (
</span>
),
},
...(unixPrivateFileCommand
? [
{
label: 'Install from a private token file (no terminal)',
command: unixPrivateFileCommand,
note: (
<span>
Before running, save the separately revealed token to{' '}
<code>/root/.config/pulse-agent/bootstrap-token</code> using a trusted file editor
or upload path. The file must be root-owned with mode <code>0600</code> in a
root-owned <code>0700</code> 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.
</span>
),
},
]
: []),
...privateFileInstallSnippets(unixPrivateFileCommand),
],
},
windows: {
@@ -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);
@@ -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,15 +452,13 @@ 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)
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:
@@ -444,7 +470,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 +481,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 +561,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 +571,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 +908,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).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/,
);
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)',
@@ -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/tty 2>/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/tty 2>/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 </dev/tty); then echo "A terminal is required to enter the token. No installation was attempted." >&2; exit 1; fi;',
'stty -echo </dev/tty;',
// Some NAS hosts expose stty only as a BusyBox applet. Keep the selected
// utility for both mode changes and cleanup; never fall back to echoed
// input. Missing TTY, missing tool and failed mode changes are distinct.
'if ! { : </dev/tty; } 2>/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/tty 2>/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/tty 2>/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 </dev/tty; then echo "Token input was interrupted. No installation was attempted." >&2; exit 1; fi;`,
'if [ "${#pulse_token}" -gt 4096 ]; then while IFS= read -r -s -n 4097 pulse_discard </dev/tty && [ "${#pulse_discard}" -eq 4097 ]; do :; done; unset pulse_discard; fi;',
'stty "$tty_state" </dev/tty; unset tty_state;',
'if ! "${tty_control[@]}" "$tty_state" </dev/tty 2>/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;',
@@ -233,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(' ');
};