Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03eda02c20 | ||
|
|
3b4055017e |
@@ -33,7 +33,6 @@ export default tseslint.config(
|
|||||||
'packages/db/vitest.config.ts',
|
'packages/db/vitest.config.ts',
|
||||||
'packages/storage/vitest.config.ts',
|
'packages/storage/vitest.config.ts',
|
||||||
'packages/mosaic/vitest.config.ts',
|
'packages/mosaic/vitest.config.ts',
|
||||||
'packages/mosaic/vitest.setup.ts',
|
|
||||||
'packages/mosaic/__tests__/*.ts',
|
'packages/mosaic/__tests__/*.ts',
|
||||||
'tools/federation-harness/*.ts',
|
'tools/federation-harness/*.ts',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
||||||
|
|
||||||
|
# Normalize the ambient umask so directory modes are a property of the installer
|
||||||
|
# and not of whatever shell invoked it (#1236). Debian/Ubuntu ship umask 002, so
|
||||||
|
# every `mkdir -p` below yielded 0775 — and the fleet env boundary rejects any
|
||||||
|
# managed directory with `mode & 0o022`, which made `mosaic fleet init --write`
|
||||||
|
# impossible on a stock install of those distros. Fedora/RHEL ship 022 and did
|
||||||
|
# not trip it, so the product worked or did not depending on the operator's
|
||||||
|
# login shell. 022 is what this script already assumes it produces: see the
|
||||||
|
# umask note in make_durable_snapshot, which restores to the ambient value
|
||||||
|
# precisely so "every later sync copy and new framework dir" gets 0644/0755.
|
||||||
|
# Now that value is 022 rather than whatever was inherited.
|
||||||
|
umask 022
|
||||||
|
|
||||||
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
|
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
|
||||||
# never an environment variable — so this opt-out can never sit silently
|
# never an environment variable — so this opt-out can never sit silently
|
||||||
# inherited in a shell profile. See #869 Point-1 C2.
|
# inherited in a shell profile. See #869 Point-1 C2.
|
||||||
@@ -696,6 +708,52 @@ sync_framework
|
|||||||
mkdir -p "$TARGET_DIR/memory"
|
mkdir -p "$TARGET_DIR/memory"
|
||||||
mkdir -p "$TARGET_DIR/credentials"
|
mkdir -p "$TARGET_DIR/credentials"
|
||||||
|
|
||||||
|
# Three directories must be 0700, not merely not-group-writable (#1236).
|
||||||
|
# The fleet code guards them with two different masks in two different
|
||||||
|
# languages, and the strict one wins:
|
||||||
|
#
|
||||||
|
# assertPrivateManagedDirectory (fleet-reconciler.js, `mode & 0o077`)
|
||||||
|
# -> MOSAIC_HOME and MOSAIC_HOME/fleet, checked before the roster lock is
|
||||||
|
# taken, so every mutating `mosaic fleet` command dies at 0755.
|
||||||
|
# assert_private_directory (tools/fleet/start-agent-session.sh, `mode & 077`)
|
||||||
|
# -> MOSAIC_HOME/fleet/agents, checked before a pane is ever spawned.
|
||||||
|
#
|
||||||
|
# Their laxer siblings (`mode & 0o022`) accept 0755, which is why normalizing
|
||||||
|
# the umask above is necessary and not sufficient — a correct umask-022 install
|
||||||
|
# still produces 0755 and still cannot run `mosaic fleet init --write`. Say the
|
||||||
|
# strict modes outright rather than inferring them from a umask.
|
||||||
|
#
|
||||||
|
# Only these. The rest of the tree is content, stays 0755, and is only ever
|
||||||
|
# reached by the 0o022 checks, which 0755 satisfies.
|
||||||
|
chmod 700 "$TARGET_DIR" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR — 'mosaic fleet' mutations will fail as unsafe-permissions."
|
||||||
|
if [[ -d "$TARGET_DIR/fleet" ]]; then
|
||||||
|
chmod 700 "$TARGET_DIR/fleet" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/fleet — 'mosaic fleet' mutations will fail as unsafe-permissions."
|
||||||
|
fi
|
||||||
|
# fleet/agents does not exist on a first install — the CLI creates it 0700 on
|
||||||
|
# demand. It is chmod'd here for the UPGRADE case: a tree built under umask 002
|
||||||
|
# has it at 0775, and the repair sweep below cannot rescue it, because stripping
|
||||||
|
# group/other write from 0755 leaves 0750 and `mode & 077` is still non-zero.
|
||||||
|
if [[ -d "$TARGET_DIR/fleet/agents" ]]; then
|
||||||
|
chmod 700 "$TARGET_DIR/fleet/agents" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/fleet/agents — agent sessions will fail to start as unsafe-permissions."
|
||||||
|
fi
|
||||||
|
# credentials/ holds secrets and was never meant to be group-readable either.
|
||||||
|
# It is not on the fleet boundary, so a failure here breaks nothing — but it is
|
||||||
|
# the one directory where a silently-failed chmod leaves secrets group-readable,
|
||||||
|
# which is precisely the failure worth a line in the output.
|
||||||
|
chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || \
|
||||||
|
warn "Could not set 0700 on $TARGET_DIR/credentials — stored secrets may be readable by other users on this host."
|
||||||
|
|
||||||
|
# Repair an existing tree. The umask above only governs directories this run
|
||||||
|
# creates, so a host installed under umask 002 before this fix keeps its 0775
|
||||||
|
# dirs through every upgrade and stays broken. Strips group/other WRITE only —
|
||||||
|
# never read or execute — so it can repair the boundary violation without
|
||||||
|
# changing who can traverse or read anything. Scoped to directories: file modes
|
||||||
|
# are the manifest's business, not this fix's.
|
||||||
|
find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w {} + 2>/dev/null || true
|
||||||
|
|
||||||
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
||||||
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
||||||
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
|
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { isHostLeaseVariable, scrubHostLeaseEnv } from './host-lease-env.js';
|
|
||||||
|
|
||||||
describe('host lease environment scrubbing', () => {
|
|
||||||
test('removes every lease variable and reports what it removed', () => {
|
|
||||||
const environment = {
|
|
||||||
MOSAIC_LEASE_GENERATION_FILE: '/run/user/1001/mosaic-lease/generation-abc.state',
|
|
||||||
MOSAIC_LEASE_SESSION_ID: 'a'.repeat(64),
|
|
||||||
MOSAIC_LEASE_BROKER_SOCKET: '/run/user/1001/mosaic-lease/broker.sock',
|
|
||||||
MOSAIC_LEASE_RUNTIME: 'claude',
|
|
||||||
MOSAIC_RUNTIME_GENERATION: '388',
|
|
||||||
PATH: '/usr/bin',
|
|
||||||
MOSAIC_AGENT_NAME: 'fred',
|
|
||||||
} as NodeJS.ProcessEnv;
|
|
||||||
|
|
||||||
expect(scrubHostLeaseEnv(environment)).toEqual([
|
|
||||||
'MOSAIC_LEASE_BROKER_SOCKET',
|
|
||||||
'MOSAIC_LEASE_GENERATION_FILE',
|
|
||||||
'MOSAIC_LEASE_RUNTIME',
|
|
||||||
'MOSAIC_LEASE_SESSION_ID',
|
|
||||||
'MOSAIC_RUNTIME_GENERATION',
|
|
||||||
]);
|
|
||||||
expect(environment).toEqual({ PATH: '/usr/bin', MOSAIC_AGENT_NAME: 'fred' });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a lease variable added later is scrubbed without being listed anywhere', () => {
|
|
||||||
// The prefix rule is the point: this is the case a hand-maintained list would miss.
|
|
||||||
const environment = { MOSAIC_LEASE_SOMETHING_NEW: 'x' } as NodeJS.ProcessEnv;
|
|
||||||
expect(scrubHostLeaseEnv(environment)).toEqual(['MOSAIC_LEASE_SOMETHING_NEW']);
|
|
||||||
expect(environment).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('leaves unrelated variables alone', () => {
|
|
||||||
const environment = {
|
|
||||||
MOSAIC_AGENT_NAME: 'fred',
|
|
||||||
MOSAIC_HOME: '/home/fred/.mosaic',
|
|
||||||
HOME: '/home/fred',
|
|
||||||
} as NodeJS.ProcessEnv;
|
|
||||||
|
|
||||||
expect(scrubHostLeaseEnv(environment)).toEqual([]);
|
|
||||||
expect(environment).toEqual({
|
|
||||||
MOSAIC_AGENT_NAME: 'fred',
|
|
||||||
MOSAIC_HOME: '/home/fred/.mosaic',
|
|
||||||
HOME: '/home/fred',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('classifies by prefix, not by an exact list', () => {
|
|
||||||
expect(isHostLeaseVariable('MOSAIC_LEASE_ANYTHING')).toBe(true);
|
|
||||||
expect(isHostLeaseVariable('MOSAIC_RUNTIME_GENERATION')).toBe(true);
|
|
||||||
expect(isHostLeaseVariable('MOSAIC_RUNTIME')).toBe(false);
|
|
||||||
expect(isHostLeaseVariable('LEASE_MOSAIC_X')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wiring check. On a clean checkout or in CI these variables are unset, so this
|
|
||||||
// passes whether or not vitest.setup.ts is registered -- it is worth little there and
|
|
||||||
// is not claimed to be. Its value is inside a Mosaic-managed agent seat, where the
|
|
||||||
// variables ARE set and this is the assertion that catches the setup file being
|
|
||||||
// dropped from vitest.config.ts. That is the environment the leak was found in.
|
|
||||||
test('the suite does not run with the host lease identity in scope', () => {
|
|
||||||
expect(Object.keys(process.env).filter(isHostLeaseVariable)).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* Remove the host's live lease identity from an environment before tests run.
|
|
||||||
*
|
|
||||||
* The lease specs start their own broker on a private socket and then spawn the real
|
|
||||||
* hook scripts against it, building each child's environment as `{ ...process.env, <the
|
|
||||||
* few vars this case cares about> }`. That spread is the problem: when the suite runs
|
|
||||||
* inside a Mosaic-managed agent seat, `process.env` already carries that seat's real
|
|
||||||
* lease identity, and the parts the spread does not override survive into the child.
|
|
||||||
*
|
|
||||||
* `MOSAIC_LEASE_GENERATION_FILE` is the one that bites. `read_runtime_generation()`
|
|
||||||
* prefers that file over `MOSAIC_RUNTIME_GENERATION`, so a case that carefully sets
|
|
||||||
* `MOSAIC_RUNTIME_GENERATION: '1'` is silently overruled by the host's generation
|
|
||||||
* counter -- which on a long-lived seat is in the hundreds. The revoke client reads it,
|
|
||||||
* sends it, and the test broker advances the session to that generation. Every later
|
|
||||||
* `authorize` in the case sends generation 1, is now behind, and is denied with
|
|
||||||
* `STALE_GENERATION` instead of the `MUTATOR_UNVERIFIED` the case asserts. The gate
|
|
||||||
* still denies, so this is not a hole in the product -- but it turns four acceptance
|
|
||||||
* tests red for a reason that has nothing to do with the code under test.
|
|
||||||
*
|
|
||||||
* It only reproduces inside a managed seat. On a clean checkout or in CI these vars are
|
|
||||||
* unset, the suite is green, and the leak is invisible -- which is why it survived.
|
|
||||||
*
|
|
||||||
* Scrubbing by prefix rather than by an explicit list is deliberate: any lease variable
|
|
||||||
* added later leaks by exactly the same route, and a list would have to be remembered.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const HOST_LEASE_PREFIX = 'MOSAIC_LEASE_';
|
|
||||||
const HOST_LEASE_EXTRA = ['MOSAIC_RUNTIME_GENERATION'];
|
|
||||||
|
|
||||||
export function isHostLeaseVariable(name: string): boolean {
|
|
||||||
return name.startsWith(HOST_LEASE_PREFIX) || HOST_LEASE_EXTRA.includes(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Deletes the host lease variables from `environment`; returns the names removed. */
|
|
||||||
export function scrubHostLeaseEnv(environment: NodeJS.ProcessEnv): string[] {
|
|
||||||
const removed = Object.keys(environment).filter(isHostLeaseVariable);
|
|
||||||
for (const name of removed) {
|
|
||||||
delete environment[name];
|
|
||||||
}
|
|
||||||
return removed.sort();
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
testTimeout: 30_000,
|
testTimeout: 30_000,
|
||||||
setupFiles: ['./vitest.setup.ts'],
|
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
include: ['src/commands/skill.ts', 'src/lease-broker/broker-test-client.ts'],
|
include: ['src/commands/skill.ts', 'src/lease-broker/broker-test-client.ts'],
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { scrubHostLeaseEnv } from './src/test-support/host-lease-env.js';
|
|
||||||
|
|
||||||
// Runs before every spec file in this package. See src/test-support/host-lease-env.ts
|
|
||||||
// for why the host's lease identity must not reach a spawned hook process.
|
|
||||||
scrubHostLeaseEnv(process.env);
|
|
||||||
Reference in New Issue
Block a user