From acd56038781ce3e785aabb2e09597bd4cc803051 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:22:43 +0100 Subject: [PATCH 1/3] Keep confined PVE filesystem help consistent with its collector The deployment table still calls the typed helper a pct reader, while the implementation uses socket-free discovery and command fallback cannot recover within PrivateNetwork. Link the deployment choice, collector help and support matrix to one shipped explanation of partial, unavailable and older-helper readings, without recommending privilege or state changes. Contract-Neutral: Guidance and consuming-proof coverage only; no telemetry, privilege, API, installer or availability contract delta. Change-source: pulse-maintainer --- docs/AGENT_SECURITY.md | 63 +++++---- docs/UNIFIED_AGENT.md | 12 +- .../browser-tests/build-doc-command-help.mjs | 2 +- .../browser-tests/pve-helper-guidance.cjs | 123 ++++++++++++++++++ frontend-modern/public/docs/AGENT_SECURITY.md | 63 +++++---- frontend-modern/public/docs/UNIFIED_AGENT.md | 12 +- .../docs/__tests__/pveHelperGuidance.test.ts | 38 ++++++ scripts/tests/test_pve_helper_guidance.py | 69 ++++++++++ 8 files changed, 327 insertions(+), 55 deletions(-) create mode 100644 frontend-modern/browser-tests/pve-helper-guidance.cjs create mode 100644 frontend-modern/src/features/docs/__tests__/pveHelperGuidance.test.ts create mode 100644 scripts/tests/test_pve_helper_guidance.py diff --git a/docs/AGENT_SECURITY.md b/docs/AGENT_SECURITY.md index e82c6b9970..57280d15b5 100644 --- a/docs/AGENT_SECURITY.md +++ b/docs/AGENT_SECURITY.md @@ -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 | +| All mounted 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//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 ` for a guest only -when the statfs probe is unavailable or fails. It reports +statfs through `/proc//root`. Root/full-trust collectors can fall back to +`pct list`, `lxc-info` and `pct df ` 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 full-trust 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 | diff --git a/docs/UNIFIED_AGENT.md b/docs/UNIFIED_AGENT.md index 391ecce817..32c0fe93ad 100644 --- a/docs/UNIFIED_AGENT.md +++ b/docs/UNIFIED_AGENT.md @@ -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 ` 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/full-trust collector can fall back to `pct list`, `lxc-info` and +`pct df `; 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. diff --git a/frontend-modern/browser-tests/build-doc-command-help.mjs b/frontend-modern/browser-tests/build-doc-command-help.mjs index 1214bc925e..c85a6149f4 100644 --- a/frontend-modern/browser-tests/build-doc-command-help.mjs +++ b/frontend-modern/browser-tests/build-doc-command-help.mjs @@ -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`)); diff --git a/frontend-modern/browser-tests/pve-helper-guidance.cjs b/frontend-modern/browser-tests/pve-helper-guidance.cjs new file mode 100644 index 0000000000..a20bd32012 --- /dev/null +++ b/frontend-modern/browser-tests/pve-helper-guidance.cjs @@ -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; }); diff --git a/frontend-modern/public/docs/AGENT_SECURITY.md b/frontend-modern/public/docs/AGENT_SECURITY.md index e82c6b9970..57280d15b5 100644 --- a/frontend-modern/public/docs/AGENT_SECURITY.md +++ b/frontend-modern/public/docs/AGENT_SECURITY.md @@ -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 | +| All mounted 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//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 ` for a guest only -when the statfs probe is unavailable or fails. It reports +statfs through `/proc//root`. Root/full-trust collectors can fall back to +`pct list`, `lxc-info` and `pct df ` 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 full-trust 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 | diff --git a/frontend-modern/public/docs/UNIFIED_AGENT.md b/frontend-modern/public/docs/UNIFIED_AGENT.md index 391ecce817..32c0fe93ad 100644 --- a/frontend-modern/public/docs/UNIFIED_AGENT.md +++ b/frontend-modern/public/docs/UNIFIED_AGENT.md @@ -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 ` 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/full-trust collector can fall back to `pct list`, `lxc-info` and +`pct df `; 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. diff --git a/frontend-modern/src/features/docs/__tests__/pveHelperGuidance.test.ts b/frontend-modern/src/features/docs/__tests__/pveHelperGuidance.test.ts new file mode 100644 index 0000000000..b78f94fff0 --- /dev/null +++ b/frontend-modern/src/features/docs/__tests__/pveHelperGuidance.test.ts @@ -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', + ); + }); +}); diff --git a/scripts/tests/test_pve_helper_guidance.py b/scripts/tests/test_pve_helper_guidance.py new file mode 100644 index 0000000000..964ed416c3 --- /dev/null +++ b/scripts/tests/test_pve_helper_guidance.py @@ -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/full-trust 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() From 399618939d3e7d1711b24ecbd43ba9129fed2d56 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:27:36 +0100 Subject: [PATCH 2/3] Name scoped legacy PCT access without implying root is required The legacy grant-pct path deliberately permits only list and df outside the typed helper. Keep that distinction instead of calling every fallback a full-trust collector, and describe configured mounts rather than promising every mounted filesystem. Contract-Neutral: Guidance wording only; no telemetry, privilege, API, installer or availability contract delta. Change-source: pulse-maintainer --- docs/AGENT_SECURITY.md | 8 ++++---- docs/UNIFIED_AGENT.md | 4 ++-- frontend-modern/public/docs/AGENT_SECURITY.md | 8 ++++---- frontend-modern/public/docs/UNIFIED_AGENT.md | 4 ++-- scripts/tests/test_pve_helper_guidance.py | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/AGENT_SECURITY.md b/docs/AGENT_SECURITY.md index 57280d15b5..93a2924702 100644 --- a/docs/AGENT_SECURITY.md +++ b/docs/AGENT_SECURITY.md @@ -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 [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 | +| 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,8 +114,8 @@ 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//root`. Root/full-trust collectors can fall back to -`pct list`, `lxc-info` and `pct df ` when discovery or a mount probe fails. +statfs through `/proc//root`. Root or explicitly `--grant-pct` collectors can fall back to +`pct list` and `pct df ` (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 @@ -324,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'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 full-trust 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 | +| 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 | diff --git a/docs/UNIFIED_AGENT.md b/docs/UNIFIED_AGENT.md index 32c0fe93ad..517a0d119e 100644 --- a/docs/UNIFIED_AGENT.md +++ b/docs/UNIFIED_AGENT.md @@ -693,8 +693,8 @@ 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. The current collector first uses socket-free cgroup v2 and `/proc` discovery plus bounded mount probes. A -root/full-trust collector can fall back to `pct list`, `lxc-info` and -`pct df `; those commands cannot recover readings inside the isolated +root or explicitly `--grant-pct` collector can fall back to `pct list` and +`pct df ` (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 diff --git a/frontend-modern/public/docs/AGENT_SECURITY.md b/frontend-modern/public/docs/AGENT_SECURITY.md index 57280d15b5..93a2924702 100644 --- a/frontend-modern/public/docs/AGENT_SECURITY.md +++ b/frontend-modern/public/docs/AGENT_SECURITY.md @@ -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 [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 | +| 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,8 +114,8 @@ 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//root`. Root/full-trust collectors can fall back to -`pct list`, `lxc-info` and `pct df ` when discovery or a mount probe fails. +statfs through `/proc//root`. Root or explicitly `--grant-pct` collectors can fall back to +`pct list` and `pct df ` (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 @@ -324,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'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 full-trust 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 | +| 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 | diff --git a/frontend-modern/public/docs/UNIFIED_AGENT.md b/frontend-modern/public/docs/UNIFIED_AGENT.md index 32c0fe93ad..517a0d119e 100644 --- a/frontend-modern/public/docs/UNIFIED_AGENT.md +++ b/frontend-modern/public/docs/UNIFIED_AGENT.md @@ -693,8 +693,8 @@ 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. The current collector first uses socket-free cgroup v2 and `/proc` discovery plus bounded mount probes. A -root/full-trust collector can fall back to `pct list`, `lxc-info` and -`pct df `; those commands cannot recover readings inside the isolated +root or explicitly `--grant-pct` collector can fall back to `pct list` and +`pct df ` (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 diff --git a/scripts/tests/test_pve_helper_guidance.py b/scripts/tests/test_pve_helper_guidance.py index 964ed416c3..abe4721dd1 100644 --- a/scripts/tests/test_pve_helper_guidance.py +++ b/scripts/tests/test_pve_helper_guidance.py @@ -21,7 +21,7 @@ class PVEHelperGuidanceTest(unittest.TestCase): 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/full-trust collectors can fall back', 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')) From c708959f8e99e0b99e69f6b534bc49fb41a5a5c9 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:40:39 +0100 Subject: [PATCH 3/3] Record source-bound consuming proof for confined-helper help The actual Docs fetch, Markdown renderer and router pass desktop Chromium and 390px WebKit navigation and reading checks. All six original captures were inspected with the native image tool; no host, agent or installed recovery is asserted. Contract-Neutral: Browser receipt only; no runtime, guide content or subsystem contract delta. Change-source: pulse-maintainer --- .../pve-helper-guidance/browser-result.json | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/qualification/pve-helper-guidance/browser-result.json diff --git a/docs/qualification/pve-helper-guidance/browser-result.json b/docs/qualification/pve-helper-guidance/browser-result.json new file mode 100644 index 0000000000..7518f114a8 --- /dev/null +++ b/docs/qualification/pve-helper-guidance/browser-result.json @@ -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." +}