This commit is contained in:
@@ -26,6 +26,15 @@ 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 |
|
||||
@@ -55,6 +64,13 @@ 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
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -162,13 +162,24 @@ function redactArgv(argv: string[]): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function recordLaunch(runtime: RuntimeName, cliArgs: string[], yolo: boolean): void {
|
||||
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'];
|
||||
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',
|
||||
@@ -193,14 +204,26 @@ function recordLaunch(runtime: RuntimeName, cliArgs: string[], yolo: boolean): v
|
||||
appendFileSync(join(LAUNCH_LEDGER_DIR, 'events.ndjson'), `${JSON.stringify(record)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch (err) {
|
||||
// Never block a launch on bookkeeping — but never fail silently either.
|
||||
console.error(
|
||||
`[mosaic] WARNING: launch record not written: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
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) {
|
||||
console.error(
|
||||
`[mosaic] ERROR [${result.code}]: mandatory launch provenance could not be recorded; runtime was not started.`,
|
||||
);
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
function checkMosaicHome(): void {
|
||||
@@ -925,7 +948,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
cliArgs.push(...args);
|
||||
}
|
||||
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
|
||||
recordLaunch('claude', cliArgs, yolo);
|
||||
requireLaunchRecord('claude', cliArgs, yolo);
|
||||
execLeaseGatedRuntime('claude', cliArgs, process.env, yolo);
|
||||
break;
|
||||
}
|
||||
@@ -939,7 +962,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
cliArgs.push(...args);
|
||||
}
|
||||
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
|
||||
recordLaunch('codex', cliArgs, yolo);
|
||||
requireLaunchRecord('codex', cliArgs, yolo);
|
||||
execRuntime('codex', cliArgs, { ...process.env, ...harnessEnv('codex') });
|
||||
break;
|
||||
}
|
||||
@@ -948,7 +971,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}...`);
|
||||
recordLaunch('opencode', args, yolo);
|
||||
requireLaunchRecord('opencode', args, yolo);
|
||||
execRuntime('opencode', args, { ...process.env, ...harnessEnv('opencode') });
|
||||
break;
|
||||
}
|
||||
@@ -964,7 +987,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
|
||||
cliArgs.push(...args);
|
||||
}
|
||||
console.log(`[mosaic] Launching ${label}${modeStr}${missionStr}...`);
|
||||
recordLaunch('pi', cliArgs, yolo);
|
||||
requireLaunchRecord('pi', cliArgs, yolo);
|
||||
execLeaseGatedRuntime('pi', cliArgs);
|
||||
break;
|
||||
}
|
||||
@@ -1008,19 +1031,31 @@ function execLeaseGatedRuntime(
|
||||
);
|
||||
}
|
||||
|
||||
/** exec into the runtime, replacing the current process. */
|
||||
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. */
|
||||
function execRuntime(cmd: string, args: string[], env: NodeJS.ProcessEnv = process.env): void {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1030,6 +1065,15 @@ 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 = {
|
||||
@@ -1041,8 +1085,7 @@ function launchClaudexProduction(args: string[], yolo: boolean): void {
|
||||
checkSequentialThinking('claude');
|
||||
},
|
||||
composePrompt: () => buildRuntimePrompt('claude'),
|
||||
execLeaseGated: (cmdArgs, env, dangerous) =>
|
||||
execLeaseGatedRuntime('claude', cmdArgs, env, dangerous),
|
||||
execLeaseGated: execRecordedClaudexRuntime,
|
||||
};
|
||||
void launchClaudex(args, yolo, adapter);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user