Compare commits

..
Author SHA1 Message Date
code-be-02 2451cb4f98 fix(git-tools): sanitize WP5b for the public-package gates (review identity scrub + tool index)
ci/woodpecker/pr/ci Pipeline was successful
verify-sanitized denylist: an operator-identity token in a ci-queue-wait
comment (spec finding references genericized - the framework-PR
firewall applies to shipped files). check-tools-index: repo-decl.sh
shipped means documented in the resident index in the SAME commit, and
executable. Full sanitization CI step replayed green locally
(verify-sanitized, resident budget x2, enumeration guard, tools index
x2).
2026-08-25 07:34:44 -05:00
code-be-02 87c08d25d4 feat(git-tools): consume .mosaic/repo.json declarations in compat mode (T51 WP5b, closes #1413)
Spec of record: docs/plans/2026-08-23_repo-structure-declaration.md
(brain repo) sections 4 (consumption contract), 5.1/5.3/5.4, 1.2a.

New shared lib repo-decl.sh: one consumption surface for the wrappers.
Loads .mosaic/repo.json, classifies absent/invalid/valid via the WP1
validator (5.1: ALL consumers invoke the same script; 5.4 point 1:
invalid = ABSENT + loud error naming file/key/reason), extracts the
consumed fields, normalizes origin for 5.3 comparisons, validates
transitions per 4.2 (a CLI flag is input, not authority), and resolves
host:/ paths FAIL-CLOSED while MOSAIC_HOST_ROOT is unset (1.2a — no
WP5b consumer resolves a path today; the helper exists so the first
that needs one cannot guess). v1 declarations validate but carry no
consumable fields: legacy behavior with a note.

pr-create.sh: base precedence -B (validated as an allowed transition)
-> declared integration_trunk -> legacy WP5a forge-default floor
(unmanaged/absent/v1 per 4.3 reversible class, warn + legacy). Remote
mismatch vs canonical_remote refuses (write path, 5.3).

pr-merge.sh: transition validation per declared flow; the hardcoded
main/next target check survives only for undeclared repos during the
rollout window (4.3 irreversible class, loud warning). Remote mismatch
refuses.

ci-queue-wait.sh: ROUTE CONTEXT only (4.1, C4/jarvis F8/DR2 R9) —
branch-selection semantics untouched, absence silent, invalid reported
per 5.4.

mosaic-worktree.sh: staged rule 4.4 — invalid declaration fails
branch-creation loud, absent warns and proceeds, valid contributes
policy ADVICE only (4.5: placement stays derived; the advisory
worktree_root comparison runs only when MOSAIC_HOST_ROOT is set, per
1.2a warn-and-omit). Consuming via a self-located source line and
set -u-safe env access.

mutate-push-guard.sh: NO change — spec 4.1 names it for push-to-trunk
protection, but the tool as shipped is a mutation-coverage meta-tool
for push-guard.sh with no trunk-protection logic to consult; the
disposition is documented in #1413 rather than force-feeding a fake
consumption.

All wrappers degrade SILENTLY to legacy behavior when repo-decl.sh is
absent from a copied tool subset (a legal deployment shape; a note
there broke single-line diagnostic contracts in
test-pr-merge-message-field).

test-repo-decl-consumption.sh: 67 assertions, green x2, hermetic; runs
RED against pre-change tools via WP5B_TOOLS (49 red there — red-first
evidence). Covers every 5.4 hostile-input class applicable to consumed
fields (missing, malformed, unknown schema_version, v1, unknown key,
bad refs, cross-field, userinfo URL, remote mismatch) plus transition
validation, base precedence, absence policies, staged worktree rule,
route context, and 1.2a fail-closed. Enumerated on the S1 surface
(enumeration guard green: population 71, enumerated 57).

Neighbor suites green: WP5a fallback suite, all six pr-merge suites,
worktree large-repo, help/login/interactive suites. S1 chain failures
(fleet-units systemd bus, invariant_r host Pi version, pr-edit
credential-helper env) reproduce identically at origin/next —
environmental, untouched by this diff. No TS/vitest lane touched
(shell tools only).
2026-08-25 07:34:44 -05:00
3 changed files with 0 additions and 204 deletions
-6
View File
@@ -14,16 +14,10 @@ import { mountMcpHandler } from './mcp/mcp.controller.js';
import { McpService } from './mcp/mcp.service.js';
import { detectAndAssertTier, TierDetectionError } from '@mosaicstack/storage';
import { resolveGatewayConfigPath } from './env.js';
import { assertValidationPipeSeesDtoDecorators } from './validation-pipe-check.js';
async function bootstrap(): Promise<void> {
const logger = new Logger('Bootstrap');
// Fail loud BEFORE anything else if the global ValidationPipe cannot see
// the guarded DTOs' decorated properties (#1391): a broken metatype turns
// every request body into a 400 at first use; this surfaces it at boot.
assertValidationPipeSeesDtoDecorators();
if (!process.env['BETTER_AUTH_SECRET']) {
throw new Error('BETTER_AUTH_SECRET is required');
}
@@ -1,104 +0,0 @@
/**
* Boot-time ValidationPipe metatype self-check (#1391).
*
* The check exists to fail loud at boot when the global pipe cannot see a
* guarded DTO's decorated properties — the #436 class-erasure signature and
* its dependency-graph cousins. Red/green arms:
*
* GREEN real module state: BootstrapSetupDto's three properties are
* decorated and visible through the globalThis-shared storage.
* RED a control class with NO decorators (the erasure shape): the
* check throws PipeMetatypeCheckError naming every property.
* RED-2 a control where one property is decorated and two are not: the
* error names exactly the missing two — the miss list is precise,
* not a blanket failure.
*/
import { describe, expect, it } from 'vitest';
import { IsString } from 'class-validator';
import {
assertValidationPipeSeesDtoDecorators,
PipeMetatypeCheckError,
} from './validation-pipe-check.js';
describe('assertValidationPipeSeesDtoDecorators (#1391 boot check)', () => {
it('GREEN: passes on real module state (decorated DTO visible to the pipe)', () => {
expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow();
});
it('RED control: a class whose properties lost their decorators throws, naming them', async () => {
// Simulate metatype erasure: an undecorated class standing where a
// decorated DTO should be. Redefine the guard table for the test by
// importing the module and pointing its table at the eroded class —
// the check reads the table at call time, so a fresh module instance
// with a swapped table reproduces the boot failure deterministically.
const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js');
class ErodedDto {
name?: string;
email?: string;
password?: string;
}
const original = PIPE_GUARDED_DTOS[0];
expect(original).toBeDefined();
// Swap in the eroded target (same declared properties, zero decorators).
(
PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }>
).splice(0, PIPE_GUARDED_DTOS.length, {
name: 'ErodedDto',
target: ErodedDto,
properties: ['name', 'email', 'password'],
});
try {
expect(() => assertValidationPipeSeesDtoDecorators()).toThrow(PipeMetatypeCheckError);
try {
assertValidationPipeSeesDtoDecorators();
} catch (err) {
const message = err instanceof Error ? err.message : '';
expect(message).toContain('ErodedDto.name');
expect(message).toContain('ErodedDto.email');
expect(message).toContain('ErodedDto.password');
}
} finally {
// Restore real module state for any later test in this file.
(PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original);
}
// And confirm the restore is real.
expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow();
});
it('RED-2 control: a partially decorated class names exactly the missing properties', async () => {
const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js');
class HalfErodedDto {
@IsString()
name?: string;
email?: string;
password?: string;
}
const original = PIPE_GUARDED_DTOS[0];
(
PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }>
).splice(0, PIPE_GUARDED_DTOS.length, {
name: 'HalfErodedDto',
target: HalfErodedDto,
properties: ['name', 'email', 'password'],
});
try {
try {
assertValidationPipeSeesDtoDecorators();
expect.unreachable('partially decorated DTO must fail the boot check');
} catch (err) {
const message = err instanceof Error ? err.message : '';
expect(message).toContain('HalfErodedDto.email');
expect(message).toContain('HalfErodedDto.password');
expect(message).not.toContain('HalfErodedDto.name has no');
}
} finally {
(PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original);
}
});
});
-94
View File
@@ -1,94 +0,0 @@
import 'reflect-metadata';
import { getMetadataStorage } from 'class-validator';
import { BootstrapSetupDto } from './admin/bootstrap.dto.js';
/**
* Boot-time self-check: the global ValidationPipe must be able to SEE the
* decorated properties of the DTOs it guards (#1391, #436 class).
*
* WHY THIS EXISTS. When Nest resolves a @Body() metatype to Object — via
* `import type` class erasure (#436), or a dependency graph where the
* controller's decorators and the application's route enhancers disagree
* (#1391's hypothesized dual-@nestjs/common on a mixed install) — the
* ValidationPipe's whitelist treats every property as forbidden. The first
* symptom is a 400 on the FIRST bootstrap attempt of a fresh install, the
* worst place to discover wiring damage: the operator cannot tell a broken
* payload from a broken daemon.
*
* This check fails LOUD at boot instead: if the pipe cannot see the DTO's
* decorated properties, the gateway refuses to start with a named cause.
* It catches the whole class — erasure, decorator metadata loss — on every
* host, at the moment the damage exists rather than at first use.
*
* Storage sharing note: class-validator keys its metadata storage on
* globalThis, so duplicate package copies do NOT hide metadata (measured,
* #1391 diagnosis). What hides it is losing the metatype itself, which is
* what this asserts against.
*/
/**
* DTOs the global pipe guards, mapped to the properties the whitelist must
* admit. Target is the CONSTRUCTOR (the object class itself): class-validator
* decorators register metadata keyed on the constructor, and its executor
* looks up `object.constructor` (ValidationExecutor.js:50) — the probe
* through `prototype` returns zero. Extend when adding DTOs to the app.
*/
export const PIPE_GUARDED_DTOS: Array<{
name: string;
target: abstract new (...args: never[]) => unknown;
properties: string[];
}> = [
{
name: 'BootstrapSetupDto',
target: BootstrapSetupDto,
properties: ['name', 'email', 'password'],
},
];
export class PipeMetatypeCheckError extends Error {
constructor(missing: string[]) {
super(
'ValidationPipe metatype check failed: ' +
missing.join('; ') +
'. The global ValidationPipe cannot see decorated DTO properties — ' +
'every request body would be rejected as non-whitelisted. ' +
'Check for import-type erasure or decorator metadata loss in the ' +
'dependency graph (see issues #436, #1391).',
);
this.name = 'PipeMetatypeCheckError';
}
}
/**
* Assert the pipe's whitelist can see every guarded DTO's decorated
* properties. Throws PipeMetatypeCheckError (fail-loud at boot) listing
* each miss. Pure function of module state: no I/O, safe to call twice.
*/
export function assertValidationPipeSeesDtoDecorators(): void {
const storage = getMetadataStorage();
const missing: string[] = [];
for (const dto of PIPE_GUARDED_DTOS) {
// class-validator records constraints keyed on the DTO's constructor
// (decorators run on the class), and its executor resolves them via
// object.constructor. A property with no recorded metadata is invisible
// to the whitelist — whatever the cause — and fails here.
// Signature mirrors ValidationExecutor.js:50 — (constructor, schema, always,
// strictGroups, groups?). No schema, always=true, no groups: every
// constraint regardless of grouping, which is what the whitelist sees.
const metadatas = storage.getTargetValidationMetadatas(dto.target, '', true, false);
const decorated = new Set(metadatas.map((m) => m.propertyName));
for (const property of dto.properties) {
if (!decorated.has(property)) {
missing.push(
`${dto.name}.${property} has no class-validator constraints visible to the pipe`,
);
}
}
}
if (missing.length > 0) {
throw new PipeMetatypeCheckError(missing);
}
}