Integrate reviewed confined PVE helper guidance

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-14 07:26:18 +01:00
9 changed files with 408 additions and 55 deletions
+41 -22
View File
@@ -101,7 +101,7 @@ manual probes during a freeze or thaw.
|---|---|---|
| PVE/PBS/PMG inventory, node status, VM/container status, storage usage, and normal Proxmox API metrics | Add the Proxmox connection with a read-only or narrowly scoped API token | No |
| VM guest disk and memory details through QEMU Guest Agent | Use Proxmox API permissions such as `VM.GuestAgent.Audit` and `VM.GuestAgent.FileRead` where supported | No host agent for the Proxmox node |
| All mounted LXC filesystem capacities and usage | Install the Unified Agent on the owning PVE node; the safe profile uses the typed helper's bounded fixed-shape `pct` reads | Yes, but the safe profile confines it to the root helper rather than a root collector |
| Configured LXC filesystem capacities and usage | Install the Unified Agent on the owning PVE node; the safe profile uses [socket-free discovery and bounded mount probes](#proxmox-lxc-filesystem-readings-with-the-typed-helper) | Yes, but the safe profile confines it to the root helper rather than a root collector |
| Docker/Podman containers inside a VM or LXC through guest-local reporting | Install the agent inside that VM/LXC with Docker/Podman monitoring enabled, or use another explicit guest access/reporting path | Requires runtime-socket access: root-equivalent for a rootful daemon, or a collector-owned rootless socket |
| Docker containers inside an LXC from a Proxmox host agent | Turn on **Discover Docker in LXC guests** in Settings → System → General (admin only), or start Pulse with `PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true` to lock it on; optionally limit guests with `PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS=101,102` | Requires a root/equivalent Pulse agent on the Proxmox node and explicit server opt-in |
| Host SMART, temperatures, local ZFS/Ceph/mdadm detail, arbitrary mount reads, and full host telemetry | Install the agent on that host | Varies: the safe typed helper covers SMART; capabilities outside the matrix still require the explicit root/full-telemetry profile |
@@ -114,10 +114,10 @@ LXCs by default. Its automatic LXC filesystem collector is a node-local
capacity query only. On cgroup v2 PVE hosts it finds running guests from the
node's container configs and cgroup state, reads each running guest's init
process ID and status from cgroup and `/proc`, and measures mount capacity with
statfs through `/proc/<pid>/root`. When that discovery cannot establish a guest's state (for
example on cgroup v1 hosts) it falls back to `pct list`, with `lxc-info` to
find each guest's init process, and it runs `pct df <vmid>` for a guest only
when the statfs probe is unavailable or fails. It reports
statfs through `/proc/<pid>/root`. Root or explicitly `--grant-pct` collectors can fall back to
`pct list` and `pct df <vmid>` (with `lxc-info` where permitted) when discovery or a mount probe fails.
That command path is not usable inside the isolated typed helper; see
[helper-backed LXC readings](#proxmox-lxc-filesystem-readings-with-the-typed-helper). It reports
mount keys, volume labels, mount paths, and capacity/usage numbers. It does not
run a command inside the guest or read guest file contents, environment, or
container-runtime metadata. It skips guests reported stopped and bounds command
@@ -198,22 +198,6 @@ block devices. If the helper is missing, incompatible, or rejects a request,
only the affected telemetry disappears; the collector does not fall back to
sudo, root, or a broader local command path.
Proxmox's `pct list` uses pmxcfs abstract Unix IPC, which is not reachable from
the helper's private network namespace. On affected PVE hosts this leaves
helper-backed LXC filesystem telemetry unavailable, while independent SMART
and API monitoring can continue. Prefer the existing Proxmox API connection
for the readings it provides; do not add a unit override or a root/sudo
fallback. A successful SMART reading does not establish LXC helper health.
When an inventory succeeds only partly, operation v2 preserves the collected
rows and explicitly names omitted container IDs. The helper module stays
degraded, and omitted containers cannot refresh old filesystem readings.
Operation v1 remains complete-only; a newer collector uses it only when an
older helper explicitly rejects v2. An unavailable inventory is not an empty
healthy snapshot or zero disk usage. An installer-managed helper upgrade
restarts the verified helper after checking the effective isolated unit and
socket identity; a collector-only automatic update cannot replace its helper.
The installer treats the effective systemd configuration as part of the safe
profile boundary. The collector, helper service, helper socket, and an
installed action runner must load from the installer-owned `FragmentPath` with
@@ -291,6 +275,41 @@ packet never reaches Docker. The post-run attester re-verifies those same
snapshot paths against the receipt. No qualifying RC containing this boundary
has been published yet.
### Proxmox LXC filesystem readings with the typed helper
The current helper can collect LXC filesystem readings **without `pct` or
`lxc-info` requests**. It reads the owning node's container configuration,
finds running guests and their init processes through cgroup v2 and `/proc`,
and measures configured mounts with the bounded filesystem prober. It does not
execute a command inside a guest, read guest file contents or accept a
caller-selected VMID or path.
Proxmox's `pct` and `lxc-info` commands use host-network-namespace Unix IPC.
Their fallback cannot recover a failed discovery or mount probe inside
`PrivateNetwork=true`. On a cgroup v1 host, or when a running guest's state
cannot be established, the helper can therefore report degraded or unavailable
inventory rather than a complete list. Independent SMART and API readings can
still work; a successful SMART reading does not establish LXC helper health.
Use the existing reading's status, age and collection path before treating
missing filesystems as missing guests. Keep the Pulse server, collector and
helper versions separate; a newer guide or collector alone does not establish
that the installed helper has this path. Older helper versions relying on
`pct` can fail even when SMART works. Prefer the existing Proxmox API connection
for the readings it already provides, without claiming it covers every mount.
Do not remove `PrivateNetwork`, add a unit override, enable a root/sudo
fallback, reinstall or restart anything merely to diagnose a missing reading.
Keep credentials and full service/configuration records private.
When an inventory succeeds only partly, operation v2 preserves the collected
rows and explicitly names omitted container IDs. The helper module stays
degraded, and omitted containers cannot refresh old filesystem readings.
Operation v1 remains complete-only; a newer collector uses it only when an
older helper explicitly rejects v2. An unavailable inventory is not an empty
healthy snapshot or zero disk usage. An installer-managed helper upgrade
restarts the verified helper after checking the effective isolated unit and
socket identity; a collector-only automatic update cannot replace its helper.
### Safe-profile support and qualification matrix
`Qualified` below means the named evidence exercised the behavior on a real
@@ -305,7 +324,7 @@ or generic command path.
| Proxmox VE/PBS/PMG inventory, status, storage, and ordinary metrics | API-only connection with a narrowly scoped token; no host agent | No collector, helper, or runner authority is required for this data | Supported independently of the safe host-agent profile; it does not prove host-local SMART, LXC filesystem, or action parity | `agent-lifecycle`: keep API permissions and returned telemetry covered by provider tests |
| Standard Linux systemd host telemetry and collector update | Unprivileged `pulse-agent` plus the root-owned typed helper | Core `/proc`, filesystem, network, RAID, and hwmon telemetry stays in the collector; signed updates cross the fixed helper protocol and commit only after an authoritative report | **Qualified on disposable Ubuntu 24.04.4 arm64 at committed main `22fd662fb794f63efb9d3ca2158de73c4e07e1b8`** across the schema-v6 twenty-scenario run. It covers install, migration, explicit/automatic rollback, helper protocol and resource boundaries, effective-unit override rejection, reporting continuity, authoritative executable-digest commit, watchdog rollback, helper-restart recovery, and last-known-good restoration. It remains opt-in pending exact-RC reproduction, representative provider/appliance parity, and external review | `deployment-installability` and `security-privacy`: reproduce the same helper transaction from trusted release artifacts on representative hosts and accept the external boundary review |
| Linux SMART telemetry | `smart.snapshot` through the no-network helper; no caller-selected device or arguments | Implemented, unqualified on representative physical disks. Helper failure omits/degrades SMART only; the collector does not retry as root | Does not yet justify SMART parity or a default change | `agent-lifecycle`: record live SATA, SAS/controller, USB bridge, and NVMe evidence, including standby, permission failure, timeout, and partial-data cases |
| Proxmox node-local LXC filesystem telemetry | `proxmox.lxc_filesystems` through the no-network helper. Configs are read through the pmxcfs mount, running guests and their init processes come from cgroup v2 and `/proc`, and usage comes from the shared filesystem prober, which pins the guest's init, re-checks its identity, and resolves each mount inside the guest root (openat2, or a symlink-refusing component walk where the helper's seccomp filter blocks openat2); the caller selects no VMID, path, or command. `pct` and `lxc-info` cannot work inside the helper, because pmxcfs and the LXC monitor listen on abstract Unix sockets that belong to the host network namespace (#2511); they remain the fallback whenever discovery cannot establish a guest's state, on cgroup v1 hosts, for root and `--grant-pct` installs, and per container when the prober read fails | Implemented. Exercised on a disposable PVE 9.2.18 node under the helper unit's sandbox properties, with running unprivileged (rootfs plus a mount point) and privileged guests and a stopped one; readings matched `pct df`. This is one PVE version, with no timeout, output-bound, helper-loss, or live nested-namespace evidence. Helper failure omits/degrades this snapshot only | Does not yet justify Proxmox host-agent parity or a default change | `agent-lifecycle`: record timeout, output-bound, and helper-loss behavior across supported PVE versions |
| Proxmox node-local LXC filesystem telemetry | `proxmox.lxc_filesystems` through the no-network helper's [socket-free discovery and bounded mount probes](#proxmox-lxc-filesystem-readings-with-the-typed-helper). Failed discovery or probes remain degraded/unavailable; command fallback works only outside the helper, in a separately chosen root or `--grant-pct` profile | Implemented. Exercised on a disposable PVE 9.2.18 node under the helper unit's sandbox properties, with running unprivileged (rootfs plus a mount point) and privileged guests and a stopped one; readings matched `pct df`. This is one PVE version, with no timeout, output-bound, helper-loss, or live nested-namespace evidence. Helper failure omits/degrades this snapshot only | Does not yet justify Proxmox host-agent parity or a default change | `agent-lifecycle`: record timeout, output-bound, and helper-loss behavior across supported PVE versions |
| Rootful Docker or Podman inventory | No direct collector access to a root-equivalent daemon socket | Implemented as a typed-helper summary-only fallback. Migration preserves container ID/name/image/state/status/creation inventory and marks the report `typed-helper-summary`; stats, secondary inventories, update checks, and actions remain unavailable | The standalone rootless packet exercises live same-family rootful Docker and Podman helper fallback plus summary-inventory semantic parity during socket loss. It does not qualify the complete standalone rootful fresh-install, migration, restart, helper-loss/recovery, and bounds matrix. Full rootful parity remains an explicit default blocker; the legacy/root profile is not safe-profile evidence | `agent-lifecycle`: record the remaining standalone lifecycle matrix on representative rootful Docker and Podman and decide explicitly whether reduced telemetry is sufficient |
| Collector-owned rootless Docker or Podman | Direct access only to one usable runtime socket owned by the `pulse-agent` UID whose daemon also attests rootless mode | Qualified locally for the exact source-bound monitoring path. Discovery is deferred until the collector account exists and accepts exactly one readable/writable, non-symlink socket owned by that UID across Docker and Podman. Cross-user, root-owned, remote, unreadable, non-rootless-daemon, or ambiguous endpoints are rejected. An installer-owned exact pin survives daemon loss and update; the running collector can move between direct rootless monitoring and rootful typed-helper summary without regaining action authority | **Qualified on distinct disposable Ubuntu 24.04/systemd 255 arm64 hosts at commit `60041ad9e60c282c892f944e04f777b874991a5d`.** All eleven canonical scenarios passed for Docker and all eleven for Podman. Receipt SHA-256: `7a116d63ab0cd1560482165055f0a8c9158ce0a8333a27bd9e8256582a52dfb5`; local attestation SHA-256: `566279ecd7d7dfa89c92f24243e9fcd5ae3e295d4284bd559d4be42f1e83b3b5`. This is local opt-in artifact-bound self-attestation, not published-RC provenance, action/update qualification, external review, or authorization for a default change | `deployment-installability`: reproduce the exact packet from published release artifacts; separately qualify any desired rootless image-update/action surface; retain the opt-in default until the remaining provider, appliance, and review conditions pass |
| Separate runner package update and package-cache cleanup | Root-owned `pulse-agent-runner`, host-bound action credential, typed request, postcondition, and durable receipt | The schema-v6 committed-main systemd receipt records a real verified apt-cache mutation, stale-fingerprint refusal, replay, nonce-bound readiness, effective-unit override rejection, exact credential rotation, bodyless self-revocation, and collector/helper continuity. A separate production Router regression exercises HTTPS issuance, WSS admission, encrypted token persistence, failed-rotation rollback, exact socket invalidation, two server restarts, old-secret rejection, and durable self-revoke | Qualified for the exercised systemd fixture paths and focused production Router lifecycle. Focused installer tests also cover atomic pending cancellation and fail-closed credential retention. The evidence does not cover every runner operation, representative providers, or an exact release candidate | `agent-lifecycle` and `api-contracts`: reproduce the combined systemd and production Router path from trusted RC artifacts with representative package-update success/failure/cancellation evidence |
+7 -5
View File
@@ -691,11 +691,13 @@ and task inventory on the Docker page.
Pulse does not use a Proxmox node agent to look inside LXCs by default. The
node agent does automatically collect filesystem capacity for running LXCs
when the local `pct` tool is available. On cgroup v2 PVE hosts it finds running
guests from the node's container configs and cgroup state and measures mount
capacity with statfs through each guest's init process in `/proc`; it falls back
to bounded `pct list` and `pct df <vmid>` calls when that discovery or a
per-container probe cannot establish a guest's state. It reports only mount
when the local `pct` tool is available. The current collector first uses
socket-free cgroup v2 and `/proc` discovery plus bounded mount probes. A
root or explicitly `--grant-pct` collector can fall back to `pct list` and
`pct df <vmid>` (with `lxc-info` where permitted); those commands cannot recover readings inside the isolated
typed helper. See [helper-backed LXC readings](AGENT_SECURITY.md#proxmox-lxc-filesystem-readings-with-the-typed-helper)
for partial, unavailable and older-helper results. Do not remove `PrivateNetwork`
or broaden the agent's privileges to clear a missing reading. It reports only mount
keys, volume labels, mount paths, and capacity/usage values; it does not run
commands inside a guest or read guest files. Stopped LXCs retain the normal
API-derived disk view.
@@ -0,0 +1,81 @@
{
"version": 1,
"base_sha": "399618939d3e7d1711b24ecbd43ba9129fed2d56",
"verified_at": "2026-10-14T05:40:36Z",
"result": "passed",
"documentation_paths": [
"docs/AGENT_SECURITY.md",
"docs/UNIFIED_AGENT.md",
"frontend-modern/public/docs/AGENT_SECURITY.md",
"frontend-modern/public/docs/UNIFIED_AGENT.md"
],
"content_sha256": {
"docs/AGENT_SECURITY.md": "ed6179dfb8665f707cdd2e38cb840f91b161729779b009802ad40eb3a4a16f6e",
"docs/UNIFIED_AGENT.md": "6ddbd9d5661bccf5c32c08b666d5c55d68844c44beda571fff9b5599d37f7b47",
"frontend-modern/public/docs/AGENT_SECURITY.md": "ed6179dfb8665f707cdd2e38cb840f91b161729779b009802ad40eb3a4a16f6e",
"frontend-modern/public/docs/UNIFIED_AGENT.md": "6ddbd9d5661bccf5c32c08b666d5c55d68844c44beda571fff9b5599d37f7b47"
},
"routes": [
"/docs/UNIFIED_AGENT#inside-guest-runtime-boundaries",
"/docs/AGENT_SECURITY#proxmox-lxc-filesystem-readings-with-the-typed-helper"
],
"viewports": [
{
"engine": "desktop-chromium",
"browser_version": "141.0.7390.37",
"width": 1360,
"height": 900,
"theme": "light"
},
{
"engine": "phone-webkit",
"browser_version": "26.0",
"width": 390,
"height": 900,
"theme": "dark"
}
],
"playwright": "1.56.1",
"source_inputs": 2827,
"binding_sha256": "4e24d7487f8bd99e0ee568ee70d6bc5949bfe28a7c9a0aea5767fe0bd0b4719a",
"served_document_sha256": {
"frontend-modern/public/docs/AGENT_SECURITY.md": "ed6179dfb8665f707cdd2e38cb840f91b161729779b009802ad40eb3a4a16f6e",
"frontend-modern/public/docs/UNIFIED_AGENT.md": "6ddbd9d5661bccf5c32c08b666d5c55d68844c44beda571fff9b5599d37f7b47"
},
"interactions": [
"Actual shipped Unified Agent link: keyboard Enter on desktop, touch on phone; same-router navigation, heading focus, reload deep-link focus.",
"Confined discovery/fallback, old-version, partial-v2/strict-v1, not-zero and helper-update distinctions rendered from actual mirrored files; no API requests or host operation."
],
"pixel_review": "All six captures opened with native view_image; readable body, wrapped phone heading/code and safety warnings, no page overflow. Existing wide qualification matrix has a separate horizontal viewport; not every column is qualified.",
"captures": [
{
"file": "desktop-chromium-entry.png",
"sha256": "0f0adc112d0b0259dfae0dc481fa470e0d1939bf560cf2f5ebd72e7a36116c54"
},
{
"file": "desktop-chromium-helper.png",
"sha256": "e89cc59255e039576df50c0682c2f74c993dbdad0e745ba7769b2ceea5cbb89b"
},
{
"file": "desktop-chromium-partial.png",
"sha256": "d66fa63d1678f17b237b5ba3c6995e50138cd8d14522d0e4534f5ac847d36ed2"
},
{
"file": "phone-webkit-entry.png",
"sha256": "518c801ce3efb2b70920ba4c08a09b24f786ef7195f990daf89a28478495e946"
},
{
"file": "phone-webkit-helper.png",
"sha256": "bdab502180e3c4436b616463aadef0bb393a0a785cea1a6356546a61dbb7f281"
},
{
"file": "phone-webkit-partial.png",
"sha256": "606087d9992d36a8f9d075babd381e77a76e6ef3153f2e9015fd7847fc892164"
}
],
"cleanup": {
"browser_closed": true,
"server_closed": true
},
"limits": "Static shipped help, not native PVE/helper recovery, every-device, published or installed acceptance. A frontend-file guard skip is not this browser proof."
}
@@ -55,7 +55,7 @@ try {
for (const file of ['package.json', 'package-lock.json', 'tsconfig.json', 'index.html'])
binding.sources[file] = hash(fs.readFileSync(path.join(root, file)));
fs.mkdirSync(path.join(output, 'docs'), { recursive: true });
for (const name of ['FAQ', 'UNIFIED_AGENT', 'TROUBLESHOOTING', 'DOCKER', 'PBS']) {
for (const name of ['FAQ', 'UNIFIED_AGENT', 'TROUBLESHOOTING', 'DOCKER', 'PBS', 'AGENT_SECURITY']) {
const file = `public/docs/${name}.md`;
binding.sources[file] = hash(fs.readFileSync(path.join(root, file)));
fs.copyFileSync(path.join(root, file), path.join(output, 'docs', `${name}.md`));
@@ -0,0 +1,123 @@
// Real shipped Docs fetch/renderer/router, with no host or agent operation.
const fs = require('node:fs');
const path = require('node:path');
const http = require('node:http');
const crypto = require('node:crypto');
const assert = require('node:assert/strict');
const { chromium, webkit } = require('playwright');
const root = '/workspace/frontend-modern';
const preview = '/workspace/tmp/doc-command-help/static';
const output = '/workspace/tmp/pve-helper-guidance';
const anchor = 'proxmox-lxc-filesystem-readings-with-the-typed-helper';
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
const bindingBytes = fs.readFileSync(path.join(preview, 'binding.json'));
const binding = JSON.parse(bindingBytes);
const result = { result: 'incomplete', source_sha: binding.source_sha,
binding_sha256: digest(bindingBytes), playwright: require('playwright/package.json').version,
cases: [], captures: [], errors: [], requests: [], served: [], cleanup: {},
limits: 'Shipped help navigation/reading only; no native PVE, helper, installed recovery or release acceptance.' };
(async () => {
let server, browser;
try {
fs.mkdirSync(output, { recursive: true });
assert.equal(result.playwright, '1.56.1');
assert.equal(result.playwright, JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json'))
.packages['node_modules/@playwright/test'].version);
for (const [file, hash] of Object.entries(binding.sources))
assert.equal(digest(fs.readFileSync(path.join(root, file))), hash, file);
const assets = new Map();
for (const [file, hash] of Object.entries(binding.artifacts)) {
const bytes = fs.readFileSync(path.join(preview, file));
assert.equal(digest(bytes), hash, file); assets.set(file, bytes);
}
for (const name of ['AGENT_SECURITY', 'UNIFIED_AGENT'])
assert.equal(binding.artifacts[`docs/${name}.md`], binding.sources[`public/docs/${name}.md`]);
result.source_inputs = Object.keys(binding.sources).length;
server = http.createServer((req, res) => {
if (req.method !== 'GET') { res.writeHead(405); res.end(); return; }
const pathname = new URL(req.url, 'http://fixture.invalid').pathname;
const file = pathname.startsWith('/docs/') && !pathname.endsWith('.md')
? 'browser-tests/doc-command-help.html' : pathname.slice(1);
if (!assets.has(file)) { res.writeHead(404); res.end(); return; }
const bytes = assets.get(file);
result.served.push({ pathname, file, sha256: digest(bytes) });
const ext = path.extname(file);
res.writeHead(200, { 'Content-Type': ({ '.md': 'text/markdown', '.css': 'text/css',
'.js': 'text/javascript', '.html': 'text/html' })[ext] + '; charset=utf-8', 'Cache-Control': 'no-store' });
res.end(bytes);
});
await new Promise((resolve, reject) => {
server.once('error', reject); server.listen(0, '127.0.0.1', resolve);
});
const origin = `http://127.0.0.1:${server.address().port}`;
for (const [name, engine, width, dark] of [
['desktop-chromium', chromium, 1360, false], ['phone-webkit', webkit, 390, true],
]) {
browser = await engine.launch(engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } : { headless: true });
const context = await browser.newContext({ viewport: { width, height: 900 },
isMobile: width < 500, hasTouch: width < 500, locale: 'en-GB' });
const page = await context.newPage(); page.setDefaultTimeout(12000);
const record = { name, width, height: 900, dark, browser: browser.version(), checks: [] };
result.cases.push(record);
page.on('pageerror', error => result.errors.push(error.message));
await page.route('**/*', route => {
const url = new URL(route.request().url()); result.requests.push({ name, path: url.pathname });
assert.equal(url.origin, origin, 'no external requests permitted');
assert(!url.pathname.startsWith('/api/'), 'help must not probe a backend');
return route.continue();
});
await page.addInitScript(dark => document.addEventListener('DOMContentLoaded', () =>
document.documentElement.classList.toggle('dark', dark)), dark);
const capture = async suffix => {
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'page overflow');
const file = `${name}-${suffix}.png`; await page.screenshot({ path: path.join(output, file) });
result.captures.push({ file, sha256: digest(fs.readFileSync(path.join(output, file))), name });
};
const activate = async link => {
if (width < 500) await link.tap(); else { await link.focus(); await page.keyboard.press('Enter'); }
};
await page.goto(`${origin}/docs/UNIFIED_AGENT#inside-guest-runtime-boundaries`, { waitUntil: 'domcontentloaded' });
const link = page.locator(`article a[href="/docs/AGENT_SECURITY#${anchor}"]`);
await link.waitFor();
await link.evaluate(element => window.scrollBy(0, element.closest('p').getBoundingClientRect().top - 64));
await capture('entry');
await page.evaluate(() => window.__helperDocsShell = 'same-shell');
await activate(link); await page.waitForURL(`**/docs/AGENT_SECURITY#${anchor}`);
await page.waitForFunction(anchor => {
const heading = document.getElementById(anchor);
return heading && document.activeElement === heading && heading.getBoundingClientRect().top >= 0
&& heading.getBoundingClientRect().top < innerHeight;
}, anchor);
assert.equal(await page.evaluate(() => window.__helperDocsShell), 'same-shell');
const section = await page.locator(`#${anchor}`).evaluate(heading => {
let text = ''; for (let el = heading.nextElementSibling; el && el.tagName !== 'H3'; el = el.nextElementSibling)
text += el.textContent + ' '; return text.replace(/\s+/g, ' ');
});
for (const phrase of ['without pct or lxc-info requests', 'fallback cannot recover',
'cgroup v1', 'degraded or unavailable', 'Do not remove PrivateNetwork',
'omitted containers cannot refresh old filesystem readings', 'Operation v1 remains complete-only',
'collector-only automatic update cannot replace its helper']) assert(section.includes(phrase), phrase);
record.checks.push('actual shipped cross-guide link, keyboard/touch, same shell, focused anchor, all boundary/partial distinctions');
await capture('helper');
const partial = page.locator('article p').filter({ hasText: 'When an inventory succeeds only partly' });
await partial.evaluate(element => window.scrollBy(0, element.getBoundingClientRect().top - 64));
await capture('partial');
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(anchor => document.activeElement?.id === anchor, anchor);
record.checks.push('reload preserves deep-link heading focus');
await context.close(); await browser.close(); browser = null;
}
assert.equal(result.errors.length, 0);
result.result = 'passed';
} catch (error) { result.failure = error.stack; throw error; }
finally {
if (browser) await browser.close(); result.cleanup.browser_closed = true;
if (server) await new Promise((resolve, reject) => {
server.close(error => error ? reject(error) : resolve()); server.closeAllConnections();
}); result.cleanup.server_closed = true;
fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2) + '\n');
}
})().catch(error => { console.error(error); process.exitCode = 1; });
+41 -22
View File
@@ -101,7 +101,7 @@ manual probes during a freeze or thaw.
|---|---|---|
| PVE/PBS/PMG inventory, node status, VM/container status, storage usage, and normal Proxmox API metrics | Add the Proxmox connection with a read-only or narrowly scoped API token | No |
| VM guest disk and memory details through QEMU Guest Agent | Use Proxmox API permissions such as `VM.GuestAgent.Audit` and `VM.GuestAgent.FileRead` where supported | No host agent for the Proxmox node |
| All mounted LXC filesystem capacities and usage | Install the Unified Agent on the owning PVE node; the safe profile uses the typed helper's bounded fixed-shape `pct` reads | Yes, but the safe profile confines it to the root helper rather than a root collector |
| Configured LXC filesystem capacities and usage | Install the Unified Agent on the owning PVE node; the safe profile uses [socket-free discovery and bounded mount probes](#proxmox-lxc-filesystem-readings-with-the-typed-helper) | Yes, but the safe profile confines it to the root helper rather than a root collector |
| Docker/Podman containers inside a VM or LXC through guest-local reporting | Install the agent inside that VM/LXC with Docker/Podman monitoring enabled, or use another explicit guest access/reporting path | Requires runtime-socket access: root-equivalent for a rootful daemon, or a collector-owned rootless socket |
| Docker containers inside an LXC from a Proxmox host agent | Turn on **Discover Docker in LXC guests** in Settings → System → General (admin only), or start Pulse with `PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true` to lock it on; optionally limit guests with `PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS=101,102` | Requires a root/equivalent Pulse agent on the Proxmox node and explicit server opt-in |
| Host SMART, temperatures, local ZFS/Ceph/mdadm detail, arbitrary mount reads, and full host telemetry | Install the agent on that host | Varies: the safe typed helper covers SMART; capabilities outside the matrix still require the explicit root/full-telemetry profile |
@@ -114,10 +114,10 @@ LXCs by default. Its automatic LXC filesystem collector is a node-local
capacity query only. On cgroup v2 PVE hosts it finds running guests from the
node's container configs and cgroup state, reads each running guest's init
process ID and status from cgroup and `/proc`, and measures mount capacity with
statfs through `/proc/<pid>/root`. When that discovery cannot establish a guest's state (for
example on cgroup v1 hosts) it falls back to `pct list`, with `lxc-info` to
find each guest's init process, and it runs `pct df <vmid>` for a guest only
when the statfs probe is unavailable or fails. It reports
statfs through `/proc/<pid>/root`. Root or explicitly `--grant-pct` collectors can fall back to
`pct list` and `pct df <vmid>` (with `lxc-info` where permitted) when discovery or a mount probe fails.
That command path is not usable inside the isolated typed helper; see
[helper-backed LXC readings](#proxmox-lxc-filesystem-readings-with-the-typed-helper). It reports
mount keys, volume labels, mount paths, and capacity/usage numbers. It does not
run a command inside the guest or read guest file contents, environment, or
container-runtime metadata. It skips guests reported stopped and bounds command
@@ -198,22 +198,6 @@ block devices. If the helper is missing, incompatible, or rejects a request,
only the affected telemetry disappears; the collector does not fall back to
sudo, root, or a broader local command path.
Proxmox's `pct list` uses pmxcfs abstract Unix IPC, which is not reachable from
the helper's private network namespace. On affected PVE hosts this leaves
helper-backed LXC filesystem telemetry unavailable, while independent SMART
and API monitoring can continue. Prefer the existing Proxmox API connection
for the readings it provides; do not add a unit override or a root/sudo
fallback. A successful SMART reading does not establish LXC helper health.
When an inventory succeeds only partly, operation v2 preserves the collected
rows and explicitly names omitted container IDs. The helper module stays
degraded, and omitted containers cannot refresh old filesystem readings.
Operation v1 remains complete-only; a newer collector uses it only when an
older helper explicitly rejects v2. An unavailable inventory is not an empty
healthy snapshot or zero disk usage. An installer-managed helper upgrade
restarts the verified helper after checking the effective isolated unit and
socket identity; a collector-only automatic update cannot replace its helper.
The installer treats the effective systemd configuration as part of the safe
profile boundary. The collector, helper service, helper socket, and an
installed action runner must load from the installer-owned `FragmentPath` with
@@ -291,6 +275,41 @@ packet never reaches Docker. The post-run attester re-verifies those same
snapshot paths against the receipt. No qualifying RC containing this boundary
has been published yet.
### Proxmox LXC filesystem readings with the typed helper
The current helper can collect LXC filesystem readings **without `pct` or
`lxc-info` requests**. It reads the owning node's container configuration,
finds running guests and their init processes through cgroup v2 and `/proc`,
and measures configured mounts with the bounded filesystem prober. It does not
execute a command inside a guest, read guest file contents or accept a
caller-selected VMID or path.
Proxmox's `pct` and `lxc-info` commands use host-network-namespace Unix IPC.
Their fallback cannot recover a failed discovery or mount probe inside
`PrivateNetwork=true`. On a cgroup v1 host, or when a running guest's state
cannot be established, the helper can therefore report degraded or unavailable
inventory rather than a complete list. Independent SMART and API readings can
still work; a successful SMART reading does not establish LXC helper health.
Use the existing reading's status, age and collection path before treating
missing filesystems as missing guests. Keep the Pulse server, collector and
helper versions separate; a newer guide or collector alone does not establish
that the installed helper has this path. Older helper versions relying on
`pct` can fail even when SMART works. Prefer the existing Proxmox API connection
for the readings it already provides, without claiming it covers every mount.
Do not remove `PrivateNetwork`, add a unit override, enable a root/sudo
fallback, reinstall or restart anything merely to diagnose a missing reading.
Keep credentials and full service/configuration records private.
When an inventory succeeds only partly, operation v2 preserves the collected
rows and explicitly names omitted container IDs. The helper module stays
degraded, and omitted containers cannot refresh old filesystem readings.
Operation v1 remains complete-only; a newer collector uses it only when an
older helper explicitly rejects v2. An unavailable inventory is not an empty
healthy snapshot or zero disk usage. An installer-managed helper upgrade
restarts the verified helper after checking the effective isolated unit and
socket identity; a collector-only automatic update cannot replace its helper.
### Safe-profile support and qualification matrix
`Qualified` below means the named evidence exercised the behavior on a real
@@ -305,7 +324,7 @@ or generic command path.
| Proxmox VE/PBS/PMG inventory, status, storage, and ordinary metrics | API-only connection with a narrowly scoped token; no host agent | No collector, helper, or runner authority is required for this data | Supported independently of the safe host-agent profile; it does not prove host-local SMART, LXC filesystem, or action parity | `agent-lifecycle`: keep API permissions and returned telemetry covered by provider tests |
| Standard Linux systemd host telemetry and collector update | Unprivileged `pulse-agent` plus the root-owned typed helper | Core `/proc`, filesystem, network, RAID, and hwmon telemetry stays in the collector; signed updates cross the fixed helper protocol and commit only after an authoritative report | **Qualified on disposable Ubuntu 24.04.4 arm64 at committed main `22fd662fb794f63efb9d3ca2158de73c4e07e1b8`** across the schema-v6 twenty-scenario run. It covers install, migration, explicit/automatic rollback, helper protocol and resource boundaries, effective-unit override rejection, reporting continuity, authoritative executable-digest commit, watchdog rollback, helper-restart recovery, and last-known-good restoration. It remains opt-in pending exact-RC reproduction, representative provider/appliance parity, and external review | `deployment-installability` and `security-privacy`: reproduce the same helper transaction from trusted release artifacts on representative hosts and accept the external boundary review |
| Linux SMART telemetry | `smart.snapshot` through the no-network helper; no caller-selected device or arguments | Implemented, unqualified on representative physical disks. Helper failure omits/degrades SMART only; the collector does not retry as root | Does not yet justify SMART parity or a default change | `agent-lifecycle`: record live SATA, SAS/controller, USB bridge, and NVMe evidence, including standby, permission failure, timeout, and partial-data cases |
| Proxmox node-local LXC filesystem telemetry | `proxmox.lxc_filesystems` through the no-network helper. Configs are read through the pmxcfs mount, running guests and their init processes come from cgroup v2 and `/proc`, and usage comes from the shared filesystem prober, which pins the guest's init, re-checks its identity, and resolves each mount inside the guest root (openat2, or a symlink-refusing component walk where the helper's seccomp filter blocks openat2); the caller selects no VMID, path, or command. `pct` and `lxc-info` cannot work inside the helper, because pmxcfs and the LXC monitor listen on abstract Unix sockets that belong to the host network namespace (#2511); they remain the fallback whenever discovery cannot establish a guest's state, on cgroup v1 hosts, for root and `--grant-pct` installs, and per container when the prober read fails | Implemented. Exercised on a disposable PVE 9.2.18 node under the helper unit's sandbox properties, with running unprivileged (rootfs plus a mount point) and privileged guests and a stopped one; readings matched `pct df`. This is one PVE version, with no timeout, output-bound, helper-loss, or live nested-namespace evidence. Helper failure omits/degrades this snapshot only | Does not yet justify Proxmox host-agent parity or a default change | `agent-lifecycle`: record timeout, output-bound, and helper-loss behavior across supported PVE versions |
| Proxmox node-local LXC filesystem telemetry | `proxmox.lxc_filesystems` through the no-network helper's [socket-free discovery and bounded mount probes](#proxmox-lxc-filesystem-readings-with-the-typed-helper). Failed discovery or probes remain degraded/unavailable; command fallback works only outside the helper, in a separately chosen root or `--grant-pct` profile | Implemented. Exercised on a disposable PVE 9.2.18 node under the helper unit's sandbox properties, with running unprivileged (rootfs plus a mount point) and privileged guests and a stopped one; readings matched `pct df`. This is one PVE version, with no timeout, output-bound, helper-loss, or live nested-namespace evidence. Helper failure omits/degrades this snapshot only | Does not yet justify Proxmox host-agent parity or a default change | `agent-lifecycle`: record timeout, output-bound, and helper-loss behavior across supported PVE versions |
| Rootful Docker or Podman inventory | No direct collector access to a root-equivalent daemon socket | Implemented as a typed-helper summary-only fallback. Migration preserves container ID/name/image/state/status/creation inventory and marks the report `typed-helper-summary`; stats, secondary inventories, update checks, and actions remain unavailable | The standalone rootless packet exercises live same-family rootful Docker and Podman helper fallback plus summary-inventory semantic parity during socket loss. It does not qualify the complete standalone rootful fresh-install, migration, restart, helper-loss/recovery, and bounds matrix. Full rootful parity remains an explicit default blocker; the legacy/root profile is not safe-profile evidence | `agent-lifecycle`: record the remaining standalone lifecycle matrix on representative rootful Docker and Podman and decide explicitly whether reduced telemetry is sufficient |
| Collector-owned rootless Docker or Podman | Direct access only to one usable runtime socket owned by the `pulse-agent` UID whose daemon also attests rootless mode | Qualified locally for the exact source-bound monitoring path. Discovery is deferred until the collector account exists and accepts exactly one readable/writable, non-symlink socket owned by that UID across Docker and Podman. Cross-user, root-owned, remote, unreadable, non-rootless-daemon, or ambiguous endpoints are rejected. An installer-owned exact pin survives daemon loss and update; the running collector can move between direct rootless monitoring and rootful typed-helper summary without regaining action authority | **Qualified on distinct disposable Ubuntu 24.04/systemd 255 arm64 hosts at commit `60041ad9e60c282c892f944e04f777b874991a5d`.** All eleven canonical scenarios passed for Docker and all eleven for Podman. Receipt SHA-256: `7a116d63ab0cd1560482165055f0a8c9158ce0a8333a27bd9e8256582a52dfb5`; local attestation SHA-256: `566279ecd7d7dfa89c92f24243e9fcd5ae3e295d4284bd559d4be42f1e83b3b5`. This is local opt-in artifact-bound self-attestation, not published-RC provenance, action/update qualification, external review, or authorization for a default change | `deployment-installability`: reproduce the exact packet from published release artifacts; separately qualify any desired rootless image-update/action surface; retain the opt-in default until the remaining provider, appliance, and review conditions pass |
| Separate runner package update and package-cache cleanup | Root-owned `pulse-agent-runner`, host-bound action credential, typed request, postcondition, and durable receipt | The schema-v6 committed-main systemd receipt records a real verified apt-cache mutation, stale-fingerprint refusal, replay, nonce-bound readiness, effective-unit override rejection, exact credential rotation, bodyless self-revocation, and collector/helper continuity. A separate production Router regression exercises HTTPS issuance, WSS admission, encrypted token persistence, failed-rotation rollback, exact socket invalidation, two server restarts, old-secret rejection, and durable self-revoke | Qualified for the exercised systemd fixture paths and focused production Router lifecycle. Focused installer tests also cover atomic pending cancellation and fail-closed credential retention. The evidence does not cover every runner operation, representative providers, or an exact release candidate | `agent-lifecycle` and `api-contracts`: reproduce the combined systemd and production Router path from trusted RC artifacts with representative package-update success/failure/cancellation evidence |
+7 -5
View File
@@ -691,11 +691,13 @@ and task inventory on the Docker page.
Pulse does not use a Proxmox node agent to look inside LXCs by default. The
node agent does automatically collect filesystem capacity for running LXCs
when the local `pct` tool is available. On cgroup v2 PVE hosts it finds running
guests from the node's container configs and cgroup state and measures mount
capacity with statfs through each guest's init process in `/proc`; it falls back
to bounded `pct list` and `pct df <vmid>` calls when that discovery or a
per-container probe cannot establish a guest's state. It reports only mount
when the local `pct` tool is available. The current collector first uses
socket-free cgroup v2 and `/proc` discovery plus bounded mount probes. A
root or explicitly `--grant-pct` collector can fall back to `pct list` and
`pct df <vmid>` (with `lxc-info` where permitted); those commands cannot recover readings inside the isolated
typed helper. See [helper-backed LXC readings](AGENT_SECURITY.md#proxmox-lxc-filesystem-readings-with-the-typed-helper)
for partial, unavailable and older-helper results. Do not remove `PrivateNetwork`
or broaden the agent's privileges to clear a missing reading. It reports only mount
keys, volume labels, mount paths, and capacity/usage values; it does not run
commands inside a guest or read guest files. Stopped LXCs retain the normal
API-derived disk view.
@@ -0,0 +1,38 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { renderDocMarkdown } from '../docMarkdown';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..');
const anchor = 'proxmox-lxc-filesystem-readings-with-the-typed-helper';
const article = (name: string): HTMLElement => {
const element = document.createElement('article');
element.innerHTML = renderDocMarkdown(
readFileSync(path.join(root, 'frontend-modern/public/docs', `${name}.md`), 'utf8'), name,
);
return element;
};
describe('confined PVE helper reading guidance', () => {
it('links the deployment choice and support matrix to the same shipped section', () => {
const guide = article('AGENT_SECURITY');
expect(guide.querySelector(`#${anchor}`)?.textContent).toBe(
'Proxmox LXC filesystem readings with the typed helper',
);
expect(guide.querySelectorAll(`a[href="#${anchor}"]`).length).toBeGreaterThanOrEqual(3);
const row = [...guide.querySelectorAll('tr')].find(element =>
element.textContent?.includes('Proxmox node-local LXC filesystem telemetry'))!;
expect(row.textContent).toContain('command fallback works only outside the helper');
expect(row.querySelector(`a[href="#${anchor}"]`)).not.toBeNull();
});
it('keeps the confined-helper distinction reachable from Unified Agent', () => {
const guide = article('UNIFIED_AGENT');
const link = guide.querySelector(`a[href="/docs/AGENT_SECURITY#${anchor}"]`);
expect(link?.textContent).toBe('helper-backed LXC readings');
expect(link?.closest('p')?.textContent?.replace(/\s+/g, ' ')).toContain(
'those commands cannot recover readings inside the isolated typed helper',
);
});
});
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Static guide/source binding, not installed PVE or filesystem recovery."""
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
HEADING = '### Proxmox LXC filesystem readings with the typed helper\n'
ANCHOR = '#proxmox-lxc-filesystem-readings-with-the-typed-helper'
def plain(text):
return ' '.join(text.replace('**', '').replace('`', '').split())
class PVEHelperGuidanceTest(unittest.TestCase):
# A source-root override supports a sensitivity check of the real parent.
root = ROOT
def test_existing_guides_and_matrix_reach_one_consistent_boundary(self):
security = (self.root / 'docs/AGENT_SECURITY.md').read_text()
general = plain(security.split('## Proxmox Deployment Choices\n', 1)[1].split('\n## ', 1)[0])
self.assertNotIn("the safe profile uses the typed helper's bounded fixed-shape pct reads", general)
self.assertIn('Root or explicitly --grant-pct collectors can fall back', general)
self.assertIn('not usable inside the isolated typed helper', general)
self.assertGreaterEqual(security.count(ANCHOR), 3)
row = next(line for line in security.splitlines() if line.startswith('| Proxmox node-local LXC'))
self.assertIn('command fallback works only outside the helper', row)
self.assertNotIn('they remain the fallback whenever', row)
agent = (self.root / 'docs/UNIFIED_AGENT.md').read_text().split(
'### Inside-Guest Runtime Boundaries\n', 1)[1].split('\n## ', 1)[0]
self.assertIn('AGENT_SECURITY.md' + ANCHOR, agent)
self.assertIn('those commands cannot recover readings inside the isolated', plain(agent))
for name in ('AGENT_SECURITY.md', 'UNIFIED_AGENT.md'):
self.assertEqual((self.root / 'docs' / name).read_bytes(),
(self.root / 'frontend-modern/public/docs' / name).read_bytes())
def test_partial_and_unknown_readings_do_not_authorise_privilege_or_state_churn(self):
text = plain((self.root / 'docs/AGENT_SECURITY.md').read_text().split(HEADING, 1)[1].split('\n### ', 1)[0])
for distinction in ('without pct or lxc-info requests', 'cgroup v2 and /proc',
'fallback cannot recover', 'cgroup v1', 'degraded or unavailable',
'successful SMART reading does not establish LXC helper health',
"reading's status, age and collection path", 'helper versions separate',
'Older helper versions', 'without claiming it covers every mount',
'Do not remove PrivateNetwork', 'root/sudo fallback, reinstall or restart',
'full service/configuration records private', 'operation v2 preserves',
'omitted containers cannot refresh old filesystem readings',
'Operation v1 remains complete-only', 'explicitly rejects v2',
'not an empty healthy snapshot or zero disk usage',
'collector-only automatic update cannot replace its helper'):
with self.subTest(distinction=distinction):
self.assertIn(distinction, text)
self.assertNotIn('```', text, 'read existing evidence rather than run a probe')
def test_current_collector_has_the_described_closed_discovery_and_partial_path(self):
discover = (self.root / 'internal/hostagent/proxmox_lxc_running.go').read_text()
collect = (self.root / 'internal/hostagent/proxmox_lxc_filesystems.go').read_text()
tests = (self.root / 'internal/hostagent/proxmox_lxc_filesystems_test.go').read_text()
for symbol in ('discoverRunningProxmoxLXCContainers', 'cgroup.controllers', 'filesystemprobe.IsNamespaceInit'):
self.assertIn(symbol, discover)
for symbol in ('CollectProxmoxLXCFilesystemsLocalResult', 'ProxmoxLXCCollectionPartial',
'OmittedVMIDs', 'result.Degraded = true'):
self.assertIn(symbol, collect)
self.assertIn('TestDiscoverRunningProxmoxLXCContainersFindsInitWithoutSockets', tests)
self.assertIn('pct and lxc-info exist but every call fails', tests)
if __name__ == '__main__':
unittest.main()