From b21f086e85ec422338c567fea68f4455f796dd97 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2026 14:07:33 +0100
Subject: [PATCH 1/3] Keep Unix lifecycle credentials out of copied commands
Replace literal credential bootstraps with the exercised bounded Core private-entry pattern, preserve a checked local private-file route for non-terminal FreeBSD command fields, and consolidate Unix repair, upgrade and removal transport without changing token issuance, TLS choice or host identity. Removal does not depend on a new binary preflight. Windows credential transport remains the next rework step. Add executable PTY/file/lifecycle proofs and the missing DOMPurify after-attributes detached-subtree regression, with parent-bound browser evidence.
Change-source: pulse-maintainer
---
.../v6/internal/subsystems/agent-lifecycle.md | 43 +-
.../v6/internal/subsystems/api-contracts.md | 42 +-
.../browser-tests/unix-private-install.cjs | 257 ++++++++
.../browser-tests/unix-private-install.html | 12 +
.../browser-tests/unix-private-install.tsx | 103 ++++
frontend-modern/browser-verification.json | 63 +-
.../AI/__tests__/aiChatUtils.test.ts | 10 +-
.../InfrastructureAgentDoctorPage.tsx | 16 +-
.../InfrastructureInstallerSection.tsx | 21 +-
.../InfrastructureOperationsModel.test.tsx | 16 +-
.../infrastructureAgentDoctorModel.test.ts | 77 ++-
...reOperationsState.branchcov0722pm.test.tsx | 47 +-
.../infrastructureOperationsModel.tsx | 37 +-
.../useInfrastructureInstallState.tsx | 11 +-
.../useInfrastructureOperationsState.tsx | 113 ++--
.../__tests__/agentInstallCommand.test.ts | 558 ++++++++++++------
.../src/utils/agentInstallCommand.ts | 99 ++--
17 files changed, 1119 insertions(+), 406 deletions(-)
create mode 100644 frontend-modern/browser-tests/unix-private-install.cjs
create mode 100644 frontend-modern/browser-tests/unix-private-install.html
create mode 100644 frontend-modern/browser-tests/unix-private-install.tsx
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index 4c1a7267b1..3d70a8035d 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -8344,20 +8344,37 @@ Focused proof lives in `internal/hostagent/agent_new_test.go`
`TestGetReliableMachineIDNormalizesWindowsBraces`); the Windows recovery file is
cross-compiled with `GOOS=windows go build ./internal/hostagent/`.
-### Unix bootstrap survives single-line command fields
+### Private Unix lifecycle bootstrap
-The shared frontend Unix installer command uses explicit shell statement
-separators rather than literal newlines or backslash-newline continuations.
-This preserves its grammar when copied into a single-line command field (#2123).
-The builder rejects embedded CR/LF in its normalized inputs rather than silently
-altering a quoted URL, credential, CA path or option. Shell quoting is unchanged.
-Download and unprivileged preflight still precede token-file creation and sudo
-installation. Root and sudo paths retain private token directories, file modes,
-TLS options, non-interactive operation and cleanup on success or failure.
-`agentInstallCommand.test.ts` covers normalized-input syntax and
-synthetic root/sudo execution under sh and Bash, including download, preflight
-and install failures. A clipboard fixture proves input compatibility, not native
-FreeBSD installation, offline dependency availability or service persistence.
+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;
+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.
+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
+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
+console/SSH session; native pfSense/FreeBSD installation is not inferred.
+
+Unix upgrades, explicit credential repairs and uninstall now share this
+complete-download transport rather than executing a partial fetch pipe or
+putting tokens in argv. Repairs/removal retain canonical ID and hostname;
+ordinary updates preserve their saved-state credential/identity route. Removal
+skips new-binary preflight, which is not a detachment prerequisite. Issuance,
+scopes, tenant/host binding, TLS and command-execution opt-in are unchanged.
+Windows credential handling is deliberately unchanged pending its own complete
+validated step. `agentInstallCommand.test.ts` is the executable helper proof;
+`infrastructureAgentDoctorModel.test.ts` verifies the real operations closures
+and existing credential/identity choices. Browser receipts cover real settings
+components with synthetic APIs, not installed outcome or reporter acceptance.
### Agent Doctor handoff stays readable on a phone
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 1fcbf79804..7c10b4f07d 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -11015,19 +11015,37 @@ status-only or unrelated test for the setup contract. The registry routes only `
setup proof policy; unrelated API runtime paths retain their existing verification
requirements. No runtime paths or required contract updates are exempted.
-### Single-line Unix client install transport
+### Single-line private Unix client install transport
-`frontend-modern/src/utils/agentInstallCommand.ts` emits a single-line Unix
-bootstrap with explicit shell separators so text-input paste normalization
-cannot join shell statements (#2123). It retains the canonical URL, quoted
-credential-to-private-file transport, custom CA/insecure continuity, and
-preflight-before-install ordering; no API request, response or auth scope changes.
-Normalized values containing CR/LF are rejected, not rewritten. Regression
-coverage in `agentInstallCommand.test.ts` checks shell syntax after
-text-input normalization, literal quoted token bytes, root/sudo private-file
-permissions, cleanup and early exit when download or preflight fails. This
-client transport repair does not establish the reporter's native pfSense abort
-cause or successful offline installation.
+`frontend-modern/src/utils/agentInstallCommand.ts` retains explicit single-line
+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
+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
+validation precedes the download so authentication cannot consume token input.
+
+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 does not put credentials in GUI command fields. It does not claim a GUI can
+create private files or establish native appliance installation.
+
+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
+state without a new prompt. Uninstall skips new-binary preflight so detachment
+is not blocked by availability of an unrelated replacement binary. Server
+minting, scope, TLS choice, execution opt-in and response shapes are unchanged.
+Windows command transport is unchanged and remains separate unfinished work.
+`agentInstallCommand.test.ts` exercises actual shells, PTYs, private-file
+boundaries and failures; `infrastructureAgentDoctorModel.test.ts` exercises the
+real lifecycle closures. These are modelled installer/privilege probes, not
+native system installation or publication.
### Pulse Mobile pairing readiness wording
diff --git a/frontend-modern/browser-tests/unix-private-install.cjs b/frontend-modern/browser-tests/unix-private-install.cjs
new file mode 100644
index 0000000000..96e520158d
--- /dev/null
+++ b/frontend-modern/browser-tests/unix-private-install.cjs
@@ -0,0 +1,257 @@
+// UI proof only. No real token issuance, appliance, download or installation.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { chromium, webkit } = require('playwright');
+(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 results = [];
+ let browser;
+ const secret = 'browser-synthetic-install-token';
+ try {
+ await server.listen();
+ for (const [engine, width, tone] of [
+ ['chromium', 1280, 'light'],
+ ['webkit', 390, 'dark'],
+ ]) {
+ browser = await { chromium, webkit }[engine].launch(
+ engine === 'chromium'
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({
+ viewport: { width, height: 900 },
+ ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
+ });
+ const errors = [];
+ page.on('pageerror', (e) => errors.push(e.message));
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ console.log('PAGE_CONSOLE_ERROR', m.text());
+ errors.push(m.text());
+ }
+ });
+ page.on('requestfailed', (r) => console.log('REQUEST_FAILED', r.url(), r.failure()));
+ let minted = 0;
+ let optionalAuth = false;
+ await page.addInitScript(() => {
+ window.__copies = [];
+ Object.defineProperty(navigator, 'clipboard', {
+ value: { writeText: async (text) => window.__copies.push(text) },
+ });
+ });
+ await page.route('**/api/**', async (route) => {
+ const url = new URL(route.request().url());
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ if (url.pathname === '/api/security/status')
+ return route.fulfill({
+ json: {
+ requiresAuth: !optionalAuth,
+ hasAuthentication: !optionalAuth,
+ apiTokenConfigured: !optionalAuth,
+ },
+ });
+ if (url.pathname === '/api/agent-install-command') {
+ const body = route.request().postDataJSON();
+ assert.equal(body.type, 'host');
+ assert.equal(body.enableCommands, false);
+ minted++;
+ return route.fulfill({
+ json: {
+ token: secret,
+ record: {
+ id: 'synthetic-token-record',
+ name: 'Synthetic installer',
+ prefix: 'browser',
+ suffix: 'token',
+ createdAt: new Date().toISOString(),
+ },
+ },
+ });
+ }
+ if (url.pathname === '/api/state')
+ 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.waitForTimeout(1000);
+ console.log('PAGE_TITLE', await page.title());
+ console.log(
+ JSON.stringify({
+ engine,
+ errors,
+ body: (await page.locator('body').innerText()).slice(0, 300),
+ }),
+ );
+ await page
+ .getByRole('heading', { name: 'Private Unix agent installation', exact: true })
+ .waitFor();
+ if (tone === 'dark')
+ await page.evaluate(() => document.documentElement.classList.add('dark'));
+ await page.getByRole('button', { name: 'Generate token', exact: true }).first().click();
+ const dialog = page.getByRole('dialog', { name: 'API token ready' });
+ await dialog.waitFor();
+ await page.waitForFunction(() => {
+ const d = document.querySelector('[role="dialog"]');
+ return d && getComputedStyle(d).opacity === '1';
+ });
+ assert.match(await dialog.innerText(), /silent “Pulse agent token” prompt/);
+ const copyToken = dialog.getByRole('button', { name: 'Copy token', exact: true });
+ 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 page.keyboard.press('Escape');
+ await dialog.waitFor({ state: 'hidden' });
+ // The real component's platform cards and copy handlers.
+ const code = page.locator('code').filter({ hasText: 'Pulse agent token' });
+ assert.ok((await code.count()) >= 3);
+ const unixCommands = await code.allTextContents();
+ for (const command of unixCommands) {
+ assert.ok(!command.includes(secret));
+ assert.ok(command.includes('--token-file "$token_file"'));
+ assert.ok(!/[\r\n]/.test(command));
+ }
+ const guiCode = page.locator('code').filter({ hasText: 'token_parent=${token_file%/*}' });
+ assert.equal(await guiCode.count(), 1);
+ const guiCommand = await guiCode.innerText();
+ assert.ok(guiCommand.includes('/root/.config/pulse-agent/bootstrap-token'));
+ assert.ok(!guiCommand.includes('read -r'));
+ assert.ok(!guiCommand.includes(secret));
+ assert.ok(
+ (await page.locator('body').innerText()).includes(
+ 'Never put the token in a GUI command field',
+ ),
+ );
+ await page.getByRole('button', { name: 'Copy Install command', exact: true }).click();
+ assert.equal(await page.evaluate(() => window.__copies.at(-1)), unixCommands[0]);
+ await page
+ .getByRole('button', {
+ name: 'Copy Install from a private token file (no terminal) command',
+ exact: true,
+ })
+ .click();
+ assert.equal(await page.evaluate(() => window.__copies.at(-1)), guiCommand);
+ 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));
+
+ await page.getByRole('button', { name: 'Show token only', exact: true }).click();
+ await dialog.waitFor();
+ assert.equal(minted, 1);
+ await dialog.getByRole('button', { name: 'Dismiss', exact: true }).click();
+ await page
+ .getByRole('button', { name: 'Show advanced connection and install options', exact: true })
+ .click();
+ await page
+ .getByLabel('Connection URL (Agent → Pulse)', { exact: true })
+ .fill('https://pulse.example/base/');
+ await page
+ .getByLabel('Custom CA certificate path (optional)', { exact: true })
+ .fill('/etc/pulse/ca.pem');
+ for (const label of [
+ 'Copy Unix uninstall',
+ 'Copy Unix credential repair',
+ 'Copy Unix saved-state update',
+ ]) {
+ await page.getByRole('button', { name: label, exact: true }).click();
+ const command = await page.evaluate(() => window.__copies.at(-1));
+ assert.ok(!command.includes(secret));
+ assert.ok(command.includes('https://pulse.example/base'));
+ assert.ok(command.includes('--cacert'));
+ assert.ok(!command.includes('| bash'));
+ if (label.includes('uninstall')) {
+ assert.ok(command.includes('--uninstall'));
+ assert.ok(command.includes('agent-fixture-42'));
+ assert.ok(command.includes('node.example'));
+ assert.ok(!command.includes('--preflight-only'));
+ } else {
+ assert.ok(command.includes('--update'));
+ assert.ok(command.includes('--enable-docker'));
+ assert.ok(command.includes('--preflight-only'));
+ if (label.includes('credential')) {
+ assert.ok(command.includes('agent-fixture-42'));
+ assert.ok(command.includes('node.example'));
+ assert.ok(command.includes('--token-file'));
+ } else {
+ assert.ok(!command.includes('--token-file'));
+ assert.ok(!command.includes('read -r'));
+ }
+ }
+ }
+ assert.ok(
+ (await page.locator('body').innerText()).includes(
+ 'Never insert the token into the command',
+ ),
+ );
+ await page.screenshot({
+ path: path.join(artifacts, `${engine}-commands.png`),
+ fullPage: true,
+ });
+ optionalAuth = true;
+ await page.goto('http://127.0.0.1:5241/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();
+ assert.ok(optionalCommands.length >= 3);
+ for (const command of optionalCommands) {
+ assert.ok(!command.includes('--token-file'));
+ assert.ok(!command.includes('read -r'));
+ }
+ assert.equal(
+ await page
+ .getByRole('button', {
+ name: 'Copy Install from a private token file (no terminal) command',
+ exact: true,
+ })
+ .count(),
+ 0,
+ );
+ assert.deepEqual(errors, []);
+ results.push({
+ engine,
+ version: browser.version(),
+ width,
+ tone,
+ issued: minted,
+ copied: await page.evaluate(() => window.__copies.length),
+ unixCommands: unixCommands.length,
+ guiCommand: true,
+ optionalAuth: true,
+ errors,
+ });
+ 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 }));
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((e) => {
+ console.error(e);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/unix-private-install.html b/frontend-modern/browser-tests/unix-private-install.html
new file mode 100644
index 0000000000..862d8a5e58
--- /dev/null
+++ b/frontend-modern/browser-tests/unix-private-install.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Unix private agent installation
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/unix-private-install.tsx b/frontend-modern/browser-tests/unix-private-install.tsx
new file mode 100644
index 0000000000..dd4ef40c6c
--- /dev/null
+++ b/frontend-modern/browser-tests/unix-private-install.tsx
@@ -0,0 +1,103 @@
+// Production settings state, installer and token dialog; synthetic API data only.
+import { Route, Router } from '@solidjs/router';
+import { render } from 'solid-js/web';
+import {
+ InfrastructureOperationsStateProvider,
+ useInfrastructureOperationsContext,
+} from '../src/components/Settings/useInfrastructureOperationsState';
+import { InfrastructureInstallerSection } from '../src/components/Settings/InfrastructureInstallerSection';
+import { InfrastructureAgentDoctorPage } from '../src/components/Settings/InfrastructureAgentDoctorPage';
+import type { InfrastructureAgentDoctorTarget } from '../src/components/Settings/infrastructureAgentUpdateCommandsModel';
+import { TokenRevealDialog } from '../src/components/TokenRevealDialog';
+import '../src/index.css';
+
+const Operations = () => {
+ const state = useInfrastructureOperationsContext();
+ const identity = {
+ agentActionId: 'agent-fixture-42',
+ agentId: 'unused-alias',
+ hostname: 'node.example',
+ };
+ const connection = {
+ id: 'agent:agent-fixture-42',
+ type: 'agent',
+ name: 'node.example',
+ address: 'node.example',
+ state: 'unauthorized',
+ agentIdentity: { hostname: 'node.example', commandsEnabled: false },
+ } as Parameters[0];
+ return (
+
+
Actual operations state: identity and trust options must survive.
+
+
+
+
+ );
+};
+const Fixture = () => (
+
+
+
Private Unix agent installation
+
+
+
+
+
+
+);
+const Doctor = () => {
+ const target = {
+ key: 'agent:doctor-fixture',
+ connectionId: 'agent:doctor-fixture',
+ displayName: 'doctor-fixture',
+ contextLabel: 'Machine',
+ currentVersion: '6.4.5',
+ installFlags: ['--enable-docker'],
+ status: 'critical',
+ reasons: [],
+ evidence: [],
+ needsUpdate: false,
+ needsCredentialRepair: true,
+ commandPlatform: 'linux',
+ safeCollector: false,
+ actionRunnerPosture: [],
+ actionRunnerCredentialEligible: false,
+ source: 'ledger',
+ connection: {
+ id: 'agent:doctor-fixture',
+ type: 'agent',
+ name: 'doctor-fixture',
+ address: 'doctor.example',
+ state: 'unauthorized',
+ agentIdentity: { hostname: 'doctor.example', commandsEnabled: false },
+ },
+ } as InfrastructureAgentDoctorTarget;
+ return ;
+};
+render(
+ () => (
+
+
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index 7e82fe7313..949d210a07 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,23 +1,27 @@
{
"version": 1,
- "base_sha": "6b23876ec8104965c7842e194e055f3206134d4f",
- "verified_at": "2026-10-01T04:21:56.727078Z",
+ "base_sha": "9189a7262e84b201a5af6649caf0f8cff7c6d144",
+ "verified_at": "2026-10-01T13:06:33.056850Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/api/nodes.ts",
- "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx",
- "frontend-modern/src/components/Settings/useNodeModalState.ts",
- "frontend-modern/src/stores/tokenReveal.ts"
+ "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
+ "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx",
+ "frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx",
+ "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx",
+ "frontend-modern/src/utils/agentInstallCommand.ts"
],
"content_sha256": {
- "frontend-modern/src/api/nodes.ts": "398f3b87ce192c7650a9e34f184ecdca0e8baf35b57d90ef5f8f697348675fec",
- "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "e4168945744f8ec20b5439f5730b1ecd8b4811b29ec00da4e7e3798f868a1b41",
- "frontend-modern/src/components/Settings/useNodeModalState.ts": "9372c3a4b346a0e31733a7042c0a65b5ebe3518b12595c6c6a68b1cadd667dcd",
- "frontend-modern/src/stores/tokenReveal.ts": "c21ac970bde6229506f7d6e627430b6e91c9ea983f705a28e3d149e4415c39b7"
+ "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "416c7627303c665f956168a32e5aaf6af4268abacdd7e93045ab26cd45732d14",
+ "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "d2ff336c1e602794fd696aed3541684498adfcfeaf1f4e603e21a44865fce3bb",
+ "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx": "983a44871cfb908851c215a177e4a39185f7ac9017cc1b2152323b7ee396995a",
+ "frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx": "c57bf5a99d457d6d21f0d22e0cc34af3618db214391fd09afce34743173a1c0f",
+ "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx": "276c89076d36670752f5d37be1895d739ae13501ea9d87d1cc3a497c7e6f5895",
+ "frontend-modern/src/utils/agentInstallCommand.ts": "9bb98b1db1b49aa478f894b346479951954899d4c6049d991ec4c75730cafa30"
},
"routes": [
- "/browser-tests/proxmox-private-input.html?type=pve",
- "/browser-tests/proxmox-private-input.html?type=pbs"
+ "/browser-tests/unix-private-install.html",
+ "/browser-tests/unix-private-install.html?optional=1"
],
"viewports": [
{
@@ -30,34 +34,29 @@
}
],
"states": [
- "Actual settings state/setup section and existing token-reveal dialog with production CSS; synthetic server artifacts and no installed bootstrap execution",
- "Separate command and credential copy for API Inventory and Host Telemetry Agent; raw secrets absent from copied shell text",
- "PVE/PBS tokenless script download reuses live bootstrap metadata; credential cache clears on close/reopen",
- "Held setup and agent issuance completed after modal close is discarded: no secret cache and no reveal dialog",
- "Default TLS verification and monitoring-only agent request (enableCommands=false) unchanged",
- "Desktop light and phone-emulated WebKit dark token reveal is readable; screenshots visually inspected"
+ "Actual infrastructure installer, operations closures, Agent Doctor and token dialog with production CSS; synthetic APIs, not installed agent/appliance execution",
+ "Required-auth Unix command and credential remain separate; single-line command grammar, private file no-terminal variant and separate-token instructions",
+ "Canonical repair/removal identity and custom CA preserved; saved-state update has no replacement credential; removal has no new-binary preflight",
+ "Optional-auth commands require no terminal or token-file; default TLS verification and monitoring-only issuance unchanged",
+ "Desktop Chromium light and phone-emulated WebKit dark; four final screenshots visually inspected"
],
"interactions": [
- "Copy command; copy token via Enter and pointer; Escape and Dismiss close token dialog",
- "Close/reopen setup; download script; switch API/Agent paths",
- "Hold setup/agent response, close modal, then release response and verify no late reveal/cache"
+ "Generate scoped host token, copy token with keyboard, Escape, reopen existing reveal without issuing another token",
+ "Copy actual Linux installer, no-terminal private-file installer and Agent Doctor repair commands; raw credential absent from each clipboard result",
+ "Change endpoint and custom CA; copy actual operations-state repair/removal/saved-state update commands; identity/profile/trust checked",
+ "Reload with optional auth and confirm tokenless commands"
],
- "command": "pulse-worker-browser frontend-modern/browser-tests/proxmox-private-input.cjs; pulse-worker-browser frontend-modern/browser-tests/proxmox-private-input.cjs --phone (separate commands from assigned workspace root)",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/unix-private-install.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-desktop/pbs-agent.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-desktop/pbs-setup.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-desktop/pve-agent.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-desktop/pve-setup.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-desktop/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-phone/pbs-agent.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-phone/pbs-setup.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-phone/pve-agent.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-phone/pve-setup.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-yp94gsia/browser-phone/result.json"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/chromium-commands.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/chromium-token.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/webkit-commands.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-wielhxw1/browser/webkit-token.png"
]
}
diff --git a/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts b/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts
index d11a3c0c53..bb17caecc2 100644
--- a/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts
+++ b/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts
@@ -214,7 +214,7 @@ describe('aiChatUtils', () => {
expect(link?.getAttribute('rel')).toBe('noopener noreferrer');
});
- it.each(['beforeSanitizeElements', 'uponSanitizeElement'] as const)(
+ it.each(['beforeSanitizeElements', 'uponSanitizeElement', 'afterSanitizeAttributes'] as const)(
'neutralizes a hook-detached IN_PLACE subtree through the %s hook',
(hookName) => {
// Configure the same singleton hooks used by renderMarkdown before
@@ -232,8 +232,10 @@ describe('aiChatUtils', () => {
};
if (hookName === 'beforeSanitizeElements') {
DOMPurify.addHook('beforeSanitizeElements', removeSection);
- } else {
+ } else if (hookName === 'uponSanitizeElement') {
DOMPurify.addHook('uponSanitizeElement', removeSection);
+ } else {
+ DOMPurify.addHook('afterSanitizeAttributes', removeSection);
}
try {
@@ -253,8 +255,10 @@ describe('aiChatUtils', () => {
} finally {
if (hookName === 'beforeSanitizeElements') {
DOMPurify.removeHook('beforeSanitizeElements', removeSection);
- } else {
+ } else if (hookName === 'uponSanitizeElement') {
DOMPurify.removeHook('uponSanitizeElement', removeSection);
+ } else {
+ DOMPurify.removeHook('afterSanitizeAttributes', removeSection);
}
}
},
diff --git a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
index 8abce47160..dd6ada8420 100644
--- a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
+++ b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx
@@ -917,6 +917,18 @@ export const InfrastructureAgentDoctorPage: Component{command()}
+
+
+ Run this command in a console or SSH session first, then
+ paste the separately revealed token at the silent Pulse
+ agent token prompt. Never insert the token into the command.
+
+
@@ -929,7 +941,9 @@ export const InfrastructureAgentDoctorPage: Component
{(entry) => (
diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
index c7142a16e2..d5135b3d50 100644
--- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
+++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
@@ -396,12 +396,13 @@ export const InfrastructureInstallerSection: Component
First-host install token {state.latestRecord()?.name}{' '}
- prepared automatically. Commands below already include this credential.
+ prepared automatically. Unix commands request it separately at a silent
+ prompt.
>
) : (
<>
- Install token {state.latestRecord()?.name} created. Commands
- below now include this credential.
+ Install token {state.latestRecord()?.name} created. Unix
+ commands keep it separate: copy the command first, then use Show token only.
>
)}
@@ -421,8 +422,8 @@ export const InfrastructureInstallerSection: Component
- Tokens are optional on this Pulse instance. Confirm to generate commands without
- embedding a token.
+ Tokens are optional on this Pulse instance. Confirm to generate commands without a
+ token.
Copy the default command for the first host first. It checks this Pulse URL and
- the matching agent binary before asking for administrator privileges, then
- installs Pulse Agent as a background service on each machine where you want full
- node-local telemetry.
+ the matching agent binary before reading a Unix token, then installs Pulse Agent
+ as a background service on each machine where you want full node-local
+ telemetry.