Batch remote drift checks into agent file manifests

This commit is contained in:
2026-10-05 12:52:33 -04:00
parent 10ab071bce
commit f16879036e
11 changed files with 779 additions and 34 deletions
+31 -1
View File
@@ -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);
+1 -1
View File
@@ -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> {