Compare commits

..
Author SHA1 Message Date
fargo 0eae02fdf8 docs(ri-050): fix the TASKS table (10 columns restored) and add the pipeline-timing caveat (#1315 review)
ci/woodpecker/pr/ci Pipeline was successful
rev-code-02 REQUEST_CHANGES (review 230):

- BLOCKER: the truth-sync edit appended an 11th cell to three rows and an
  11th separator group; GFM then renders no table at all. Fixed: content
  folded into the existing notes cells, separator back to 10 groups, all
  rows verified 10 columns.
- SHOULD FIX: every cited pipeline (incl. headline 2517) finished by
  00:34Z, before the registry pin landed at 23:42:05Z. The pack now says
  so itself: the A/B proves the credential value at identical commits and
  nothing about the later pin; no cited pipeline exercises post-pin state.
2026-08-20 13:11:23 -05:00
fargo b3a1199fff style(ri-050): TASKS.md formatting with repo-pinned prettier 3.8.1 (#1275) 2026-08-20 13:11:06 -05:00
fargo 9b6b0fa2a8 style(ri-050): prettier formatting for evidence pack tables (#1275) 2026-08-20 13:11:06 -05:00
fargo f609abc9e2 docs(ri-050): release evidence pack — RI-050 floor evidenced, registry credential proven (pipeline 2517) (#1275) 2026-08-20 13:11:06 -05:00
fredandgate-merge-01 4f22a58041 guides: two measurement rules about pinned tool versions (#1316)
ci/woodpecker/push/publish Pipeline was successful
Co-authored-by: fred <[email protected]>
2026-08-20 16:40:58 +00:00
Ghostgate-merge-01Ghost <>
9b6869fab7 fix(#1179): require security authority wiring (#1189)
ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: Ghost <>
2026-08-20 16:19:45 +00:00
21 changed files with 613 additions and 525 deletions
@@ -0,0 +1,332 @@
import { type Type } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import type { SlashCommandPayload } from '@mosaicstack/types';
import { describe, expect, it, vi } from 'vitest';
import { AgentService, type AgentSession } from '../agent/agent.service.js';
import { ProviderService } from '../agent/provider.service.js';
import { AppModule } from '../app.module.js';
import { CommandAuthorizationService } from '../commands/command-authorization.service.js';
import { CommandExecutorService } from '../commands/command-executor.service.js';
import { CommandsModule } from '../commands/commands.module.js';
import { CommandRuntimeApprovalVerifier } from '../commands/runtime-approval-verifier.js';
import { PreferencesModule } from '../preferences/preferences.module.js';
import { SystemOverrideService } from '../preferences/system-override.service.js';
const fakeDb = {
$client: { exec: async (): Promise<void> => {} },
execute: async (): Promise<{ rows: unknown[] }> => ({ rows: [] }),
select: () => ({
from: () => ({
where: async (): Promise<Array<{ count: number }>> => [{ count: 1 }],
}),
}),
insert: () => ({ values: async (): Promise<void> => {} }),
};
const fakeProviderService = {
onModuleInit: async (): Promise<void> => {},
onModuleDestroy: (): void => {},
getRegistry: () => ({ getAvailable: () => [], getAll: () => [], find: () => undefined }),
getDefaultModel: () => undefined,
listAvailableModels: () => [],
listProviders: () => [],
getAdapter: () => undefined,
getProvidersHealth: () => [],
};
function compileRealAppGraph(): Promise<TestingModule> {
return Test.createTestingModule({ imports: [AppModule] })
.overrideProvider('DB_HANDLE')
.useValue({ db: fakeDb, close: async (): Promise<void> => {} })
.overrideProvider('DB')
.useValue(fakeDb)
.overrideProvider('STORAGE_ADAPTER')
.useValue({
name: 'required-security-wiring-test',
migrate: async (): Promise<void> => {},
close: async (): Promise<void> => {},
})
.overrideProvider('AUTH')
.useValue({})
.overrideProvider('BRAIN')
.useValue({ conversations: {}, agents: {} })
.overrideProvider('LOG_SERVICE')
.useValue({})
.overrideProvider('MEMORY')
.useValue({})
.overrideProvider('MEMORY_ADAPTER')
.useValue({})
.overrideProvider(ProviderService)
.useValue(fakeProviderService)
.compile();
}
function providerToken(provider: unknown): unknown {
return typeof provider === 'function' ? provider : (provider as { provide?: unknown })?.provide;
}
interface MaskingConsumer {
moduleType: Type<unknown>;
token: Type<unknown>;
useValue: object;
}
async function compileWithoutProvider(
moduleType: Type<unknown>,
missingToken: Type<unknown>,
maskingConsumer: MaskingConsumer,
): Promise<{ error: unknown; moduleRef: TestingModule | undefined }> {
const touchedModules = new Set([moduleType, maskingConsumer.moduleType]);
const originals = Array.from(touchedModules, (touchedModule: Type<unknown>) => ({
moduleType: touchedModule,
providers: (Reflect.getMetadata('providers', touchedModule) ?? []) as unknown[],
exports: (Reflect.getMetadata('exports', touchedModule) ?? []) as unknown[],
}));
for (const original of originals) {
const providers = original.providers.flatMap((provider: unknown): unknown[] => {
const token = providerToken(provider);
if (original.moduleType === moduleType && token === missingToken) return [];
if (original.moduleType === maskingConsumer.moduleType && token === maskingConsumer.token) {
return [{ provide: maskingConsumer.token, useValue: maskingConsumer.useValue }];
}
return [provider];
});
const exports = original.exports.filter(
(exported: unknown): boolean =>
original.moduleType !== moduleType || providerToken(exported) !== missingToken,
);
Reflect.defineMetadata('providers', providers, original.moduleType);
Reflect.defineMetadata('exports', exports, original.moduleType);
}
let moduleRef: TestingModule | undefined;
let error: unknown;
try {
moduleRef = await compileRealAppGraph();
} catch (caught: unknown) {
error = caught;
} finally {
for (const original of originals) {
Reflect.defineMetadata('providers', original.providers, original.moduleType);
Reflect.defineMetadata('exports', original.exports, original.moduleType);
}
}
return { error, moduleRef };
}
async function closeIfCompiled(moduleRef: TestingModule | undefined): Promise<void> {
if (moduleRef) await moduleRef.close();
}
describe('required security wiring — real AppModule startup refusal', () => {
it('FL-01 positive control: the real graph compiles when CommandAuthorizationService is bound', async () => {
const moduleRef = await compileRealAppGraph();
try {
expect(moduleRef.get(CommandAuthorizationService, { strict: false })).toBeInstanceOf(
CommandAuthorizationService,
);
} finally {
await moduleRef.close();
}
});
it('FL-01 negative control: absence read as permission is refused at module compilation', async () => {
const { error, moduleRef } = await compileWithoutProvider(
CommandsModule,
CommandAuthorizationService,
{
moduleType: CommandsModule,
token: CommandRuntimeApprovalVerifier,
useValue: {},
},
);
await closeIfCompiled(moduleRef);
expect(
error,
'absence read as permission: AppModule compilation accepted a missing CommandAuthorizationService binding',
).toBeInstanceOf(Error);
if (!(error instanceof Error)) return;
expect(error.message).toContain('CommandExecutorService');
expect(error.message).toContain('CommandAuthorizationService');
});
it('FL-11 positive control: the real graph compiles when SystemOverrideService is bound', async () => {
const moduleRef = await compileRealAppGraph();
try {
expect(moduleRef.get(SystemOverrideService, { strict: false })).toBeInstanceOf(
SystemOverrideService,
);
} finally {
await moduleRef.close();
}
});
it('FL-11 negative control: absence read as permission is refused at module compilation', async () => {
const { error, moduleRef } = await compileWithoutProvider(
PreferencesModule,
SystemOverrideService,
{
moduleType: CommandsModule,
token: CommandExecutorService,
useValue: {},
},
);
await closeIfCompiled(moduleRef);
expect(
error,
'absence read as permission: AppModule compilation accepted a missing SystemOverrideService binding',
).toBeInstanceOf(Error);
if (!(error instanceof Error)) return;
expect(error.message).toContain('AgentService');
expect(error.message).toContain('SystemOverrideService');
});
});
const actorScope = { userId: 'security-user', tenantId: 'security-tenant' };
const conversationId = 'security-conversation';
function directExecutorWithoutAuthorization(systemOverrideSet: ReturnType<typeof vi.fn>) {
const registry = {
getManifest: vi.fn(() => ({
version: 1,
commands: [
{
name: 'system',
aliases: [],
description: 'Set instruction authority',
scope: 'agent' as const,
execution: 'socket' as const,
available: true,
},
],
skills: [],
})),
};
return new CommandExecutorService(
registry as never,
{ getSession: vi.fn() } as never,
{ set: systemOverrideSet, clear: vi.fn() } as never,
{ collect: vi.fn() } as never,
null,
{ agents: {} } as never,
null,
null,
{ getServerStatuses: vi.fn(() => []), getToolDefinitions: vi.fn(() => []) } as never,
undefined as never,
);
}
function directAgentWithoutSystemOverride(piPrompt: ReturnType<typeof vi.fn>): {
service: AgentService;
session: AgentSession;
} {
const service = new AgentService(
{
getDefaultModel: vi.fn(() => null),
getRegistry: vi.fn(() => ({})),
findModel: vi.fn(),
listAvailableModels: vi.fn(() => []),
} as never,
{} as never,
{} as never,
{ available: false } as never,
{} as never,
{ getToolDefinitions: vi.fn(() => []) } as never,
{ loadForSession: vi.fn(async () => ({ metaTools: [], promptAdditions: [] })) } as never,
undefined as never,
null,
{ collect: vi.fn().mockResolvedValue(undefined) } as never,
null,
);
const session = {
id: conversationId,
provider: 'test-provider',
modelId: 'test-model',
piSession: { prompt: piPrompt },
listeners: new Set(),
unsubscribe: vi.fn(),
createdAt: Date.now(),
promptCount: 0,
channels: new Set(),
skillPromptAdditions: [],
sandboxDir: process.cwd(),
allowedTools: null,
userId: actorScope.userId,
tenantId: actorScope.tenantId,
metrics: {
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
modelSwitches: 0,
messageCount: 0,
lastActivityAt: new Date(0).toISOString(),
},
} as unknown as AgentSession;
const internals = service as unknown as { sessions: Map<string, AgentSession> };
internals.sessions.set(conversationId, session);
return { service, session };
}
describe('required security wiring — malformed direct absence has zero effects', () => {
it('FL-01 refuses command execution before any command effect when authorization is absent', async () => {
const systemOverrideSet = vi.fn().mockResolvedValue(undefined);
const executor = directExecutorWithoutAuthorization(systemOverrideSet);
const payload: SlashCommandPayload = {
command: 'system',
args: 'authority that must not be stored',
conversationId,
};
let error: unknown;
try {
await executor.execute(payload, actorScope);
} catch (caught: unknown) {
error = caught;
}
expect
.soft(
error,
'absence read as permission: direct executor accepted missing command authorization',
)
.toBeInstanceOf(Error);
expect
.soft(
systemOverrideSet,
'absence read as permission: command effect occurred without command authorization',
)
.not.toHaveBeenCalled();
});
it('FL-11 refuses prompt execution before any provider or session effect when system override authority is absent', async () => {
const piPrompt = vi.fn().mockResolvedValue(undefined);
const { service, session } = directAgentWithoutSystemOverride(piPrompt);
let error: unknown;
try {
await service.prompt(conversationId, 'must not reach provider', actorScope);
} catch (caught: unknown) {
error = caught;
}
expect
.soft(
error,
'absence read as permission: direct session accepted missing system override authority',
)
.toBeInstanceOf(Error);
expect
.soft(
piPrompt,
'absence read as permission: provider prompt occurred without system override authority',
)
.not.toHaveBeenCalled();
expect
.soft(
session.promptCount,
'absence read as permission: session state changed without system override authority',
)
.toBe(0);
});
});
@@ -26,7 +26,7 @@ function makeService(operatorMemory: unknown = null): AgentService {
{} as never,
{ getToolDefinitions: vi.fn(() => []) } as never,
{ loadForSession: vi.fn(async () => ({ metaTools: [], promptAdditions: [] })) } as never,
null,
{ get: vi.fn().mockResolvedValue(null), renew: vi.fn().mockResolvedValue(undefined) } as never,
null,
{ collect: vi.fn().mockResolvedValue(undefined) } as never,
operatorMemory as never,
+9 -11
View File
@@ -132,9 +132,8 @@ export class AgentService implements OnModuleDestroy {
@Inject(CoordService) private readonly coordService: CoordService,
@Inject(McpClientService) private readonly mcpClientService: McpClientService,
@Inject(SkillLoaderService) private readonly skillLoaderService: SkillLoaderService,
@Optional()
@Inject(SystemOverrideService)
private readonly systemOverride: SystemOverrideService | null,
private readonly systemOverride: SystemOverrideService,
@Optional()
@Inject(PreferencesService)
private readonly preferencesService: PreferencesService | null,
@@ -709,23 +708,22 @@ export class AgentService implements OnModuleDestroy {
throw new Error(`No agent session found: ${sessionId}`);
}
this.assertSessionScope(session, scope);
session.promptCount += 1;
// Channel attachments are untrusted URI references. Preserve exact,
// authenticated metadata for the agent without treating it as authority.
const attachmentContext = this.attachmentContext(attachments);
// Prepend session-scoped system override if present (renew TTL on each turn)
// Prepend session-scoped system override if present (renew TTL on each turn).
// Required instruction-authority wiring is consulted before session/provider effects.
let effectiveMessage = `${message}${attachmentContext}`;
if (this.systemOverride) {
const override = await this.systemOverride.get(sessionId, scope);
if (override) {
effectiveMessage = `[System Override]\n${override}\n\n${effectiveMessage}`;
await this.systemOverride.renew(sessionId, scope);
this.logger.debug(`Applied system override for session ${sessionId}`);
}
const override = await this.systemOverride.get(sessionId, scope);
if (override) {
effectiveMessage = `[System Override]\n${override}\n\n${effectiveMessage}`;
await this.systemOverride.renew(sessionId, scope);
this.logger.debug(`Applied system override for session ${sessionId}`);
}
session.promptCount += 1;
try {
await session.piSession.prompt(effectiveMessage);
} catch (err) {
+5 -14
View File
@@ -451,24 +451,15 @@ describe('AppModule federation gating', (): void => {
);
it(
'rejects an invalid explicit monorepo-root dotenv tier with a typed startup refusal',
'attributes an invalid monorepo-root dotenv tier to the default',
async (): Promise<void> => {
const failure = await loadModuleGraphFromDotenv({
const graph = await loadModuleGraphFromDotenv({
rootEnvContents: 'MOSAIC_STORAGE_TIER=invalid\n',
expectedProcessTier: 'invalid',
}).then(
(): undefined => undefined,
(error: unknown): unknown => error,
);
expect(failure).toBeInstanceOf(Error);
expect(failure).toMatchObject({
name: 'MosaicConfigEnvironmentError',
code: 'invalid_storage_tier',
});
expect((failure as Error).message).toBe(
'Invalid MOSAIC_STORAGE_TIER; expected "local", "standalone", or "federated".',
);
expect(graph.imports).not.toContain(graph.federationModule);
expectBootLogLine(graph.bootLogLines, 'local', 'default');
},
MODULE_IMPORT_TIMEOUT_MS,
);
@@ -1,51 +0,0 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { resolveChatRuntimeMode } from './chat-runtime.js';
interface TypedSelectionFailure {
readonly name: string;
readonly code: string;
}
describe('#1182 fail closed — a wrong answer must not be read as no answer', () => {
it('FL-07 rejects an invalid explicit CHAT_HARNESS_RUNTIME before embedded construction', () => {
let embeddedConstructionCount = 0;
let failure: unknown;
try {
const mode = resolveChatRuntimeMode({ CHAT_HARNESS_RUNTIME: 'pi-rpc-typo' });
if (mode === 'legacy') {
embeddedConstructionCount += 1;
}
} catch (error: unknown) {
failure = error;
}
expect
.soft(failure, 'invalid explicit runtime must produce a typed selection failure')
.toMatchObject({
name: 'ChatRuntimeConfigurationError',
code: 'invalid_chat_harness_runtime',
} satisfies TypedSelectionFailure);
expect(
embeddedConstructionCount,
'invalid explicit runtime must fail before the embedded runtime is constructed',
).toBe(0);
});
it.each([{}, { CHAT_HARNESS_RUNTIME: '' }])(
'preserves the documented transitional legacy default for true absence: %j',
(env) => {
expect(resolveChatRuntimeMode(env)).toBe('legacy');
},
);
it('anti-drift: runtime selection has no invalid-enum-to-legacy catch-all', () => {
const source = readFileSync(new URL('./chat-runtime.ts', import.meta.url), 'utf8');
expect(
source.includes("env['CHAT_HARNESS_RUNTIME'] === 'pi-rpc' ? 'pi-rpc' : 'legacy'"),
'closed runtime enums must distinguish invalid explicit input from absence',
).toBe(false);
});
});
+3 -17
View File
@@ -41,28 +41,14 @@ export class ChatRuntimeUnavailableError extends Error {
}
}
/** Typed startup refusal for an invalid explicit chat-runtime selection. */
export class ChatRuntimeConfigurationError extends Error {
readonly code = 'invalid_chat_harness_runtime' as const;
constructor() {
super('Invalid CHAT_HARNESS_RUNTIME; expected "legacy" or "pi-rpc".');
this.name = 'ChatRuntimeConfigurationError';
}
}
/**
* Resolves the process-wide chat runtime mode from the environment. Only true
* absence (unset or empty) retains the documented transitional legacy default;
* any other explicit value must be a member of the closed runtime enum.
* Resolves the process-wide chat runtime mode from the environment. Anything other
* than the exact opt-in token `pi-rpc` keeps the legacy embedded runtime.
*/
export function resolveChatRuntimeMode(
env: Record<string, string | undefined> = process.env,
): ChatRuntimeMode {
const runtime = env['CHAT_HARNESS_RUNTIME'];
if (runtime === undefined || runtime === '') return 'legacy';
if (runtime === 'legacy' || runtime === 'pi-rpc') return runtime;
throw new ChatRuntimeConfigurationError();
return env['CHAT_HARNESS_RUNTIME'] === 'pi-rpc' ? 'pi-rpc' : 'legacy';
}
// ---------------------------------------------------------------------------
@@ -80,6 +80,10 @@ const mockMcpClient = {
getToolDefinitions: vi.fn(() => []),
};
const allowAuthorization = {
authorize: vi.fn().mockResolvedValue({ allowed: true }),
};
function buildService(
redis: typeof mockRedis | null = mockRedis,
mcpClient: {
@@ -98,6 +102,7 @@ function buildService(
null,
mockChatGateway as never,
mcpClient as never,
allowAuthorization as never,
);
}
@@ -35,9 +35,8 @@ export class CommandExecutorService {
@Inject(forwardRef(() => ChatGateway))
private readonly chatGateway: ChatGateway | null,
@Inject(McpClientService) private readonly mcpClient: McpClientService,
@Optional()
@Inject(CommandAuthorizationService)
private readonly authorization: CommandAuthorizationService | null = null,
private readonly authorization: CommandAuthorizationService,
) {}
async execute(
@@ -57,13 +56,13 @@ export class CommandExecutorService {
};
}
const authorization = await this.authorization?.authorize(
const authorization = await this.authorization.authorize(
def,
payload,
userId,
payload.approvalId,
);
if (authorization && !authorization.allowed) {
if (!authorization.allowed) {
return { command, conversationId, success: false, message: authorization.reason };
}
@@ -171,7 +170,7 @@ export class CommandExecutorService {
const def = this.registry
.getManifest()
.commands.find((command) => command.name === payload.command);
if (!def || !this.authorization) return null;
if (!def) return null;
return this.authorization.createApproval(def, payload, scope.userId);
}
@@ -55,6 +55,10 @@ const mockMcpClient = {
reconnectServer: vi.fn().mockResolvedValue(undefined),
};
const allowAuthorization = {
authorize: vi.fn().mockResolvedValue({ allowed: true }),
};
// ─── Helpers ─────────────────────────────────────────────────────────────────
function buildRegistry(): CommandRegistryService {
@@ -74,6 +78,7 @@ function buildExecutor(registry: CommandRegistryService): CommandExecutorService
null, // reloadService (optional)
null, // chatGateway (optional)
mockMcpClient as never,
allowAuthorization as never,
);
}
@@ -159,6 +159,7 @@ describe('ReloadService — /reload command sanitizes plugin errors', () => {
reloadService,
mockChatGateway as never,
mockMcpClient as never,
{ authorize: vi.fn().mockResolvedValue({ allowed: true }) } as never,
);
const payload: SlashCommandPayload = { command: 'reload', conversationId: 'conv-1' };
+3 -3
View File
@@ -23,10 +23,10 @@
| RI-2-001 | done | RI-N2 (Forge): remove stub-executor false success; `--simulate` typed `simulated` results that satisfy nothing; literal-`true` gates and echo-review replaced with real gates or typed waiting-for-authority | #1275 | pi-glm-5.3 | mosaicstack/stack | fix/ri-050-forge-fail-closed | RI-0-001 | 20K | Independent review APPROVED 2026-08-17 (Gitea review 172 on PR #1278, head 99b8f6ea; reviewing seat fargo — recorded under shared host principal mos-dt-0, provenance correction posted by fred; wrapper gap filed by fred). Executed at head: forge tests 116/116, lint green, typecheck green after building macp dist (minimal-install artifact, not a defect), workspace typecheck 45/45, no external type consumers of the changed interfaces. CI red = known lane-wide fleet-test failure only, carries no information about this change (fred, log-content analysis, pipelines 2456-2458). Non-blocking finding: README L141-143 + skills/mosaic-forge/SKILL.md document bare forge run/resume, which now fails closed — fast-follow docs touch. Merge queued behind #1270. UPDATE 2026-08-18: #1270 merged; CI GREEN at head 4917df1f via serialized retry (pipeline 2477) - root cause of prior reds was CI-agent contention (web SPA timeouts under concurrent pipelines), superseding the fleet-test-failure theory. |
| RI-2-002 | done | RI-N2 (MACP): gate runner fails closed on empty commands, stub executors, and unimplemented CI-provider gates unless explicit simulate; typed capability failures | #1275 | pi-glm-5.3 | mosaicstack/stack | fix/ri-050-macp-fail-closed | RI-0-001 | 15K | PR #1293 (head 2097379e): CI green (pipeline 2465), independent review APPROVED (Gitea review 173, jarvis seat, 2026-08-17) - macp 109/109 verified at head. Merge queued behind #1276/#1277/#1278. |
| RI-3-001 | done | RI-N4: complete probe inventory mapping every TS and shell quality-rail check to one canonical check with disposition (preserve/strengthen/retire, each named) | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-qr-probe-inventory | RI-0-001 | 12K | PR #1302 (head e06a47fac591): CI green (2484), independent review APPROVED (Gitea review 187, fargo seat, 2026-08-18) — 54 rows / 21 canonical checks / dispositions 43-2-9-0 verified by row-count and code spot-checks. Merged by fargo at pinned head. |
| RI-3-002 | not-started | RI-N4: TS evaluator absorbs effective shell probes; typed results (passed/failed/blocked/error/not-applicable) with versioned digested check definitions; shell commands become thin adapters; contract/parity/negative-control tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-qr-evaluator | RI-3-001 | 30K | |
| RI-4-001 | in-progress | RI-N3: one PRD application service — `mission --plan` persists mission↔PRD linkage (ids/versions/selected requirements); `mosaic prdy` routes through the service or becomes a named import/export adapter; Markdown is a labeled generated view; explicit conflict-aware import | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-prd-authority | RI-0-001 | 35K | PR #1294 (head 8d258e1d): CI green (pipeline 2466), independent review APPROVED (Gitea review 174, jarvis seat, 2026-08-17) - prdy 20/20 + command specs 9/9 at head. Merge queued behind #1276/#1277/#1278. |
| RI-3-002 | done | RI-N4: TS evaluator absorbs effective shell probes; typed results (passed/failed/blocked/error/not-applicable) with versioned digested check definitions; shell commands become thin adapters; contract/parity/negative-control tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-qr-evaluator | RI-3-001 | 30K | PR #1308 (head 68279d61): CI green (2506), independent review APPROVED (Gitea review 188, fred, seven mutations incl. vacuous-pass + stage-removal). Merged by fargo at pinned head → next @ 245e0c4. Follow-up #1309 (digest wording). |
| RI-4-001 | done | RI-N3: one PRD application service — `mission --plan` persists mission↔PRD linkage (ids/versions/selected requirements); `mosaic prdy` routes through the service or becomes a named import/export adapter; Markdown is a labeled generated view; explicit conflict-aware import | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-prd-authority | RI-0-001 | 35K | PR #1294 (head 8d258e1d): CI green (pipeline 2466), independent review APPROVED (Gitea review 174, jarvis seat, 2026-08-17) - prdy 20/20 + command specs 9/9 at head. Merge queued behind #1276/#1277/#1278. PR #1294 (head 8d258e1d): CI green (2466), review 174. Merged 2026-08-18 overnight wave → next @ d92de53. |
| RI-5-001 | done | RI-N5: typed freshness states (current/stale/partial/unknown/unavailable); no failed-fetch-renders-empty; stale derived verdicts → unknown; mutations disabled when stale; failure-matrix tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-web-stale-safety | RI-0-001 | 25K | |
| RI-V-001 | not-started | Final verification + release evidence: all cards verified merged, negative controls demonstrated, real `next` publish run green on exact commit, evidence pack recorded | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-release-evidence | RI-1-002, RI-2-001, RI-2-002, RI-3-002, RI-4-001, RI-5-001 | 10K | |
| RI-V-001 | in-progress | Final verification + release evidence: all cards verified merged, negative controls demonstrated, real `next` publish run green on exact commit, evidence pack recorded | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-release-evidence | RI-1-002, RI-2-001, RI-2-002, RI-3-002, RI-4-001, RI-5-001 | 10K | Evidence pack live on branch docs/ri-050-release-evidence — all five requirements evidenced; registry credential fixed (jarvis, #1275 c23239) and PROVEN green: pipeline 2517 (retry of 2512, identical commit) all steps green incl. build-gateway; pack PR next, then topher review + merge, close #1275. |
## Dispatch waves (max 2 parallel workers)
+130
View File
@@ -0,0 +1,130 @@
# RI-050 Release Evidence Pack (alpha 0.0.50 release-integrity floor)
> Status: **DRAFT — proof complete, awaiting review + merge**. All five normative requirements (RI-N1..N5) merged to `next` behind the live gate. Registry credential fixed 2026-08-18 23:47Z and **proven end-to-end**: push pipeline **2517** (retry of failed 2512 at the identical commit d4d32a8, only the secret changed between runs) — all steps green including `build-gateway`. Remaining for closure: this pack PR reviewed (topher), merged to `next`, its own push pipeline green, #1275 closed. Last updated 2026-08-19 by fargo (day-takeover orchestrator).
> Card: RI-V-001. All sections marked ⏳ pending their card's merge. Normative source:
> `docs/PRD.md` § Release Integrity Workstream (#1275).
## RI-N1 — Canonical terminal verification + exact-commit publish gate
| exhibit | evidence | where |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| Gate live, fail-closed | Push pipeline **2486**: `verify` ran at exact commit, FAILED on a real latent defect (gateway cross-user-isolation cleanup on the no-DB path), and `build` / `publish-npm` / `build-gateway` were all **skipped**. First push in repo history that did not publish ungated (prior ungated publishes beside failing builds: 2439, 2462, 2482). | Woodpecker repo 47 pipeline 2486 |
| Gate-caught defect fixed | PR **#1304** (afterAll honors `dbAvailable`; both paths verified: dead-port 28 skipped + file passes; live-5433 28 passed). Review 180 (fred). | PR #1304 |
| First gated green npm publish | Push pipeline **2488** (post-#1304): `verify` GREEN → `build` GREEN → `publish-next-npm` GREEN, all publish effects behind the gate. | Woodpecker pipeline 2488 |
| Negative controls | PR **#1305**: structural DAG tests (S1 missing edge, S2 renamed effect incl. command-based npm/kaniko detection, S3 detach, S4 failure:ignore/success override, S5 when-filter, S6 HEAD-mover between verify and publish with legitimate-recheckout positive control, S7 removal) + subset-stage composition control in verify-release.test.mjs. Mutation-verified by the dispatching seat in both directions (true bypass → S1 assertion fires; non-bypass edit → correctly green). Scripts tests 20/20, CI 2490 green. | PR #1305 |
| ✅ Canonical command | `scripts/verify-release.mjs` (stage table pinned to ci.yml by checked-in test). Merged with #1277; now also invokes the RI-N4 evaluator via its `quality-rails` stage (#1308). | `scripts/verify-release.mjs` |
## RI-N2 — Forge + MACP fail-closed (typed explicit simulation)
- ✅ Forge: PR **#1278** merged (head 4917df1f; CI 2477; review 184 fred at pinned head — prior review 172 dismissed by rebase, correctly re-taken).
- ✅ MACP: PR **#1293** merged (head 2097379e; CI 2465; review 173).
- ✅ Post-merge behavior docs: PR **#1299** merged (head 8a405b14; CI 2497; review 186 fargo at pinned head — legitimate independent seat; merged 2026-08-18 with --expect-head pin, content-verified on next @ ff45f7b).
## RI-N3 — PRD authority
- ✅ PR **#1294** merged (head 8d258e1d; CI 2466; review 174).
## RI-N4 — Quality-rails evaluator
- ✅ Probe inventory: PR **#1302** merged (head e06a47fac59; CI 2484; review 187 fargo at pinned head; 54 rows / 21 canonical checks / dispositions 43-2-9-0 row-count-verified; merged 2026-08-18, content-verified on next @ 6435089).
- ✅ TS evaluator absorbs shell probes: PR **#1308** merged (head 68279d61; CI 2506; review 188 fred at pinned head — seven targeted mutations, seven detections, incl. the vacuous-pass hole M1 and stage-removal M7). Evaluator: typed fail-closed verdicts, digested versioned definitions, per-subject sets; QC-19 absorbed (verbatim-list parity oracle), QC-20 as thin adapter (verify.sh unmodified); verify-release `quality-rails` stage wired (RI-N1 consumes the evaluator). Worker-produced, independently verified by the dispatching seat (quality-rails 40/40 incl. sabotage control 6-failed/34-passed restored sha-verified; root build 25/25; typecheck 45/45).
## RI-N5 — Consequence-aware stale UI
- ✅ PR **#1300** merged (head a337d787; CI 2481; review 179). Web suite 199 → 281 tests (failure matrix + negative controls), independently re-run by the dispatching seat before merge.
## Known-open infrastructure item (not a card)
Gateway/ci-base **image** pushes fail on registry credentials: Woodpecker repo
secrets `REGISTRY_USERNAME`/`REGISTRY_PASSWORD` are rejected by the Gitea
container registry (explicit `UNAUTHORIZED` at `/v2/token`; pipeline 2494 after
PR #1306 corrected the secret references — previously masked as an ambiguous
push-permission error since at least 2439). Requires a package-scoped token
(Jason). The npm publish path is green and gated; this item tracks image pushes
only and predates the RI-050 floor.
**Update 2026-08-18 (fargo):** Jason set new secret values ~17:25Z; pipeline
**2507** (the #1308 merge push, first after the update, 18:0xZ) still fails
`build-gateway` with the identical `UNAUTHORIZED`. Read-only isolation (no
secrets read, no CI retries): the registry endpoint and auth mechanism are
HEALTHY — a valid Gitea token via basic-auth mints a JWT at `/v2/token` (200),
bad credentials 401 cleanly. Therefore the failure is isolated to the secret
VALUES, not the endpoint or pipeline. Most likely shape error (labeled guess):
the registry authenticates username + **API token with package scope**, not
username + login password; if REGISTRY_PASSWORD holds a login password rather
than a minted token value, `/v2/token` 401s exactly as observed. npm publishes
remained green in 2507; every publish step except the image push is gated and
green.
**Resolution 2026-08-18 23:47Z — FIXED on the Gitea server (jarvis, #1275
comment 23239).** Root cause was neither scope nor a missing token:
`REGISTRY_USERNAME` held `mosaic`, the **pre-rename org name**. Gitea's rename
redirect covers API/web paths but not Basic-auth username lookup, and
`mosaicstack` is an organization, which has no password — the pair could never
authenticate. Fix: `REGISTRY_USERNAME`=`woodpecker` (the existing service
account, Gitea user 41, already in `ci-publish`) and `REGISTRY_PASSWORD`= a
newly minted `write:package`-only token (`gitea admin user generate-access-token`
in the Gitea container; minting with a token is forbidden server-side). Events
`[push, tag]` preserved. Verified **without a pipeline run**:
`POST /v2/<pkg>/blobs/uploads/` opened then cancelled a session — **202** on
all four kaniko destinations (gateway, appservice, web, ci-base), anonymous
control **401**, wrong-owner control **401**. The earlier "Requires a
package-scoped token (Jason)" expectation is superseded: the defect was a
stale value from the org rename, not a scope grant Jason owed.
**Proof 2026-08-19 ~00:2xZ (fargo): pipeline 2517 green at build-gateway.**
Woodpecker retry of 2512 — identical commit d4d32a8, identical pipeline
config, only the server-side secret changed between runs — went green on
every step (clone, install, verify, build, publish-next-npm,
**build-gateway**). A/B at the same commit isolates the credential as the
variable; the stored value is byte-intact. Retry was serialized (sole run in
flight; merge-purpose CI queue guard had blocked on 2512's terminal failure
at the `next` head, which this retry also clears). The item is closed.
**Timing caveat (recorded so the pack does not outlive the memory of what the
pin was).** Every pipeline this pack cites — including headline 2517 —
finished by 2026-08-19 00:34Z, which is BEFORE the registry credential pin
landed at 2026-08-19 23:42:05Z. The A/B above remains sound regardless: it
compares identical commits (d4d32a8) with only the secret differing, so it
proves the credential value, not anything about the later pin. No pipeline
cited in this pack exercises the post-pin registry state; a green trunk
publish after the pin is a separate fact that this pack does not claim.
## Process record (audit trail)
- Merges executed under the jarvis principal (topher seat; identity provisioning
pending) via the Gitea API replicating `pr-merge.sh` semantics (head-pin +
squash + keep branch): `pr-merge.sh` hard-codes `main`-only targets and cannot
express this repo's `next` trunk — wrapper gap captured to OpenBrain
(id 9db7a95a) and to the framework queue.
- Reviews tonight: 175/178 (zane's #1298, both heads, by topher); 176/177/179/
180/181/182 (fred) — cross-review rule (producer ≠ reviewer) held on every
merge: producers were pi workers / zane; reviewers were the other seat.
- CI contention note: concurrent PR pipelines on the single CI agent can time
out the web SPA suite (measured 2470/2472 vs serialized 2475/2476/2477);
serialize retries when the queue is busy.
## Process record — 2026-08-18 day takeover (fargo)
- Takeover directive: Jason (via jarvis router + both seats' handoff documents,
relayed verbatim over comms). First-move conflict between the two handoffs
(zane: doctor PR first; topher: review-queue first) resolved on dependency
grounds per jarvis's read — topher's order won; zane's finding-2 doctor PR
(upgraded by fred's measurement) remains queued, nothing depends on it.
- Reviews 186 (#1299) + 187 (#1302): fargo, at pinned heads, as the legitimate
independent seat (topher dispatched both producers; cross-review rule held).
Both merged with --expect-head pinning via the REPO-COPY pr-merge.sh
(allows next; the installed copy still lags — zane's route, not the raw-API
break-glass), each preceded by ci-queue-wait -B next -R mosaicstack/stack.
CI green at both heads (2497, 2484). Merges content-verified on the shipping
ref (TASKS anchors at ff45f7b / 6435089).
- RI-3-002: one pi worker (zai/glm-5.3:high), independently verified by the
dispatching seat before push; PR #1308 reviewed by fred (188, seven
mutations incl. vacuous-pass and stage-removal) and merged head-pinned at
68279d61 → next @ 245e0c4.
- Registry-credential isolation measurement (above) performed read-only; no
secret values read, no retry-pushes against CI.
- One reviewer-scope disclosure (fred, review 188): fred's approval explicitly
did NOT re-run root build/typecheck/mosaic-vitest — those remain the
dispatching seat's numbers. The changed-package suites, verify-release
suite, and seven mutations were fred's own.
@@ -0,0 +1,77 @@
# #1179 — Required security DI wiring
## Objective
Eliminate the shared fail-open defect class **absence read as permission**:
- FL-01: missing `CommandAuthorizationService` must refuse Nest startup and must not permit command effects.
- FL-11: missing `SystemOverrideService` must refuse Nest startup and must not omit stored instruction authority while allowing provider/session effects.
## Tracking
- Issue: #1179, child of #1156
- Branch: `fix/1179-required-security-di`
- Base: `origin/next` at `216cd72226cd9ee17eea461cfe7cd0e010a22f02`
## Plan
1. RED: compile the real `AppModule` graph with each required provider independently removed, with a positive control for each intact binding.
2. RED: directly exercise each malformed absence path and assert zero command/provider/session effects.
3. Stop and report RED to the coordinator before production implementation.
4. After authorization, make both constructor injections required, remove absence-as-permission branches, and update explicit legitimate optional test seams.
5. Run focused Gateway tests, typecheck, lint, format, build, independent exact-head verification, and focused security review.
## Immutable path fence
Production changes are confined to:
- `apps/gateway/src/commands/command-executor.service.ts`
- `apps/gateway/src/agent/agent.service.ts`
Tests and task evidence are confined to:
- `apps/gateway/src/__tests__/required-security-wiring.test.ts`
- existing direct-constructor specs that require explicit required arguments
- `docs/scratchpads/1179-required-security-di.md`
No files in #1178, #1072, #1080, or #1054 lanes are in scope. `docs/TASKS.md` is orchestrator-owned and will not be modified.
## Budget
No explicit token ceiling was provided. Working assumption: one narrow Gateway security packet; split and stop if either arm requires unrelated module rewiring.
## Progress
- Intake read from #1179 and parent #1156.
- Base independently resolved from the issue's pre-native-stage ordering and repository `origin/next` ref; branch HEAD verified byte-for-byte against the remote ref.
- Real consumers and direct constructors inventoried.
## Tests
### RED
- `required-security-wiring.test.ts`: 4 failed, 2 passed before implementation.
- Both real-graph negative controls showed module compilation accepted the missing target binding.
- Direct FL-01 showed one unauthorized command effect; direct FL-11 showed one provider prompt and one session counter mutation.
### GREEN
- `required-security-wiring.test.ts`: 6/6 passed.
- FL-01-only production revert: exactly the two FL-01 test cases failed; all four other cases, including FL-11, passed.
- FL-11-only production revert: exactly the two FL-11 test cases failed; all four other cases, including FL-01, passed.
- Full Gateway suite: 74 files passed, 7 skipped; 831 tests passed, 17 skipped.
- Gateway typecheck: passed.
- Gateway lint: passed.
- Gateway build: passed.
- Changed-file Prettier check: passed.
### Review
- Codex code review: APPROVE, 0 findings.
- Codex focused security review: risk `none`, 0 findings.
- Independent exact-head review remains assigned to Scrappy through the coordinator.
## Risks / blockers
- `AgentModule` / `CommandsModule` / `ChatModule` contain a production cycle; the module test therefore uses the real top-level `AppModule` and replaces only storage/network leaves, preserving the target service in each arm while isolating the separate required consumer that would otherwise mask that arm's defect.
- No broad module rewrite was required.
@@ -1,59 +0,0 @@
# #1182 — fail closed when a wrong answer is read as no answer
## Objective
Implement FL-07 through FL-10 as one narrow fail-closed change: explicit invalid runtime/storage enum values and launcher failures must never be interpreted as absence or success.
## Tracking
- Issue: #1182 (child of #1156; W-F1 prerequisite)
- Branch: `fix/1182-fail-closed-launch`
- Verified base: `origin/next` at `216cd72226cd9ee17eea461cfe7cd0e010a22f02`
## Scope and fence
- Gateway chat runtime enum resolution and focused tests.
- Config storage-tier enum resolution and focused tests.
- Mosaic launcher spawn/provenance failure handling and focused integration/anti-drift tests.
- Narrow operator documentation in `packages/mosaic/README.md` if required by the behavior change.
- Preserve the #1109 lease broker without refactor; do not implement W-F1 composition.
- Do not touch #1178, #1179, #1072, #1080, #1054, `docs/TASKS.md`, or unrelated source/docs.
## Plan
1. RED: add one independent negative control per FL finding plus exact anti-drift checks.
2. Pause and report BASE / BRANCH / FENCE / SPLIT / RED to the coordinator.
3. After coordinator confirmation, implement each minimal fail-closed fix and verify each negative control independently.
4. Run affected package and repository test/typecheck/lint/build/format gates, focused security review, then commit/push/PR for independent exact-head verification.
## Split assessment
One PR remains reviewable: three narrowly bounded decision boundaries, no shared abstraction, no lease-broker refactor, and four independent tests naming the single defect class. Splitting would separate the same fail-closed invariant without reducing implementation coupling. If RED reveals material launcher harness expansion, split before production edits; the Gateway config half is the direct W-F1 selection prerequisite and would gate first.
## Budget
No explicit token or monetary cap supplied. Keep scope to the three production files, focused tests, this scratchpad, and one existing README.
## Progress
- Issue #1182 and parent #1156 read directly through Mosaic wrappers.
- Base derived from issue dependency plus repository topology: FL-07 exists on `origin/next` (introduced by #1172) and is absent from `origin/main`; branch reset before edits to the exact `origin/next` head above.
## Verification evidence
- RED observed independently for FL-07 through FL-10 before implementation.
- Focused GREEN: Gateway runtime 4/4, config 5/5, launcher 6/6.
- Four final per-finding production reverts discriminated: FL-07 made only FL-07 red; FL-08 made its unit and real Gateway boundary controls red after rebuilding config; FL-09 made only its abnormal-spawn controls red; FL-10 made all three provenance controls (`opencode`, `claudex`, and `yolo claudex`) red while sibling findings stayed green.
- Public config export negative control: removing only the `packages/config/src/index.ts` export caused TS2305 and `MosaicConfigEnvironmentError is not a constructor`; restoring it passed 5/5.
- Gateway full package: 74 files passed, 7 skipped; 829 tests passed, 17 skipped.
- Config full test/typecheck/lint/build: green.
- Mosaic typecheck/lint/build: green. Vitest is qualified-red only on the exact three update-notice stderr assertions tracked by #1190 — bare `--source`, bare `--decisions`, and bare `--observations`; 1528 other tests pass.
- The separate `test:framework-shell` command is red and is not attributed to #1190: `invariant_r_unittest.py` reports the host Pi runtime changed from measured 0.84.1 to 0.80.7. A clean archive of `origin/next@216cd722` reproduces the same single failure. The base test hardcodes the W-B measurement as `PI_VERSION = "0.84.1"`, then resolves `pi` via `shutil.which` and executes `--version`; on this host that is `/home/hermes/.npm-global/bin/pi`, whose global package reports 0.80.7. Issue #1191 tracks the immediate host drift and hardcoded-version design defect; #1184 tracks pinning/approving native Pi 0.84.1. No related source or test is changed here.
- Repository typecheck/lint/format/build: green.
- Codex security review: no findings. Initial code-review blocker (claudex provenance bypass) remediated with normal/yolo claudex coverage; subsequent public-export finding remediated.
## Risks / blockers
- Coordinated branch: no merge authority; coordinator routes independent exact-head verification.
- #1179 currently owns `apps/gateway/src/__tests__/required-security-wiring.test.ts`; this change does not touch it.
- #1190 independently tracks the pre-existing CLI smoke/update-notice stderr collision; no #1190 source or test is included here.
-1
View File
@@ -3,7 +3,6 @@ export {
DEFAULT_LOCAL_CONFIG,
DEFAULT_STANDALONE_CONFIG,
DEFAULT_FEDERATED_CONFIG,
MosaicConfigEnvironmentError,
loadConfig,
validateConfig,
detectFromEnv,
@@ -1,72 +0,0 @@
import { readFileSync } from 'node:fs';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { MosaicConfigEnvironmentError as PublicMosaicConfigEnvironmentError } from '@mosaicstack/config';
import { detectFromEnv } from './mosaic-config.js';
interface TypedSelectionFailure {
readonly name: string;
readonly code: string;
}
describe('#1182 fail closed — a wrong answer must not be read as no answer', () => {
const originalEnv = process.env;
it('exports the typed storage-tier refusal from the public @mosaicstack/config entry point', () => {
expect(new PublicMosaicConfigEnvironmentError()).toBeInstanceOf(Error);
});
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env['DATABASE_URL'];
delete process.env['VALKEY_URL'];
delete process.env['MOSAIC_STORAGE_TIER'];
});
afterEach(() => {
process.env = originalEnv;
});
it('FL-08 rejects an invalid explicit MOSAIC_STORAGE_TIER before local PGlite selection', () => {
process.env['MOSAIC_STORAGE_TIER'] = 'federatd';
let pgliteSelectionCount = 0;
let failure: unknown;
try {
const config = detectFromEnv();
if (config.storage.type === 'pglite') {
pgliteSelectionCount += 1;
}
} catch (error: unknown) {
failure = error;
}
expect
.soft(failure, 'invalid explicit storage tier must produce a typed selection failure')
.toMatchObject({
name: 'MosaicConfigEnvironmentError',
code: 'invalid_storage_tier',
} satisfies TypedSelectionFailure);
expect(
pgliteSelectionCount,
'invalid explicit storage tier must fail before local PGlite is selected',
).toBe(0);
});
it.each([undefined, ''])('preserves the local default for true absence: %j', (tier) => {
if (tier === undefined) delete process.env['MOSAIC_STORAGE_TIER'];
else process.env['MOSAIC_STORAGE_TIER'] = tier;
const config = detectFromEnv();
expect(config.tier).toBe('local');
expect(config.storage.type).toBe('pglite');
});
it('anti-drift: storage selection validates a non-empty tier before the local default', () => {
const source = readFileSync(new URL('./mosaic-config.ts', import.meta.url), 'utf8');
expect(
/if \(tier !== undefined && tier !== ''[^]*throw new [A-Za-z]+Error/.test(source),
'closed storage enums must reject invalid explicit input before DEFAULT_LOCAL_CONFIG',
).toBe(true);
});
});
-14
View File
@@ -20,16 +20,6 @@ export interface MosaicConfig {
memory: MemoryConfigRef;
}
/** Typed startup refusal for an invalid explicit storage-tier selection. */
export class MosaicConfigEnvironmentError extends Error {
readonly code = 'invalid_storage_tier' as const;
constructor() {
super('Invalid MOSAIC_STORAGE_TIER; expected "local", "standalone", or "federated".');
this.name = 'MosaicConfigEnvironmentError';
}
}
/* ------------------------------------------------------------------ */
/* Defaults */
/* ------------------------------------------------------------------ */
@@ -136,10 +126,6 @@ export function validateConfig(raw: unknown): MosaicConfig {
export function detectFromEnv(): MosaicConfig {
const tier = process.env['MOSAIC_STORAGE_TIER'];
if (tier !== undefined && tier !== '' && !VALID_TIERS.has(tier)) {
throw new MosaicConfigEnvironmentError();
}
if (tier === 'federated') {
if (process.env['DATABASE_URL']) {
return {
-16
View File
@@ -26,15 +26,6 @@ Set `MOSAIC_ASSUME_YES=1` (or ensure stdin is not a TTY) to skip all interactive
| `MOSAIC_ANTHROPIC_API_KEY` | _(none)_ | No |
| `MOSAIC_CORS_ORIGIN` | `http://localhost:3000` | No |
`MOSAIC_STORAGE_TIER` is a closed enum: `local`, `standalone`, or `federated`.
Unset or empty input selects the documented local default. Any other non-empty
value is a typed startup error and is rejected before a storage adapter is
selected.
The Gateway process also accepts `CHAT_HARNESS_RUNTIME=legacy|pi-rpc`. Unset or
empty input retains the transitional `legacy` default. Any other non-empty value
is a typed startup error and is rejected before either runtime is selected.
### Admin user bootstrap
| Variable | Default | Required |
@@ -64,13 +55,6 @@ mosaic yolo claude # …with --dangerously-skip-permissions
mosaic codex | opencode | pi
```
Every runtime launch requires its immutable `session.launch` provenance record
to be written before spawn. A provenance-write failure exits nonzero, starts no
runtime, and propagates no `MOSAIC_LAUNCH_ID`. Likewise, a spawn error, signal,
or missing numeric child status is reported with the fixed
`runtime_launch_failed` code and exits nonzero rather than being interpreted as
success.
### `mosaic claudex` (EXPERIMENTAL)
Runs GPT models **inside the Claude Code harness** by pointing Claude Code at a
@@ -65,6 +65,19 @@ Each of these produced a wrong conclusion before it was written down.
conclusion drawn from it describes the wrong tree. Confirm `git rev-parse --show-toplevel`
is the tree you think it is before trusting any git output.
13. **Run the repository's PINNED tool version.** `npx <tool>` resolves a local `node_modules`
install when one is present and fetches the latest release when one is not, so the same
command answers differently depending on where it ran. A reviewer measuring in a fresh clone
or a detached worktree — which is exactly where reviewers measure — has no `node_modules` and
silently gets the latest release instead of the pinned one. Measured on mosaicstack#1313: the
lockfile pins prettier 3.8.1, under which three guides pass; a version-less `npx` in a
worktree resolved 3.9.6, under which the same three fail; and 3.0.0, the floor of the declared
`^3.0.0` range, fails a different one. Three versions, three verdicts, identical bytes. Use
`node_modules/.bin/<tool>`, or name the version the lockfile pins.
14. **A formatter or linter declared as a range is a dated verdict, not a fact.** If a lockfile
pins it, the gate is reproducible today and will disagree with itself the day the pin moves.
Report a formatting failure with the version that produced it, always.
### Feedback Categories
- **Blocker**: must fix before merge (security, bugs, test failures)
@@ -1,193 +0,0 @@
import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { createRequire } from 'node:module';
import { delimiter, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
const require = createRequire(import.meta.url);
const TSX_LOADER_URL = pathToFileURL(require.resolve('tsx')).href;
const COMMANDER_MODULE_URL = pathToFileURL(require.resolve('commander')).href;
const LAUNCH_MODULE_URL = pathToFileURL(join(import.meta.dirname, 'launch.ts')).href;
const DRIVER = `
const launch = await import(${JSON.stringify(LAUNCH_MODULE_URL)});
if (process.env['TEST_CLAUDEX_PROVENANCE'] === '1') {
const execute = launch.execRecordedClaudexRuntime;
if (typeof execute !== 'function') {
process.stderr.write('[missing_recorded_claudex_runtime]\\n');
process.exit(70);
}
execute([], process.env, process.env['TEST_DANGEROUS'] === '1');
} else {
const { Command } = await import(${JSON.stringify(COMMANDER_MODULE_URL)});
const program = new Command();
program.exitOverride();
launch.registerLaunchCommands(program);
await program.parseAsync(['node', 'mosaic', 'opencode']);
}
`;
interface LaunchFixture {
readonly root: string;
readonly home: string;
readonly bin: string;
readonly runtimePath: string;
}
function createFixture(): LaunchFixture {
const root = mkdtempSync('/var/tmp/mosaic-launch-fail-closed-');
const home = join(root, 'mosaic-home');
const bin = join(root, 'bin');
const runtimePath = join(bin, 'opencode');
mkdirSync(join(home, 'runtime', 'opencode'), { recursive: true });
mkdirSync(bin, { recursive: true });
writeFileSync(join(home, 'AGENTS.md'), '# test agents\n');
writeFileSync(join(home, 'SOUL.md'), '# test soul\n');
writeFileSync(join(home, 'USER.md'), '# test user\n');
writeFileSync(join(home, 'TOOLS.md'), '# test tools\n');
writeFileSync(join(home, 'runtime', 'opencode', 'RUNTIME.md'), '# test runtime\n');
return { root, home, bin, runtimePath };
}
function installRuntime(fixture: LaunchFixture, source: string): void {
writeFileSync(fixture.runtimePath, source);
chmodSync(fixture.runtimePath, 0o755);
}
function runLauncher(
fixture: LaunchFixture,
extraEnv: NodeJS.ProcessEnv = {},
): SpawnSyncReturns<string> {
const env: NodeJS.ProcessEnv = {
...process.env,
...extraEnv,
MOSAIC_HOME: fixture.home,
PATH: `${fixture.bin}${delimiter}${process.env['PATH'] ?? ''}`,
};
delete env['MOSAIC_AGENT_NAME'];
delete env['MOSAIC_AGENT_CLASS'];
delete env['MOSAIC_AGENT_TOOL_POLICY'];
return spawnSync(
process.execPath,
['--import', TSX_LOADER_URL, '--input-type=module', '--eval', DRIVER],
{
cwd: fixture.root,
encoding: 'utf8',
env,
},
);
}
describe('#1182 fail closed — a wrong answer must not be read as no answer', () => {
let fixture: LaunchFixture;
beforeEach(() => {
fixture = createFixture();
});
afterEach(() => {
rmSync(fixture.root, { recursive: true, force: true });
});
it.each([
{
condition: 'spawn error',
runtime: '#!/definitely/missing/interpreter\n',
},
{
condition: 'signal with null status',
runtime: '#!/usr/bin/env bash\nkill -TERM $$\n',
},
])('FL-09 converts $condition into a sanitized nonzero launch failure', ({ runtime }) => {
installRuntime(fixture, runtime);
const result = runLauncher(fixture);
expect.soft(result.status, 'spawn failure must never be converted into exit 0').not.toBe(0);
expect
.soft(result.stderr, 'spawn failure must report the fixed typed runtime_launch_failed code')
.toContain('[runtime_launch_failed]');
expect(
result.stderr,
'spawn diagnostics must not disclose the runtime fixture path',
).not.toContain(fixture.runtimePath);
});
it.each([
{ launcher: 'opencode', extraEnv: {} },
{
launcher: 'claudex',
extraEnv: { TEST_CLAUDEX_PROVENANCE: '1' },
},
{
launcher: 'yolo claudex',
extraEnv: { TEST_CLAUDEX_PROVENANCE: '1', TEST_DANGEROUS: '1' },
},
])(
'FL-10 refuses $launcher spawn when mandatory provenance cannot be recorded and fabricates no launch ID',
({ extraEnv }) => {
const spawnMarker = join(fixture.root, 'runtime-spawned');
const launchIdMarker = join(fixture.root, 'runtime-saw-launch-id');
installRuntime(
fixture,
`#!/usr/bin/env bash\nprintf 'spawned' > "$TEST_SPAWN_MARKER"\nif [[ -n "\${MOSAIC_LAUNCH_ID:-}" ]]; then printf '%s' "$MOSAIC_LAUNCH_ID" > "$TEST_LAUNCH_ID_MARKER"; fi\n`,
);
const ledgerPath = join(fixture.home, 'fleet', 'run', 'sessions', 'events.ndjson');
mkdirSync(ledgerPath, { recursive: true });
const result = runLauncher(fixture, {
...extraEnv,
TEST_SPAWN_MARKER: spawnMarker,
TEST_LAUNCH_ID_MARKER: launchIdMarker,
});
expect.soft(result.status, 'mandatory provenance failure must exit nonzero').not.toBe(0);
expect
.soft(existsSync(spawnMarker), 'mandatory provenance failure must prevent spawn')
.toBe(false);
expect
.soft(
existsSync(launchIdMarker),
'a failed provenance write must not fabricate or propagate a launch ID',
)
.toBe(false);
expect
.soft(
result.stderr,
'provenance refusal must report the fixed typed launch_provenance_failed code',
)
.toContain('[launch_provenance_failed]');
expect(
result.stderr,
'provenance diagnostics must not disclose filesystem details',
).not.toContain(fixture.root);
},
);
it('anti-drift: launcher contains neither null-status success nor mandatory warn-and-run', () => {
const source = readFileSync(new URL('./launch.ts', import.meta.url), 'utf8');
expect
.soft(
source.includes('result.status ?? 0'),
'spawnSync null status must never default to success',
)
.toBe(false);
expect
.soft(
source.includes('[mosaic] WARNING: launch record not written:'),
'mandatory provenance failures must never warn and continue',
)
.toBe(false);
});
});
+25 -68
View File
@@ -200,24 +200,13 @@ function redactArgv(argv: string[]): string[] {
);
}
interface LaunchRecordSuccess {
readonly ok: true;
readonly launchId: string;
}
interface LaunchRecordFailure {
readonly ok: false;
readonly code: 'launch_provenance_failed';
}
type LaunchRecordResult = LaunchRecordSuccess | LaunchRecordFailure;
function recordLaunch(runtime: RuntimeName, cliArgs: string[], yolo: boolean): LaunchRecordResult {
// Never let a stale or caller-supplied correlation id masquerade as this launch.
delete process.env['MOSAIC_LAUNCH_ID'];
function recordLaunch(runtime: RuntimeName, cliArgs: string[], yolo: boolean): void {
try {
mkdirSync(LAUNCH_LEDGER_DIR, { recursive: true, mode: 0o700 });
// Correlation id for the lease.register half. Set into process.env so it
// propagates through every `...process.env` / `...baseEnv` spread below.
const launchId = `${Date.now().toString(36)}-${randomBytes(6).toString('hex')}`;
process.env['MOSAIC_LAUNCH_ID'] = launchId;
const record = {
seq: Date.now(),
kind: 'session.launch',
@@ -244,24 +233,12 @@ function recordLaunch(runtime: RuntimeName, cliArgs: string[], yolo: boolean): L
appendFileSync(join(LAUNCH_LEDGER_DIR, 'events.ndjson'), `${JSON.stringify(record)}\n`, {
mode: 0o600,
});
return { ok: true, launchId };
} catch {
return { ok: false, code: 'launch_provenance_failed' };
}
}
function requireLaunchRecord(runtime: RuntimeName, cliArgs: string[], yolo: boolean): string {
const result = recordLaunch(runtime, cliArgs, yolo);
if (!result.ok) {
} catch (err) {
// Never block a launch on bookkeeping — but never fail silently either.
console.error(
`[mosaic] ERROR [${result.code}]: mandatory launch provenance could not be recorded; runtime was not started.`,
`[mosaic] WARNING: launch record not written: ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
// Propagate correlation authority only after its immutable provenance exists.
process.env['MOSAIC_LAUNCH_ID'] = result.launchId;
return result.launchId;
}
// ─── Pre-flight checks ──────────────────────────────────────────────────────
@@ -999,7 +976,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
cliArgs.push(...args);
}
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
requireLaunchRecord('claude', cliArgs, yolo);
recordLaunch('claude', cliArgs, yolo);
execLeaseGatedRuntime('claude', cliArgs, process.env, yolo);
break;
}
@@ -1013,7 +990,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
cliArgs.push(...args);
}
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
requireLaunchRecord('codex', cliArgs, yolo);
recordLaunch('codex', cliArgs, yolo);
execRuntime('codex', cliArgs, { ...process.env, ...harnessEnv('codex') });
break;
}
@@ -1022,7 +999,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
// opencode follows XDG, so its config resolves to $XDG_CONFIG_HOME/opencode.
ensureRuntimeConfig('opencode', join(harnessHome('opencode'), 'opencode', 'AGENTS.md'));
console.log(`[mosaic] Launching ${label}${modeStr}...`);
requireLaunchRecord('opencode', args, yolo);
recordLaunch('opencode', args, yolo);
execRuntime('opencode', args, { ...process.env, ...harnessEnv('opencode') });
break;
}
@@ -1038,7 +1015,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
cliArgs.push(...args);
}
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
requireLaunchRecord('pi', cliArgs, yolo);
recordLaunch('pi', cliArgs, yolo);
execLeaseGatedRuntime('pi', cliArgs);
break;
}
@@ -1082,31 +1059,19 @@ function execLeaseGatedRuntime(
);
}
type RuntimeLaunchFailureReason = 'spawn_error' | 'signal' | 'missing_status';
interface RuntimeLaunchFailure {
readonly code: 'runtime_launch_failed';
readonly reason: RuntimeLaunchFailureReason;
}
function refuseRuntimeLaunch(reason: RuntimeLaunchFailureReason): never {
const failure: RuntimeLaunchFailure = { code: 'runtime_launch_failed', reason };
console.error(
`[mosaic] ERROR [${failure.code}]: runtime process did not produce a successful exit result (${failure.reason}).`,
);
process.exit(1);
}
/** Spawn the runtime and preserve only a real numeric exit status as success. */
/** exec into the runtime, replacing the current process. */
function execRuntime(cmd: string, args: string[], env: NodeJS.ProcessEnv = process.env): void {
const result = spawnSync(cmd, args, {
stdio: 'inherit',
env,
});
if (result.error !== undefined) refuseRuntimeLaunch('spawn_error');
if (result.signal !== null) refuseRuntimeLaunch('signal');
if (result.status === null) refuseRuntimeLaunch('missing_status');
process.exit(result.status);
try {
// Use execFileSync with inherited stdio to replace the process
const result = spawnSync(cmd, args, {
stdio: 'inherit',
env,
});
process.exit(result.status ?? 0);
} catch (err) {
console.error(`[mosaic] Failed to launch ${cmd}:`, err instanceof Error ? err.message : err);
process.exit(1);
}
}
/**
@@ -1116,15 +1081,6 @@ function execRuntime(cmd: string, args: string[], env: NodeJS.ProcessEnv = proce
* orchestration to `launchClaudex` in `claudex.ts`. Kept thin so the tested
* logic lives in the DI module, not here.
*/
export function execRecordedClaudexRuntime(
args: string[],
env: NodeJS.ProcessEnv,
dangerous: boolean,
): void {
const launchId = requireLaunchRecord('claude', args, dangerous);
execLeaseGatedRuntime('claude', args, { ...env, MOSAIC_LAUNCH_ID: launchId }, dangerous);
}
function launchClaudexProduction(args: string[], yolo: boolean): void {
writeSessionLock('claude');
const adapter: ClaudexHarnessAdapter = {
@@ -1136,7 +1092,8 @@ function launchClaudexProduction(args: string[], yolo: boolean): void {
checkSequentialThinking('claude');
},
composePrompt: () => buildRuntimePrompt('claude'),
execLeaseGated: execRecordedClaudexRuntime,
execLeaseGated: (cmdArgs, env, dangerous) =>
execLeaseGatedRuntime('claude', cmdArgs, env, dangerous),
};
void launchClaudex(args, yolo, adapter);
}