Batch remote drift checks into agent file manifests
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { relative, resolve, sep } from 'node:path';
|
||||
import { join, posix, relative, resolve, sep } from 'node:path';
|
||||
import { config } from '../config.js';
|
||||
import { nativeInstance, nativeRequest } from './native.js';
|
||||
import { getHost } from '../hosts/index.js';
|
||||
|
||||
function remote(path: string) {
|
||||
for (const root of [config.serversRoot, config.t6ServersRoot]) {
|
||||
@@ -16,6 +17,35 @@ function remote(path: string) {
|
||||
}
|
||||
function missing(path: string): never { throw Object.assign(new Error(`File does not exist: ${path}`), { code: 'ENOENT' }); }
|
||||
|
||||
export interface FileFingerprint { size: number; sha256: string }
|
||||
|
||||
/** One scoped agent scan; null preserves compatibility with older agents and local files. */
|
||||
export async function nativeFileManifest(root: string, directories: string[], ignoreNames: string[], managedPaths: string[]): Promise<Map<string, FileFingerprint> | null> {
|
||||
const target = remote(root);
|
||||
if (!target || target.path !== '.' || !getHost(target.instance.host_id)?.inventory.capabilities?.includes('files.manifest')) return null;
|
||||
const roots = [...new Set(directories.map(directory => relative(root, directory).split(sep).join('/')))];
|
||||
// Preserve legacy configurations that scan the entire instance root.
|
||||
if (roots.some(directory => !directory)) return null;
|
||||
const paths = [...new Set(managedPaths.map(path => relative(root, path).split(sep).join('/')))];
|
||||
const result = await nativeRequest<{ files: Array<FileFingerprint & { path: string }> }>(target.instance, 'files.manifest', {
|
||||
roots, paths, ignoreNames: ignoreNames.filter(name => !paths.some(path => path.split('/').includes(name)))
|
||||
});
|
||||
if (!result || !Array.isArray(result.files) || result.files.length > 10_000) throw new Error('Agent returned an invalid file manifest.');
|
||||
const manifest = new Map<string, FileFingerprint>();
|
||||
for (const entry of result.files) {
|
||||
if (!entry || typeof entry.path !== 'string' || !entry.path || entry.path.includes('\\') || entry.path.includes('\0')
|
||||
|| posix.normalize(entry.path) !== entry.path || entry.path.split('/').some(part => part === '..' || part === '.')
|
||||
|| !roots.some(directory => entry.path.startsWith(`${directory}/`))
|
||||
|| !Number.isSafeInteger(entry.size) || entry.size < 0 || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
|
||||
throw new Error('Agent returned an invalid file manifest entry.');
|
||||
}
|
||||
const path = join(root, entry.path);
|
||||
if (manifest.has(path)) throw new Error('Agent returned duplicate file manifest entries.');
|
||||
manifest.set(path, { size: entry.size, sha256: entry.sha256 });
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export async function readFile(path: string): Promise<Buffer> {
|
||||
const target = remote(path);
|
||||
if (!target) return fs.readFile(path);
|
||||
|
||||
@@ -31,7 +31,7 @@ export function unplacedNativeCreation(server: string): string | null {
|
||||
}
|
||||
|
||||
const observations = new Map<string, Promise<unknown>>();
|
||||
const readKinds = new Set(['instance.inspect', 'files.read', 'files.stat', 'files.list', 'logs.read']);
|
||||
const readKinds = new Set(['instance.inspect', 'files.read', 'files.stat', 'files.list', 'files.manifest', 'logs.read']);
|
||||
|
||||
/** Share overlapping observations only, never completed results or mutations. */
|
||||
export async function nativeRequest<T = Record<string, unknown>>(instance: NativeInstance, kind: string, payload: Record<string, unknown> = {}, key?: string): Promise<T> {
|
||||
|
||||
Reference in New Issue
Block a user