build: enforce service filesystem boundary (#1190)

This commit is contained in:
Brad Groux 2026-08-23 15:23:14 -05:00 committed by GitHub
parent 2a581a451a
commit 542d6dd760
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 619 additions and 0 deletions

View file

@ -289,6 +289,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check service filesystem boundary
run: pnpm check:service-filesystem-boundary
- name: Build shared (dependency for typecheck)
run: pnpm --filter @veritas-kanban/shared build

View file

@ -78,6 +78,7 @@ pnpm check:delivery-cadence # Prevents verification and review policy drift
pnpm check:security-gates # Validates CodeQL/gitleaks workflow and exact suppressions
pnpm check:gitleaks # Scans reviewed tree and proves new-secret detection
pnpm check:vite-native-config # Loads web build and test configs with Vite's native loader
pnpm check:service-filesystem-boundary # Prevents new direct filesystem imports in services
pnpm test:ci-scope # Validates path-aware CI test selection
pnpm smoke:cli-mcp # CLI ↔ MCP compatibility smoke test
pnpm test:buzz:compatibility # Credential-free composed Buzz release gate

View file

@ -0,0 +1,30 @@
# Service Filesystem Boundary
Service modules must use the storage abstraction instead of importing Node's
filesystem APIs directly. The current exceptions are tracked in
[`service-filesystem-boundary.json`](service-filesystem-boundary.json) so the
existing migration debt is explicit without allowing it to spread.
Run the boundary gate with:
```bash
pnpm check:service-filesystem-boundary
```
The gate recursively scans `server/src/services/**/*.ts` and recognizes static
imports, dynamic imports, and `require()` calls for `fs`, `node:fs`, and their
`/promises` variants. Filesystem text embedded in strings and comments is
ignored. The command exits nonzero and names the file when it finds:
- a direct import without a classified inventory entry;
- an invalid category, owner, or rationale;
- a duplicate entry; or
- a stale entry after an import has been removed.
`maximumEntries` must equal the number of classified exceptions. Any increase
therefore requires a visible inventory and ratchet change in the same review.
The remaining #1163 child issues own the reductions: #1186 covers coordination
state, #1187 operational evidence, #1188 managed content, and #1189 final
process I/O plus removal of the last compatibility exceptions. Each migration
must delete its stale inventory entries and lower `maximumEntries` in the same
change.

View file

@ -0,0 +1,324 @@
{
"schemaVersion": 1,
"maximumEntries": 53,
"entries": [
{
"path": "server/src/services/activity-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/agent-health-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/agent-permission-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/attachment-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/audit-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/broadcast-storage-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/ceremony-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/chat-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/clawdbot-agent-service.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/codex-health-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/config-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/conflict-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/delegation-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/doc-freshness-service.ts",
"category": "packaged-readonly-content",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/docs-service.ts",
"category": "packaged-readonly-content",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/error-learning-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/external-tracker-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/file-lock.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/gateway-chat-client.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/github-sync-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/integrity-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/lifecycle-hooks-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/maintenance-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/managed-list-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/metrics/task-metrics.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/metrics/telemetry-reader.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/notification-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/outbound-integration-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/pdf-report-service.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/progress-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/prompt-registry-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/queue-intake-monitor-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/reflection-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/run-session-share-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/scheduled-deliverables-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/scheduler-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/search-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/shared-resources-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/skill-security-service.ts",
"category": "packaged-readonly-content",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/sqlite-portability-service.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/status-history-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/task-identity-diagnostics.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/task-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/telemetry-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/template-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/text-extraction-service.ts",
"category": "transient-process-io",
"owner": "#1189",
"rationale": "Remaining process I/O migration is tracked in issue #1189."
},
{
"path": "server/src/services/tool-policy-service.ts",
"category": "compatibility-debt",
"owner": "#1188",
"rationale": "Managed-content storage migration is tracked in issue #1188."
},
{
"path": "server/src/services/trace-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/transition-hooks-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/work-product-service.ts",
"category": "authoritative-persistence",
"owner": "#1187",
"rationale": "Operational evidence storage migration is tracked in issue #1187."
},
{
"path": "server/src/services/workflow-run-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/workflow-service.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
},
{
"path": "server/src/services/workflow-step-executor.ts",
"category": "authoritative-persistence",
"owner": "#1186",
"rationale": "Coordination-state storage migration is tracked in issue #1186."
}
]
}

View file

@ -41,6 +41,7 @@
"check:vite-native-config": "pnpm --filter @veritas-kanban/web exec vite build --configLoader native && vitest run --configLoader native web/src/lib/__tests__/client-policy.test.ts",
"check:pnpm-settings": "node scripts/check-pnpm-settings.mjs",
"check:security-artifacts": "node scripts/check-security-artifacts.mjs",
"check:service-filesystem-boundary": "node --test scripts/check-service-filesystem-boundary.test.mjs && node scripts/check-service-filesystem-boundary.mjs",
"typecheck": "pnpm --filter @veritas-kanban/shared build && pnpm -r typecheck",
"test": "pnpm test:unit",
"test:unit": "node --test scripts/run-workspace-unit-tests.test.mjs && node scripts/run-workspace-unit-tests.mjs",

View file

@ -0,0 +1,156 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
const ALLOWED_CATEGORIES = new Set([
'authoritative-persistence',
'packaged-readonly-content',
'transient-process-io',
'compatibility-debt',
]);
const FILESYSTEM_MODULE = /^(?:node:)?fs(?:\/promises)?$/;
export function findDirectFilesystemImports(source) {
const sourceFile = ts.createSourceFile(
'service.ts',
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
);
const imports = [];
const addImport = (moduleSpecifier) => {
if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) {
const module = moduleSpecifier.text;
if (FILESYSTEM_MODULE.test(module)) {
const line = sourceFile.getLineAndCharacterOfPosition(moduleSpecifier.getStart()).line + 1;
imports.push({ module, line });
}
}
};
const visit = (node) => {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
addImport(node.moduleSpecifier);
} else if (ts.isImportEqualsDeclaration(node)) {
if (ts.isExternalModuleReference(node.moduleReference)) {
addImport(node.moduleReference.expression);
}
} else if (ts.isCallExpression(node)) {
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require';
if (isDynamicImport || isRequire) addImport(node.arguments[0]);
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return imports;
}
function collectTypeScriptFiles(directory) {
if (!existsSync(directory)) return [];
const files = [];
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...collectTypeScriptFiles(absolutePath));
else if (entry.name.endsWith('.ts')) files.push(absolutePath);
}
return files.sort();
}
export function validateServiceFilesystemBoundary({ root, inventory }) {
const violations = [];
if (inventory.schemaVersion !== 1) violations.push('inventory schemaVersion must equal 1');
if (!Array.isArray(inventory.entries)) violations.push('inventory entries must be an array');
if (violations.length > 0) return violations;
if (inventory.maximumEntries !== inventory.entries.length) {
violations.push('maximumEntries must equal the classified entry count');
}
const classified = new Map();
for (const entry of inventory.entries) {
if (!entry || typeof entry.path !== 'string') {
violations.push('every inventory entry must have a path');
continue;
}
if (classified.has(entry.path)) violations.push(`${entry.path}: duplicate inventory entry`);
classified.set(entry.path, entry);
if (!ALLOWED_CATEGORIES.has(entry.category)) {
violations.push(`${entry.path}: invalid category ${JSON.stringify(entry.category)}`);
}
if (!/^#\d+$/.test(entry.owner ?? '')) {
violations.push(`${entry.path}: owner must be a GitHub issue reference`);
}
if (typeof entry.rationale !== 'string' || entry.rationale.trim().length < 20) {
violations.push(`${entry.path}: rationale must explain the temporary exception`);
}
}
const detected = new Set();
const serviceDirectory = path.join(root, 'server/src/services');
for (const absolutePath of collectTypeScriptFiles(serviceDirectory)) {
const relativePath = path.relative(root, absolutePath).replaceAll('\\', '/');
const imports = findDirectFilesystemImports(readFileSync(absolutePath, 'utf8'));
if (imports.length === 0) continue;
detected.add(relativePath);
if (!classified.has(relativePath)) {
violations.push(
`${relativePath}:${imports[0].line}: unclassified direct filesystem import (${imports.map((item) => item.module).join(', ')})`
);
}
}
for (const classifiedPath of classified.keys()) {
if (!detected.has(classifiedPath)) {
violations.push(`${classifiedPath}: stale inventory entry has no direct filesystem import`);
}
}
return violations.sort();
}
function parseArguments(argv) {
const options = {
root: process.cwd(),
inventory: 'docs/architecture/service-filesystem-boundary.json',
};
for (let index = 0; index < argv.length; index += 1) {
if (argv[index] === '--root') options.root = path.resolve(argv[++index]);
else if (argv[index] === '--inventory') options.inventory = argv[++index];
else throw new Error(`Unknown argument: ${argv[index]}`);
}
return options;
}
export function runServiceFilesystemBoundaryCheck(argv = process.argv.slice(2)) {
let options;
try {
options = parseArguments(argv);
const inventoryPath = path.resolve(options.root, options.inventory);
const inventory = JSON.parse(readFileSync(inventoryPath, 'utf8'));
const violations = validateServiceFilesystemBoundary({ root: options.root, inventory });
if (violations.length > 0) {
console.error('Service filesystem boundary check failed.');
for (const violation of violations) console.error(`- ${violation}`);
process.exitCode = 1;
return false;
}
console.log(
`Service filesystem boundary check passed (${inventory.entries.length} classified exceptions).`
);
return true;
} catch (error) {
console.error(`Service filesystem boundary check failed: ${error.message}`);
process.exitCode = 1;
return false;
}
}
function isDirectExecution() {
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
}
if (isDirectExecution()) runServiceFilesystemBoundaryCheck();

View file

@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { findDirectFilesystemImports } from './check-service-filesystem-boundary.mjs';
const CHECK_SCRIPT = path.resolve('scripts/check-service-filesystem-boundary.mjs');
function createFixture({ source, entries = [], servicePath = 'example-service.ts' }) {
const root = mkdtempSync(path.join(tmpdir(), 'veritas-service-fs-boundary-'));
const serviceDirectory = path.join(root, 'server/src/services');
const sourcePath = path.join(serviceDirectory, servicePath);
mkdirSync(path.dirname(sourcePath), { recursive: true });
writeFileSync(sourcePath, source);
writeFileSync(
path.join(root, 'service-filesystem-boundary.json'),
`${JSON.stringify({ schemaVersion: 1, maximumEntries: entries.length, entries }, null, 2)}\n`
);
return root;
}
function runCheck(root) {
return spawnSync(
process.execPath,
[CHECK_SCRIPT, '--root', root, '--inventory', 'service-filesystem-boundary.json'],
{ encoding: 'utf8' }
);
}
test('rejects a new service filesystem import with a file diagnostic', () => {
const root = createFixture({
source: "import { readFile } from 'node:fs/promises';\n",
});
const result = runCheck(root);
assert.equal(result.status, 1);
assert.match(result.stderr, /server\/src\/services\/example-service\.ts/);
assert.match(result.stderr, /unclassified direct filesystem import/);
});
test('scans nested service directories', () => {
const root = createFixture({
source: "const fs = require('fs');\n",
servicePath: 'nested/example-service.ts',
});
const result = runCheck(root);
assert.equal(result.status, 1);
assert.match(result.stderr, /server\/src\/services\/nested\/example-service\.ts/);
});
test('ignores filesystem calls embedded in strings and comments', () => {
const source = [
`const command = "require('node:fs').writeFileSync('/tmp/example', 'x')";`,
`// import fs from 'node:fs';`,
`/* require('fs/promises') */`,
].join('\n');
assert.deepEqual(findDirectFilesystemImports(source), []);
});
test('detects filesystem re-exports', () => {
assert.deepEqual(findDirectFilesystemImports("export { readFile } from 'node:fs';\n"), [
{ module: 'node:fs', line: 1 },
]);
});
test('accepts a classified direct filesystem import', () => {
const entry = {
path: 'server/src/services/example-service.ts',
category: 'compatibility-debt',
owner: '#1189',
rationale: 'Migration is tracked by the final storage boundary issue.',
};
const root = createFixture({
source: "const fs = await import('fs/promises');\n",
entries: [entry],
});
const result = runCheck(root);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /1 classified exception/);
});
test('rejects stale inventory entries so the exception count ratchets downward', () => {
const entry = {
path: 'server/src/services/example-service.ts',
category: 'compatibility-debt',
owner: '#1189',
rationale: 'Migration is tracked by the final storage boundary issue.',
};
const root = createFixture({ source: 'export const value = 1;\n', entries: [entry] });
const result = runCheck(root);
assert.equal(result.status, 1);
assert.match(result.stderr, /stale inventory entry/);
});